diff --git a/PVE/API2/Ceph.pm b/PVE/API2/Ceph.pm new file mode 100644 index 00000000..fb37c443 --- /dev/null +++ b/PVE/API2/Ceph.pm @@ -0,0 +1,1281 @@ +package PVE::API2::Ceph; + +use strict; +use warnings; +use File::Basename; +use File::Path; +use POSIX qw (LONG_MAX); +use Cwd qw(abs_path); +use IO::Dir; +use UUID; + +use PVE::SafeSyslog; +use PVE::Tools qw(extract_param run_command file_get_contents file_read_firstline dir_glob_regex dir_glob_foreach); +use PVE::Exception qw(raise raise_param_exc); +use PVE::INotify; +use PVE::Cluster qw(cfs_lock_file cfs_read_file cfs_write_file); +use PVE::AccessControl; +use PVE::Storage; +use PVE::RESTHandler; +use PVE::RPCEnvironment; +use PVE::JSONSchema qw(get_standard_option); +use JSON; + +use base qw(PVE::RESTHandler); + +use Data::Dumper; # fixme: remove + +my $ccname = 'ceph'; # ceph cluster name +my $ceph_cfgdir = "/etc/ceph"; +my $pve_ceph_cfgpath = "/etc/pve/$ccname.conf"; +my $ceph_cfgpath = "$ceph_cfgdir/$ccname.conf"; +my $pve_mon_key_path = "/etc/pve/priv/$ccname.mon.keyring"; +my $pve_ckeyring_path = "/etc/pve/priv/$ccname.client.admin.keyring"; + +my $ceph_bootstrap_osd_keyring = "/var/lib/ceph/bootstrap-osd/$ccname.keyring"; +my $ceph_bootstrap_mds_keyring = "/var/lib/ceph/bootstrap-mds/$ccname.keyring"; + +my $ceph_bin = "/usr/bin/ceph"; + +sub purge_all_ceph_files { + # fixme: this is very dangerous - should we really support this function? + + unlink $ceph_cfgpath; + + unlink $pve_ceph_cfgpath; + unlink $pve_ckeyring_path; + unlink $pve_mon_key_path; + + unlink $ceph_bootstrap_osd_keyring; + unlink $ceph_bootstrap_mds_keyring; + + system("rm -rf /var/lib/ceph/mon/ceph-*"); + + # remove osd? + +} + +my $check_ceph_installed = sub { + my ($noerr) = @_; + + if (! -x $ceph_bin) { + die "ceph binaries not installed\n" if !$noerr; + return undef; + } + + return 1; +}; + +my $check_ceph_inited = sub { + my ($noerr) = @_; + + return undef if !&$check_ceph_installed($noerr); + + if (! -f $pve_ceph_cfgpath) { + die "pveceph configuration not initialized\n" if !$noerr; + return undef; + } + + return 1; +}; + +my $check_ceph_enabled = sub { + my ($noerr) = @_; + + return undef if !&$check_ceph_inited($noerr); + + if (! -f $ceph_cfgpath) { + die "pveceph configuration not enabled\n" if !$noerr; + return undef; + } + + return 1; +}; + +my $parse_ceph_config = sub { + my ($filename) = @_; + + my $cfg = {}; + + return $cfg if ! -f $filename; + + my $fh = IO::File->new($filename, "r") || + die "unable to open '$filename' - $!\n"; + + my $section; + + while (defined(my $line = <$fh>)) { + $line =~ s/[;#].*$//; + $line =~ s/^\s+//; + $line =~ s/\s+$//; + next if !$line; + + $section = $1 if $line =~ m/^\[(\S+)\]$/; + if (!$section) { + warn "no section - skip: $line\n"; + next; + } + + if ($line =~ m/^(.*\S)\s*=\s*(\S.*)$/) { + $cfg->{$section}->{$1} = $2; + } + + } + + return $cfg; +}; + +my $run_ceph_cmd = sub { + my ($cmd, %params) = @_; + + my $timeout = 5; + + run_command(['ceph', '-c', $pve_ceph_cfgpath, + '--connect-timeout', $timeout, + @$cmd], %params); +}; + +my $run_ceph_cmd_text = sub { + my ($cmd, %opts) = @_; + + my $out = ''; + + my $quiet = delete $opts{quiet}; + + my $parser = sub { + my $line = shift; + $out .= "$line\n"; + }; + + my $errfunc = sub { + my $line = shift; + print "$line\n" if !$quiet; + }; + + &$run_ceph_cmd($cmd, outfunc => $parser, errfunc => $errfunc); + + return $out; +}; + +my $run_ceph_cmd_json = sub { + my ($cmd, %opts) = @_; + + my $json = &$run_ceph_cmd_text([@$cmd, '--format', 'json'], %opts); + + return decode_json($json); +}; + +sub ceph_mon_status { + my ($quiet) = @_; + + return &$run_ceph_cmd_json(['mon_status'], quiet => $quiet); + +} + +my $ceph_osd_status = sub { + my ($quiet) = @_; + + return &$run_ceph_cmd_json(['osd', 'dump'], quiet => $quiet); +}; + +my $write_ceph_config = sub { + my ($cfg) = @_; + + my $out = ''; + + my $cond_write_sec = sub { + my $re = shift; + + foreach my $section (keys %$cfg) { + next if $section !~ m/^$re$/; + $out .= "[$section]\n"; + foreach my $key (sort keys %{$cfg->{$section}}) { + $out .= "\t $key = $cfg->{$section}->{$key}\n"; + } + $out .= "\n"; + } + }; + + &$cond_write_sec('global'); + &$cond_write_sec('mon'); + &$cond_write_sec('osd'); + &$cond_write_sec('mon\..*'); + &$cond_write_sec('osd\..*'); + + PVE::Tools::file_set_contents($pve_ceph_cfgpath, $out); +}; + +my $setup_pve_symlinks = sub { + # fail if we find a real file instead of a link + if (-f $ceph_cfgpath) { + my $lnk = readlink($ceph_cfgpath); + die "file '$ceph_cfgpath' already exists\n" + if !$lnk || $lnk ne $pve_ceph_cfgpath; + } else { + symlink($pve_ceph_cfgpath, $ceph_cfgpath) || + die "unable to create symlink '$ceph_cfgpath' - $!\n"; + } +}; + +my $ceph_service_cmd = sub { + run_command(['service', 'ceph', '-c', $pve_ceph_cfgpath, @_]); +}; + + +sub list_disks { + my $disklist = {}; + + my $fd = IO::File->new("/proc/mounts", "r") || + die "unable to open /proc/mounts - $!\n"; + + my $mounted = {}; + + while (defined(my $line = <$fd>)) { + my ($dev, $path, $fstype) = split(/\s+/, $line); + next if !($dev && $path && $fstype); + next if $dev !~ m|^/dev/|; + my $real_dev = abs_path($dev); + $mounted->{$real_dev} = $path; + } + close($fd); + + my $dev_is_mounted = sub { + my ($dev) = @_; + return $mounted->{$dev}; + }; + + my $dir_is_epmty = sub { + my ($dir) = @_; + + my $dh = IO::Dir->new ($dir); + return 1 if !$dh; + + while (defined(my $tmp = $dh->read)) { + next if $tmp eq '.' || $tmp eq '..'; + $dh->close; + return 0; + } + $dh->close; + return 1; + }; + + dir_glob_foreach('/sys/block', '.*', sub { + my ($dev) = @_; + + return if $dev eq '.'; + return if $dev eq '..'; + + return if $dev =~ m|^ram\d+$|; # skip ram devices + return if $dev =~ m|^loop\d+$|; # skip loop devices + return if $dev =~ m|^md\d+$|; # skip md devices + return if $dev =~ m|^dm-.*$|; # skip dm related things + return if $dev =~ m|^fd\d+$|; # skip Floppy + return if $dev =~ m|^sr\d+$|; # skip CDs + + my $devdir = "/sys/block/$dev/device"; + return if ! -d $devdir; + + my $size = file_read_firstline("/sys/block/$dev/size"); + return if !$size; + + $size = $size * 512; + + my $info = `udevadm info --path /sys/block/$dev --query all`; + return if !$info; + + return if $info !~ m/^E: DEVTYPE=disk$/m; + return if $info =~ m/^E: ID_CDROM/m; + + my $serial = 'unknown'; + if ($info =~ m/^E: ID_SERIAL_SHORT=(\S+)$/m) { + $serial = $1; + } + + my $vendor = file_read_firstline("$devdir/vendor") || 'unknown'; + my $model = file_read_firstline("$devdir/model") || 'unknown'; + + my $used = &$dir_is_epmty("/sys/block/$dev/holders") ? 0 : 1; + + $used = 1 if &$dev_is_mounted("/dev/$dev"); + + $disklist->{$dev} = { + vendor => $vendor, + model => $model, + size => $size, + serial => $serial, + }; + + my $osdid = -1; + + dir_glob_foreach("/sys/block/$dev", "$dev.+", sub { + my ($part) = @_; + if (!&$dir_is_epmty("/sys/block/$dev/$part/holders")) { + $used = 1; + } + if (my $mp = &$dev_is_mounted("/dev/$part")) { + $used = 1; + if ($mp =~ m|^/var/lib/ceph/osd/ceph-(\d+)$|) { + $osdid = $1; + } + } + }); + + $disklist->{$dev}->{used} = $used; + $disklist->{$dev}->{osdid} = $osdid; + }); + + return $disklist; +} + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "Directory index.", + permissions => { user => 'all' }, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => {}, + }, + links => [ { rel => 'child', href => "{name}" } ], + }, + code => sub { + my ($param) = @_; + + my $result = [ + { name => 'init' }, + { name => 'mon' }, + { name => 'osd' }, + { name => 'pools' }, + { name => 'stop' }, + { name => 'start' }, + { name => 'status' }, + { name => 'crush' }, + { name => 'config' }, + { name => 'log' }, + { name => 'disks' }, + ]; + + return $result; + }}); + +__PACKAGE__->register_method ({ + name => 'disks', + path => 'disks', + method => 'GET', + description => "List local disks.", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + dev => { type => 'string' }, + used => { type => 'boolean' }, + size => { type => 'integer' }, + osdid => { type => 'integer' }, + vendor => { type => 'string', optional => 1 }, + model => { type => 'string', optional => 1 }, + serial => { type => 'string', optional => 1 }, + }, + }, + # links => [ { rel => 'child', href => "{}" } ], + }, + code => sub { + my ($param) = @_; + + &$check_ceph_inited(); + + my $res = list_disks(); + + return PVE::RESTHandler::hash_to_array($res, 'dev'); + }}); + +__PACKAGE__->register_method ({ + name => 'config', + path => 'config', + method => 'GET', + description => "Get Ceph configuration.", + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { type => 'string' }, + code => sub { + my ($param) = @_; + + &$check_ceph_inited(); + + return PVE::Tools::file_get_contents($pve_ceph_cfgpath); + + }}); + +__PACKAGE__->register_method ({ + name => 'listmon', + path => 'mon', + method => 'GET', + description => "Get Ceph monitor list.", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + name => { type => 'string' }, + addr => { type => 'string' }, + }, + }, + links => [ { rel => 'child', href => "{name}" } ], + }, + code => sub { + my ($param) = @_; + + &$check_ceph_inited(); + + my $res = []; + + my $cfg = &$parse_ceph_config($pve_ceph_cfgpath); + + my $monhash = {}; + foreach my $section (keys %$cfg) { + my $d = $cfg->{$section}; + if ($section =~ m/^mon\.(\S+)$/) { + my $monid = $1; + if ($d->{'mon addr'} && $d->{'host'}) { + $monhash->{$monid} = { + addr => $d->{'mon addr'}, + host => $d->{'host'}, + name => $monid, + } + } + } + } + + eval { + my $monstat = ceph_mon_status(); + my $mons = $monstat->{monmap}->{mons}; + foreach my $d (@$mons) { + next if !defined($d->{name}); + $monhash->{$d->{name}}->{rank} = $d->{rank}; + $monhash->{$d->{name}}->{addr} = $d->{addr}; + if (grep { $_ eq $d->{rank} } @{$monstat->{quorum}}) { + $monhash->{$d->{name}}->{quorum} = 1; + } + } + }; + warn $@ if $@; + + return PVE::RESTHandler::hash_to_array($monhash, 'name'); + }}); + +__PACKAGE__->register_method ({ + name => 'init', + path => 'init', + method => 'POST', + description => "Create initial ceph default configuration and setup symlinks.", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + size => { + description => 'Number of replicas per object', + type => 'integer', + default => 2, + optional => 1, + minimum => 1, + maximum => 3, + }, + pg_bits => { + description => "Placement group bits, used to specify the default number of placement groups (Note: 'osd pool default pg num' does not work for deafult pools)", + type => 'integer', + default => 6, + optional => 1, + minimum => 6, + maximum => 14, + }, + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + &$check_ceph_installed(); + + # simply load old config if it already exists + my $cfg = &$parse_ceph_config($pve_ceph_cfgpath); + + if (!$cfg->{global}) { + + my $fsid; + my $uuid; + + UUID::generate($uuid); + UUID::unparse($uuid, $fsid); + + $cfg->{global} = { + 'fsid' => $fsid, + 'auth supported' => 'cephx', + 'auth cluster required' => 'cephx', + 'auth service required' => 'cephx', + 'auth client required' => 'cephx', + 'filestore xattr use omap' => 'true', + 'osd journal size' => '1024', + 'osd pool default min size' => 1, + }; + + # this does not work for default pools + #'osd pool default pg num' => $pg_num, + #'osd pool default pgp num' => $pg_num, + } + + $cfg->{global}->{keyring} = '/etc/pve/priv/$cluster.$name.keyring'; + $cfg->{osd}->{keyring} = '/var/lib/ceph/osd/ceph-$id/keyring'; + + $cfg->{global}->{'osd pool default size'} = $param->{size} if $param->{size}; + + if ($param->{pg_bits}) { + $cfg->{global}->{'osd pg bits'} = $param->{pg_bits}; + $cfg->{global}->{'osd pgp bits'} = $param->{pg_bits}; + } + + &$write_ceph_config($cfg); + + &$setup_pve_symlinks(); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'createmon', + path => 'mon', + method => 'POST', + description => "Create Ceph Monitor", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { type => 'string' }, + code => sub { + my ($param) = @_; + + &$check_ceph_inited(); + + &$setup_pve_symlinks(); + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + my $cfg = &$parse_ceph_config($pve_ceph_cfgpath); + + my $moncount = 0; + + my $monaddrhash = {}; + + foreach my $section (keys %$cfg) { + next if $section eq 'global'; + my $d = $cfg->{$section}; + if ($section =~ m/^mon\./) { + $moncount++; + if ($d->{'mon addr'}) { + $monaddrhash->{$d->{'mon addr'}} = $section; + } + } + } + + my $monid; + for (my $i = 0; $i < 7; $i++) { + if (!$cfg->{"mon.$i"}) { + $monid = $i; + last; + } + } + die "unable to find usable monitor id\n" if !defined($monid); + + my $monsection = "mon.$monid"; + my $monaddr = PVE::Cluster::remote_node_ip($param->{node}) . ":6789"; + my $monname = $param->{node}; + + die "monitor '$monsection' already exists\n" if $cfg->{$monsection}; + die "monitor address '$monaddr' already in use by '$monaddrhash->{$monaddr}'\n" + if $monaddrhash->{$monaddr}; + + my $worker = sub { + my $upid = shift; + + if (! -f $pve_ckeyring_path) { + run_command("ceph-authtool $pve_ckeyring_path --create-keyring " . + "--gen-key -n client.admin"); + } + + if (! -f $pve_mon_key_path) { + run_command("cp $pve_ckeyring_path $pve_mon_key_path.tmp"); + run_command("ceph-authtool $pve_mon_key_path.tmp -n client.admin --set-uid=0 " . + "--cap mds 'allow' " . + "--cap osd 'allow *' " . + "--cap mon 'allow *'"); + run_command("ceph-authtool $pve_mon_key_path.tmp --gen-key -n mon. --cap mon 'allow *'"); + run_command("mv $pve_mon_key_path.tmp $pve_mon_key_path"); + } + + my $mondir = "/var/lib/ceph/mon/$ccname-$monid"; + -d $mondir && die "monitor filesystem '$mondir' already exist\n"; + + my $monmap = "/tmp/monmap"; + + eval { + mkdir $mondir; + + if ($moncount > 0) { + my $monstat = ceph_mon_status(); # online test + &$run_ceph_cmd(['mon', 'getmap', '-o', $monmap]); + } else { + run_command("monmaptool --create --clobber --add $monid $monaddr --print $monmap"); + } + + run_command("ceph-mon --mkfs -i $monid --monmap $monmap --keyring $pve_mon_key_path"); + }; + my $err = $@; + unlink $monmap; + if ($err) { + File::Path::remove_tree($mondir); + die $err; + } + + $cfg->{$monsection} = { + 'host' => $monname, + 'mon addr' => $monaddr, + }; + + &$write_ceph_config($cfg); + + &$ceph_service_cmd('start', $monsection); + }; + + return $rpcenv->fork_worker('cephcreatemon', $monsection, $authuser, $worker); + }}); + +__PACKAGE__->register_method ({ + name => 'destroymon', + path => 'mon/{monid}', + method => 'DELETE', + description => "Destroy Ceph monitor.", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + monid => { + description => 'Monitor ID', + type => 'integer', + }, + }, + }, + returns => { type => 'string' }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + &$check_ceph_inited(); + + my $cfg = &$parse_ceph_config($pve_ceph_cfgpath); + + my $monid = $param->{monid}; + my $monsection = "mon.$monid"; + + my $monstat = ceph_mon_status(); + my $monlist = $monstat->{monmap}->{mons}; + + die "no such monitor id '$monid'\n" + if !defined($cfg->{$monsection}); + + + my $mondir = "/var/lib/ceph/mon/$ccname-$monid"; + -d $mondir || die "monitor filesystem '$mondir' does not exist on this node\n"; + + die "can't remove last monitor\n" if scalar(@$monlist) <= 1; + + my $worker = sub { + my $upid = shift; + + &$run_ceph_cmd(['mon', 'remove', $monid]); + + eval { &$ceph_service_cmd('stop', $monsection); }; + warn $@ if $@; + + delete $cfg->{$monsection}; + &$write_ceph_config($cfg); + File::Path::remove_tree($mondir); + }; + + return $rpcenv->fork_worker('cephdestroymon', $monsection, $authuser, $worker); + }}); + +__PACKAGE__->register_method ({ + name => 'stop', + path => 'stop', + method => 'POST', + description => "Stop ceph services.", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + service => { + description => 'Ceph service name.', + type => 'string', + optional => 1, + pattern => '(mon|mds|osd)\.[A-Za-z0-9]{1,32}', + }, + }, + }, + returns => { type => 'string' }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + &$check_ceph_inited(); + + my $cfg = &$parse_ceph_config($pve_ceph_cfgpath); + scalar(keys %$cfg) || die "no configuration\n"; + + my $worker = sub { + my $upid = shift; + + my $cmd = ['stop']; + if ($param->{service}) { + push @$cmd, $param->{service}; + } + + &$ceph_service_cmd(@$cmd); + }; + + return $rpcenv->fork_worker('srvstop', $param->{service} || 'ceph', + $authuser, $worker); + }}); + +__PACKAGE__->register_method ({ + name => 'start', + path => 'start', + method => 'POST', + description => "Start ceph services.", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + service => { + description => 'Ceph service name.', + type => 'string', + optional => 1, + pattern => '(mon|mds|osd)\.[A-Za-z0-9]{1,32}', + }, + }, + }, + returns => { type => 'string' }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + &$check_ceph_inited(); + + my $cfg = &$parse_ceph_config($pve_ceph_cfgpath); + scalar(keys %$cfg) || die "no configuration\n"; + + my $worker = sub { + my $upid = shift; + + my $cmd = ['start']; + if ($param->{service}) { + push @$cmd, $param->{service}; + } + + &$ceph_service_cmd(@$cmd); + }; + + return $rpcenv->fork_worker('srvstart', $param->{service} || 'ceph', + $authuser, $worker); + }}); + +__PACKAGE__->register_method ({ + name => 'status', + path => 'status', + method => 'GET', + description => "Get ceph status.", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { type => 'object' }, + code => sub { + my ($param) = @_; + + &$check_ceph_enabled(); + + return &$run_ceph_cmd_json(['status'], quiet => 1); + }}); + +__PACKAGE__->register_method ({ + name => 'lspools', + path => 'pools', + method => 'GET', + description => "List all pools.", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + pool => { type => 'integer' }, + pool_name => { type => 'string' }, + size => { type => 'integer' }, + }, + }, + links => [ { rel => 'child', href => "{pool_name}" } ], + }, + code => sub { + my ($param) = @_; + + &$check_ceph_inited(); + + my $res = &$run_ceph_cmd_json(['osd', 'dump'], quiet => 1); + + my $data = []; + foreach my $e (@{$res->{pools}}) { + my $d = {}; + foreach my $attr (qw(pool pool_name size min_size pg_num crush_ruleset)) { + $d->{$attr} = $e->{$attr} if defined($e->{$attr}); + } + push @$data, $d; + } + + return $data; + }}); + +__PACKAGE__->register_method ({ + name => 'createpool', + path => 'pools', + method => 'POST', + description => "Create POOL", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + name => { + description => "The name of the pool. It must be unique.", + type => 'string', + }, + size => { + description => 'Number of replicas per object', + type => 'integer', + default => 2, + optional => 1, + minimum => 1, + maximum => 3, + }, + pg_num => { + description => "Number of placement groups.", + type => 'integer', + default => 512, + optional => 1, + minimum => 8, + maximum => 32768, + }, + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + &$check_ceph_inited(); + + die "not fully configured - missing '$pve_ckeyring_path'\n" + if ! -f $pve_ckeyring_path; + + my $pg_num = $param->{pg_num} || 512; + my $size = $param->{size} || 2; + + &$run_ceph_cmd(['osd', 'pool', 'create', $param->{name}, $pg_num]); + + &$run_ceph_cmd(['osd', 'pool', 'set', $param->{name}, 'size', $size]); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'destroypool', + path => 'pools/{name}', + method => 'DELETE', + description => "Destroy pool", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + name => { + description => "The name of the pool. It must be unique.", + type => 'string', + }, + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + &$check_ceph_inited(); + + &$run_ceph_cmd(['osd', 'pool', 'delete', $param->{name}, $param->{name}, '--yes-i-really-really-mean-it']); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'listosd', + path => 'osd', + method => 'GET', + description => "Get Ceph osd list/tree.", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => "object", + }, + code => sub { + my ($param) = @_; + + &$check_ceph_inited(); + + my $res = &$run_ceph_cmd_json(['osd', 'tree'], quiet => 1); + + die "no tree nodes found\n" if !($res && $res->{nodes}); + + my $nodes = {}; + my $newnodes = {}; + foreach my $e (@{$res->{nodes}}) { + $nodes->{$e->{id}} = $e; + + my $new = { + id => $e->{id}, + name => $e->{name}, + type => $e->{type} + }; + + foreach my $opt (qw(status crush_weight reweight)) { + $new->{$opt} = $e->{$opt} if defined($e->{$opt}); + } + + $newnodes->{$e->{id}} = $new; + } + + foreach my $e (@{$res->{nodes}}) { + my $new = $newnodes->{$e->{id}}; + if ($e->{children} && scalar(@{$e->{children}})) { + $new->{children} = []; + $new->{leaf} = 0; + foreach my $cid (@{$e->{children}}) { + $nodes->{$cid}->{parent} = $e->{id}; + if ($nodes->{$cid}->{type} eq 'osd' && + $e->{type} eq 'host') { + $newnodes->{$cid}->{host} = $e->{name}; + } + push @{$new->{children}}, $newnodes->{$cid}; + } + } else { + $new->{leaf} = ($e->{id} >= 0) ? 1 : 0; + } + } + + my $rootnode; + foreach my $e (@{$res->{nodes}}) { + if (!$nodes->{$e->{id}}->{parent}) { + $rootnode = $newnodes->{$e->{id}}; + last; + } + } + + die "no root node\n" if !$rootnode; + + my $data = { root => $rootnode }; + + return $data; + }}); + +__PACKAGE__->register_method ({ + name => 'createosd', + path => 'osd', + method => 'POST', + description => "Create OSD", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + dev => { + description => "Block device name.", + type => 'string', + } + }, + }, + returns => { type => 'string' }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + &$check_ceph_inited(); + + &$setup_pve_symlinks(); + + -b $param->{dev} || die "no such block device '$param->{dev}'\n"; + + my $disklist = list_disks(); + + my $devname = $param->{dev}; + $devname =~ s|/dev/||; + + my $diskinfo = $disklist->{$devname}; + die "unable to get device info for '$devname'\n" + if !$diskinfo; + + die "device '$param->{dev}' is in use\n" + if $diskinfo->{used}; + + my $monstat = ceph_mon_status(1); + die "unable to get fsid\n" if !$monstat->{monmap} || !$monstat->{monmap}->{fsid}; + my $fsid = $monstat->{monmap}->{fsid}; + + if (! -f $ceph_bootstrap_osd_keyring) { + &$run_ceph_cmd(['auth', 'get', 'client.bootstrap-osd', '-o', $ceph_bootstrap_osd_keyring]); + }; + + my $worker = sub { + my $upid = shift; + + print "create OSD on $param->{dev}\n"; + + run_command(['ceph-disk', 'prepare', '--zap-disk', '--fs-type', 'xfs', + '--cluster', $ccname, '--cluster-uuid', $fsid, + '--', $param->{dev}]); + }; + + return $rpcenv->fork_worker('cephcreateods', $param->{dev}, $authuser, $worker); + }}); + +__PACKAGE__->register_method ({ + name => 'destroyosd', + path => 'osd/{osdid}', + method => 'DELETE', + description => "Destroy OSD", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + osdid => { + description => 'OSD ID', + type => 'integer', + }, + }, + }, + returns => { type => 'string' }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + &$check_ceph_inited(); + + my $osdid = $param->{osdid}; + + # fixme: not 100% sure what we should do here + + my $stat = &$ceph_osd_status(); + + my $osdlist = $stat->{osds} || []; + + my $osdstat; + foreach my $d (@$osdlist) { + if ($d->{osd} == $osdid) { + $osdstat = $d; + last; + } + } + die "no such OSD '$osdid'\n" if !$osdstat; + + die "osd is in use (in == 1)\n" if $osdstat->{in}; + #&$run_ceph_cmd(['osd', 'out', $osdid]); + + die "osd is still runnung (up == 1)\n" if $osdstat->{up}; + + my $osdsection = "osd.$osdid"; + + my $worker = sub { + my $upid = shift; + + print "destroy OSD $param->{osdid}\n"; + + eval { &$ceph_service_cmd('stop', $osdsection); }; + warn $@ if $@; + + print "Remove $osdsection from the CRUSH map\n"; + &$run_ceph_cmd(['osd', 'crush', 'remove', $osdid]); + + print "Remove the $osdsection authentication key.\n"; + &$run_ceph_cmd(['auth', 'del', $osdsection]); + + print "Remove OSD $osdsection\n"; + &$run_ceph_cmd(['osd', 'rm', $osdid]); + }; + + return $rpcenv->fork_worker('cephdestroyods', $osdsection, $authuser, $worker); + }}); + +__PACKAGE__->register_method ({ + name => 'crush', + path => 'crush', + method => 'GET', + description => "Get OSD crush map", + proxyto => 'node', + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { type => 'string' }, + code => sub { + my ($param) = @_; + + &$check_ceph_inited(); + + my $txt = &$run_ceph_cmd_text(['osd', 'crush', 'dump'], quiet => 1); + + return $txt; + }}); + +__PACKAGE__->register_method({ + name => 'log', + path => 'log', + method => 'GET', + description => "Read ceph log", + proxyto => 'node', + permissions => { + check => ['perm', '/nodes/{node}', [ 'Sys.Syslog' ]], + }, + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + start => { + type => 'integer', + minimum => 0, + optional => 1, + }, + limit => { + type => 'integer', + minimum => 0, + optional => 1, + }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + n => { + description=> "Line number", + type=> 'integer', + }, + t => { + description=> "Line text", + type => 'string', + } + } + } + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $user = $rpcenv->get_user(); + my $node = $param->{node}; + + my $logfile = "/var/log/ceph/ceph.log"; + my ($count, $lines) = PVE::Tools::dump_logfile($logfile, $param->{start}, $param->{limit}); + + $rpcenv->set_result_attrib('total', $count); + + return $lines; + }}); + + diff --git a/PVE/API2/Makefile b/PVE/API2/Makefile index bd979182..dfc2d387 100644 --- a/PVE/API2/Makefile +++ b/PVE/API2/Makefile @@ -1,6 +1,7 @@ include ../../defines.mk PERLSOURCE = \ + Ceph.pm \ APT.pm \ Subscription.pm \ VZDump.pm \ diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm index 56b53c67..e29082e7 100644 --- a/PVE/API2/Nodes.pm +++ b/PVE/API2/Nodes.pm @@ -32,6 +32,7 @@ use PVE::API2::Qemu; use PVE::API2::OpenVZ; use PVE::API2::VZDump; use PVE::API2::APT; +use PVE::API2::Ceph; use JSON; use base qw(PVE::RESTHandler); @@ -41,6 +42,11 @@ __PACKAGE__->register_method ({ path => 'qemu', }); +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Ceph", + path => 'ceph', +}); + __PACKAGE__->register_method ({ subclass => "PVE::API2::OpenVZ", path => 'openvz', @@ -110,6 +116,7 @@ __PACKAGE__->register_method ({ my ($param) = @_; my $result = [ + { name => 'ceph' }, { name => 'apt' }, { name => 'version' }, { name => 'syslog' }, diff --git a/bin/Makefile b/bin/Makefile index 1b0a3bc8..dc1f8603 100644 --- a/bin/Makefile +++ b/bin/Makefile @@ -3,6 +3,7 @@ include ../defines.mk SUBDIRS = init.d cron ocf test SCRIPTS = \ + pveceph \ vzdump \ vzrestore \ pvestatd \ @@ -20,6 +21,7 @@ SCRIPTS = \ pveperf MANS = \ + pveceph.1 \ pvectl.1 \ vzdump.1 \ vzrestore.1 \ @@ -44,6 +46,9 @@ all: ${MANS} pvemailforward pvectl.1.pod: pvectl perl -I.. ./pvectl printmanpod >$@ +pveceph.1.pod: pveceph + perl -I.. ./pveceph printmanpod >$@ + vzdump.1.pod: vzdump perl -I.. -T ./vzdump printmanpod >$@ diff --git a/bin/pveceph b/bin/pveceph new file mode 100755 index 00000000..58ec71d6 --- /dev/null +++ b/bin/pveceph @@ -0,0 +1,162 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Getopt::Long; +use Fcntl ':flock'; +use File::Path; +use IO::File; +use JSON; +use Data::Dumper; + +use PVE::SafeSyslog; +use PVE::Cluster; +use PVE::INotify; +use PVE::RPCEnvironment; +use PVE::Storage; +use PVE::Tools qw(run_command); +use PVE::JSONSchema qw(get_standard_option); +use PVE::API2::Ceph; + +use PVE::CLIHandler; + +use base qw(PVE::CLIHandler); + +$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin'; + +initlog ('pveceph'); + +die "please run as root\n" if $> != 0; + +PVE::INotify::inotify_init(); + +my $rpcenv = PVE::RPCEnvironment->init('cli'); + +$rpcenv->init_request(); +$rpcenv->set_language($ENV{LANG}); +$rpcenv->set_user('root@pam'); + +my $upid_exit = sub { + my $upid = shift; + my $status = PVE::Tools::upid_read_status($upid); + exit($status eq 'OK' ? 0 : -1); +}; + +my $nodename = PVE::INotify::nodename(); + +__PACKAGE__->register_method ({ + name => 'purge', + path => 'purge', + method => 'POST', + description => "Destroy ceph related data and configuration files.", + parameters => { + additionalProperties => 0, + properties => { + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $monstat; + + eval { $monstat = PVE::API2::Ceph::ceph_mon_status(1); }; + my $err = $@; + + die "detected running ceph services- unable to purge data\n" + if !$err; + + # fixme: this is dangerous - should we really support this function? + PVE::API2::Ceph::purge_all_ceph_files(); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'install', + path => 'install', + method => 'POST', + description => "Install ceph related packages.", + parameters => { + additionalProperties => 0, + properties => { + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $cephver = 'emperor'; + + local $ENV{DEBIAN_FRONTEND} = 'noninteractive'; + + my $keyurl = "https://ceph.com/git/?p=ceph.git;a=blob_plain;f=keys/release.asc"; + + print "download and import ceph reqpository keys\n"; + system("wget -q -O- '$keyurl'| apt-key add - 2>&1 >/dev/null") == 0 || + die "unable to download ceph release key\n"; + + + my $source = "deb http://ceph.com/debian-$cephver wheezy main\n"; + + PVE::Tools::file_set_contents("/etc/apt/sources.list.d/ceph.list", $source); + + print "update available package list\n"; + eval { run_command(['apt-get', '-q', 'update'], outfunc => sub {}, errfunc => sub {}); }; + + run_command(['apt-get', '-q', '--assume-yes', '--no-install-recommends', + '-o', 'Dpkg::Options::=--force-confnew', + 'install', '--', + 'ceph', 'ceph-common', 'gdisk']); + + return undef; + }}); + +my $cmddef = { + init => [ 'PVE::API2::Ceph', 'init', [], { node => $nodename } ], + lspools => [ 'PVE::API2::Ceph', 'lspools', [], { node => $nodename }, sub { + my $res = shift; + + printf("%-20s %10s %10s\n", "Name", "size", "pg_num"); + foreach my $p (sort {$a->{pool_name} cmp $b->{pool_name}} @$res) { + printf("%-20s %10d %10d\n", $p->{pool_name}, $p->{size}, $p->{pg_num}); + } + }], + createpool => [ 'PVE::API2::Ceph', 'createpool', ['name'], { node => $nodename }], + destroypool => [ 'PVE::API2::Ceph', 'destroypool', ['name'], { node => $nodename } ], + createosd => [ 'PVE::API2::Ceph', 'createosd', ['dev'], { node => $nodename }, $upid_exit], + destroyosd => [ 'PVE::API2::Ceph', 'destroyosd', ['osdid'], { node => $nodename }, $upid_exit], + createmon => [ 'PVE::API2::Ceph', 'createmon', [], { node => $nodename }, $upid_exit], + destroymon => [ 'PVE::API2::Ceph', 'destroymon', ['monid'], { node => $nodename }, $upid_exit], + start => [ 'PVE::API2::Ceph', 'start', ['service'], { node => $nodename }, $upid_exit], + stop => [ 'PVE::API2::Ceph', 'stop', ['service'], { node => $nodename }, $upid_exit], + install => [ __PACKAGE__, 'install', [] ], + purge => [ __PACKAGE__, 'purge', [] ], + status => [ 'PVE::API2::Ceph', 'status', [], { node => $nodename }, sub { + my $res = shift; + my $json = JSON->new->allow_nonref; + print $json->pretty->encode($res) . "\n"; + }], +}; + +my $cmd = shift; + +PVE::CLIHandler::handle_cmd($cmddef, "pveceph", $cmd, \@ARGV, undef, $0); + +exit 0; + +__END__ + +=head1 NAME + +pveceph - tool to manage ceph services on pve nodes + +=head1 SYNOPSIS + +=include synopsis + +=head1 DESCRIPTION + +Tool to manage ceph services on pve nodes. + +=include pve_copyright diff --git a/debian/control.in b/debian/control.in index cf281bcc..61ca7b4b 100644 --- a/debian/control.in +++ b/debian/control.in @@ -3,7 +3,7 @@ Version: @VERSION@-@PACKAGERELEASE@ Section: admin Priority: optional Architecture: amd64 -Depends: perl (>= 5.10.0-19), libtimedate-perl, libauthen-pam-perl, libintl-perl, rsync, libjson-perl, liblockfile-simple-perl, vncterm, qemu-server (>= 1.1-1), libwww-perl (>= 6.04-1), libnet-http-perl (>= 6.06-1), libhttp-daemon-perl, wget, libnet-dns-perl, vlan, ifenslave-2.6 (>= 1.1.0-10), liblinux-inotify2-perl, debconf (>= 0.5) | debconf-2.0, netcat-traditional, pve-cluster (>= 1.0-29), libpve-common-perl, libpve-storage-perl, libterm-readline-gnu-perl, libpve-access-control (>= 3.0-2), libio-socket-ssl-perl, libfilesys-df-perl, libfile-readbackwards-perl, libfile-sync-perl, redhat-cluster-pve, resource-agents-pve, fence-agents-pve, cstream, postfix | mail-transport-agent, libxml-parser-perl, lzop, dtach, libanyevent-perl, liburi-perl, logrotate, libanyevent-http-perl, apt-transport-https, libapt-pkg-perl, libcrypt-ssleay-perl, liblwp-protocol-https-perl, spiceterm +Depends: perl (>= 5.10.0-19), libtimedate-perl, libauthen-pam-perl, libintl-perl, rsync, libjson-perl, liblockfile-simple-perl, vncterm, qemu-server (>= 1.1-1), libwww-perl (>= 6.04-1), libnet-http-perl (>= 6.06-1), libhttp-daemon-perl, wget, libnet-dns-perl, vlan, ifenslave-2.6 (>= 1.1.0-10), liblinux-inotify2-perl, debconf (>= 0.5) | debconf-2.0, netcat-traditional, pve-cluster (>= 1.0-29), libpve-common-perl, libpve-storage-perl, libterm-readline-gnu-perl, libpve-access-control (>= 3.0-2), libio-socket-ssl-perl, libfilesys-df-perl, libfile-readbackwards-perl, libfile-sync-perl, redhat-cluster-pve, resource-agents-pve, fence-agents-pve, cstream, postfix | mail-transport-agent, libxml-parser-perl, lzop, dtach, libanyevent-perl, liburi-perl, logrotate, libanyevent-http-perl, apt-transport-https, libapt-pkg-perl, libcrypt-ssleay-perl, liblwp-protocol-https-perl, spiceterm, libuuid-perl, hdparm Conflicts: netcat-openbsd, vzdump Replaces: vzdump Provides: vzdump diff --git a/www/manager/Makefile b/www/manager/Makefile index 61f6e410..23b0d998 100644 --- a/www/manager/Makefile +++ b/www/manager/Makefile @@ -86,6 +86,7 @@ JSSRC= \ node/Tasks.js \ node/Subscription.js \ node/APT.js \ + node/Ceph.js \ node/Config.js \ qemu/StatusView.js \ window/Migrate.js \ diff --git a/www/manager/Utils.js b/www/manager/Utils.js index 5cca8205..6317b79e 100644 --- a/www/manager/Utils.js +++ b/www/manager/Utils.js @@ -553,6 +553,10 @@ Ext.define('PVE.Utils', { statics: { srvstop: ['SRV', gettext('Stop') ], srvrestart: ['SRV', gettext('Restart') ], srvreload: ['SRV', gettext('Reload') ], + cephcreatemon: ['Ceph Monitor', gettext('Create') ], + cephdestroymon: ['Ceph Monitor', gettext('Destroy') ], + cephcreateosd: ['Ceph OSD', gettext('Create') ], + cephdestroyosd: ['Ceph OSD', gettext('Destroy') ], imgcopy: ['', gettext('Copy data') ], imgdel: ['', gettext('Erase data') ], download: ['', gettext('Download') ], diff --git a/www/manager/node/Ceph.js b/www/manager/node/Ceph.js new file mode 100644 index 00000000..108dab89 --- /dev/null +++ b/www/manager/node/Ceph.js @@ -0,0 +1,991 @@ +Ext.define('PVE.CephCreatePool', { + extend: 'PVE.window.Edit', + alias: ['widget.pveCephCreatePool'], + + create: true, + + subject: 'Ceph Pool', + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/ceph/pools", + method: 'POST', + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Name'), + name: 'name', + allowBlank: false + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Size'), + name: 'size', + value: 2, + minValue: 1, + maxValue: 3, + allowBlank: false + }, + { + xtype: 'numberfield', + fieldLabel: 'pg_num', + name: 'pg_num', + value: 512, + minValue: 8, + maxValue: 32768, + allowBlank: false + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.CephPoolList', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveNodeCephPoolList', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var rstore = Ext.create('PVE.data.UpdateStore', { + interval: 3000, + storeid: 'ceph-pool-list', + model: 'ceph-pool-list', + proxy: { + type: 'pve', + url: "/api2/json/nodes/" + nodename + "/ceph/pools" + } + }); + + var store = Ext.create('PVE.data.DiffStore', { rstore: rstore }); + + PVE.Utils.monStoreErrors(me, rstore); + + var create_btn = new Ext.Button({ + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.CephCreatePool', { + nodename: nodename + }); + win.show(); + } + }); + + var remove_btn = new PVE.button.Button({ + text: gettext('Remove'), + selModel: sm, + disabled: true, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.pool_name + "'"); + msg += " " + gettext('This will permanently erase all image data.'); + + return msg; + }, + handler: function() { + var rec = sm.getSelection()[0]; + + if (!rec.data.pool_name) { + return; + } + + PVE.Utils.API2Request({ + url: "/nodes/" + nodename + "/ceph/pools/" + + rec.data.pool_name, + method: 'DELETE', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + Ext.apply(me, { + store: store, + selModel: sm, + stateful: false, + tbar: [ create_btn, remove_btn ], + columns: [ + { + header: gettext('Name'), + width: 100, + sortable: true, + dataIndex: 'pool_name' + }, + { + header: gettext('Size') + '/min', + width: 50, + sortable: false, + renderer: function(v, meta, rec) { + return v + '/' + rec.data.min_size; + }, + dataIndex: 'size' + }, + { + header: 'pg_num', + width: 100, + sortable: false, + dataIndex: 'pg_num' + }, + { + header: 'ruleset', + width: 50, + sortable: false, + dataIndex: 'crush_ruleset' + } + ], + listeners: { + show: rstore.startUpdate, + hide: rstore.stopUpdate, + destroy: rstore.stopUpdate + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('ceph-pool-list', { + extend: 'Ext.data.Model', + fields: [ 'pool_name', + { name: 'pool', type: 'integer'}, + { name: 'size', type: 'integer'}, + { name: 'min_size', type: 'integer'}, + { name: 'pg_num', type: 'integer'}, + { name: 'crush_ruleset', type: 'integer'} + ], + idProperty: 'pool_name' + }); +}); + + +Ext.define('PVE.node.CephOsdTree', { + extend: 'Ext.tree.Panel', + alias: 'widget.pveNodeCephOsdTree', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var sm = Ext.create('Ext.selection.TreeModel', {}); + + var service_cmd = function(cmd) { + var rec = sm.getSelection()[0]; + if (!(rec && rec.data.name && rec.data.host)) { + return; + } + PVE.Utils.API2Request({ + url: "/nodes/" + rec.data.host + "/ceph/" + cmd, + params: { service: rec.data.name }, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + + var start_btn = new Ext.Button({ + text: gettext('Start'), + disabled: true, + handler: function(){ service_cmd('start'); } + }); + + var stop_btn = new Ext.Button({ + text: gettext('Stop'), + disabled: true, + handler: function(){ service_cmd('stop'); } + }); + + var remove_btn = new Ext.Button({ + text: gettext('Remove'), + disabled: true, + handler: function(){ + var rec = sm.getSelection()[0]; + if (!(rec && (rec.data.id >= 0) && rec.data.host)) { + return; + } + PVE.Utils.API2Request({ + url: "/nodes/" + rec.data.host + "/ceph/osd/" + rec.data.id, + waitMsgTarget: me, + method: 'DELETE', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + var set_button_status = function() { + var rec = sm.getSelection()[0]; + + if (!rec) { + start_btn.setDisabled(true); + stop_btn.setDisabled(true); + remove_btn.setDisabled(true); + return; + } + + var isOsd = (rec.data.host && (rec.data.type === 'osd') && (rec.data.id >= 0)); + + start_btn.setDisabled(!(isOsd && (rec.data.status !== 'up'))); + stop_btn.setDisabled(!(isOsd && (rec.data.status !== 'down'))); + remove_btn.setDisabled(!(isOsd && (rec.data.status === 'down'))); + }; + + sm.on('selectionchange', set_button_status); + + var reload = function() { + PVE.Utils.API2Request({ + url: "/nodes/" + nodename + "/ceph/osd", + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + PVE.Utils.setErrorMask(me, response.htmlStatus); + }, + success: function(response, opts) { + sm.deselectAll(); + me.setRootNode(response.result.data.root); + me.expandAll(); + set_button_status(); + } + }); + }; + + var reload_btn = new Ext.Button({ + text: gettext('Reload'), + handler: reload + }); + + Ext.apply(me, { + tbar: [ reload_btn, start_btn, stop_btn, remove_btn ], + rootVisible: false, + fields: ['name', 'type', 'status', 'host', + { type: 'integre', name: 'id' }, + { type: 'number', name: 'reweight' }, + { type: 'number', name: 'crush_weight' }], + stateful: false, + selModel: sm, + columns: [ + { + xtype: 'treecolumn', + text: 'Name', + dataIndex: 'name', + width: 200 + }, + { + text: 'ID', + dataIndex: 'id', + align: 'right', + width: 60 + }, + { + text: 'weight', + dataIndex: 'crush_weight', + align: 'right', + width: 60 + }, + { + text: 'Type', + dataIndex: 'type', + align: 'right', + width: 100 + }, + { + text: 'Status', + dataIndex: 'status', + align: 'right', + width: 100 + }, + { + text: 'reweight', + dataIndex: 'reweight', + align: 'right', + width: 60 + } + ], + listeners: { + show: function() { + reload(); + } + } + }); + + me.callParent(); + + reload(); + } +}); + + +Ext.define('PVE.node.CephDiskList', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveNodeCephDiskList', + + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var rstore = Ext.create('PVE.data.UpdateStore', { + interval: 3000, + storeid: 'ceph-disk-list', + model: 'ceph-disk-list', + proxy: { + type: 'pve', + url: "/api2/json/nodes/" + nodename + "/ceph/disks" + } + }); + + var store = Ext.create('PVE.data.DiffStore', { rstore: rstore }); + + PVE.Utils.monStoreErrors(me, rstore); + + var create_btn = new PVE.button.Button({ + text: gettext('Create') + ': OSD', + selModel: sm, + disabled: true, + handler: function() { + var rec = sm.getSelection()[0]; + + PVE.Utils.API2Request({ + url: "/nodes/" + nodename + "/ceph/osd", + method: 'POST', + params: { dev: "/dev/" + rec.data.dev }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + Ext.apply(me, { + store: store, + selModel: sm, + stateful: false, + tbar: [ create_btn ], + columns: [ + { + header: gettext('Device'), + width: 100, + sortable: true, + dataIndex: 'dev' + }, + { + header: gettext('used'), + width: 50, + sortable: false, + renderer: function(v, metaData, rec) { + if (rec && (rec.data.osdid >= 0)) { + return "osd." + rec.data.osdid; + } + return PVE.Utils.format_boolean(v); + }, + dataIndex: 'used' + }, + { + header: gettext('Size'), + width: 100, + sortable: false, + renderer: PVE.Utils.format_size, + dataIndex: 'size' + }, + { + header: gettext('Vendor'), + width: 100, + sortable: true, + dataIndex: 'vendor' + }, + { + header: gettext('Model'), + width: 200, + sortable: true, + dataIndex: 'model' + }, + { + header: gettext('Serial'), + flex: 1, + sortable: true, + dataIndex: 'serial' + } + ], + listeners: { + show: rstore.startUpdate, + hide: rstore.stopUpdate, + destroy: rstore.stopUpdate + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('ceph-disk-list', { + extend: 'Ext.data.Model', + fields: [ 'dev', 'used', { name: 'size', type: 'number'}, + {name: 'osdid', type: 'number'}, + 'vendor', 'model', 'serial'], + idProperty: 'dev' + }); +}); + +Ext.define('PVE.CephCreateMon', { + extend: 'PVE.window.Edit', + alias: ['widget.pveCephCreateMon'], + + create: true, + + subject: 'Ceph Monitor', + + setNode: function(nodename) { + var me = this; + + me.nodename = nodename; + me.url = "/nodes/" + nodename + "/ceph/mon"; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.setNode(me.nodename); + + Ext.applyIf(me, { + method: 'POST', + items: [ + { + xtype: 'PVE.form.NodeSelector', + submitValue: false, + fieldLabel: gettext('Host'), + selectCurNode: true, + allowBlank: false, + listeners: { + change: function(f, value) { + me.setNode(value); + } + } + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.CephMonList', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveNodeCephMonList', + + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var rstore = Ext.create('PVE.data.UpdateStore', { + interval: 3000, + storeid: 'ceph-mon-list', + model: 'ceph-mon-list', + proxy: { + type: 'pve', + url: "/api2/json/nodes/" + nodename + "/ceph/mon" + } + }); + + var store = Ext.create('PVE.data.DiffStore', { rstore: rstore }); + + PVE.Utils.monStoreErrors(me, rstore); + + var service_cmd = function(cmd) { + var rec = sm.getSelection()[0]; + if (!rec.data.host) { + Ext.Msg.alert(gettext('Error'), "entry has no host"); + return; + } + PVE.Utils.API2Request({ + url: "/nodes/" + rec.data.host + "/ceph/" + cmd, + method: 'POST', + params: { service: "mon." + rec.data.name }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + var start_btn = new PVE.button.Button({ + text: gettext('Start'), + selModel: sm, + disabled: true, + handler: function(){ + service_cmd("start"); + } + }); + + var stop_btn = new PVE.button.Button({ + text: gettext('Stop'), + selModel: sm, + disabled: true, + handler: function(){ + service_cmd("stop"); + } + }); + + var create_btn = new Ext.Button({ + text: gettext('Create'), + handler: function(){ + var win = Ext.create('PVE.CephCreateMon', { + nodename: nodename + }); + win.show(); + } + }); + + var remove_btn = new PVE.button.Button({ + text: gettext('Remove'), + selModel: sm, + disabled: true, + handler: function() { + var rec = sm.getSelection()[0]; + + if (!rec.data.host) { + Ext.Msg.alert(gettext('Error'), "entry has no host"); + return; + } + + PVE.Utils.API2Request({ + url: "/nodes/" + rec.data.host + "/ceph/mon/" + + rec.data.name, + method: 'DELETE', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + Ext.apply(me, { + store: store, + selModel: sm, + stateful: false, + tbar: [ start_btn, stop_btn, create_btn, remove_btn ], + columns: [ + { + header: gettext('Name'), + width: 50, + sortable: true, + renderer: function(v) { return "mon." + v; }, + dataIndex: 'name' + }, + { + header: gettext('Host'), + width: 100, + sortable: true, + renderer: function(v) { + return v ? v : 'unknown'; + }, + dataIndex: 'host' + }, + { + header: gettext('Quorum'), + width: 50, + sortable: false, + renderer: PVE.Utils.format_boolean, + dataIndex: 'quorum' + }, + { + header: gettext('Address'), + flex: 1, + sortable: true, + dataIndex: 'addr' + } + ], + listeners: { + show: rstore.startUpdate, + hide: rstore.stopUpdate, + destroy: rstore.stopUpdate + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('ceph-mon-list', { + extend: 'Ext.data.Model', + fields: [ 'addr', 'name', 'rank', 'host', 'quorum' ], + idProperty: 'name' + }); +}); + +Ext.define('PVE.node.CephConfig', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephConfig', + + load: function() { + var me = this; + + PVE.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + }, + success: function(response, opts) { + var data = response.result.data; + me.update(Ext.htmlEncode(data)); + } + }); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + url: '/nodes/' + nodename + '/ceph/config', + bodyStyle: 'white-space:pre', + bodyPadding: 5, + autoScroll: true, + listeners: { + show: function() { + me.load(); + } + } + }); + + me.callParent(); + + me.load(); + } +}); + +Ext.define('PVE.node.CephCrushMap', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephCrushMap', + + load: function() { + var me = this; + + PVE.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + }, + success: function(response, opts) { + var data = response.result.data; + me.update(Ext.htmlEncode(data)); + } + }); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + url: '/nodes/' + nodename + '/ceph/crush', + bodyStyle: 'white-space:pre', + bodyPadding: 5, + autoScroll: true, + listeners: { + show: function() { + me.load(); + } + } + }); + + me.callParent(); + + me.load(); + } +}); + +Ext.define('PVE.node.CephStatus', { + extend: 'PVE.grid.ObjectGrid', + alias: 'widget.pveNodeCephStatus', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var renderquorum = function(value) { + if (!value || value.length < 0) { + return 'No'; + } + + return 'Yes {' + value.join(' ') + '}'; + }; + + var rendermonmap = function(d) { + if (!d) { + return ''; + } + + var txt = 'e' + d.epoch + ': ' + d.mons.length + " mons at "; + + Ext.Array.each(d.mons, function(d) { + txt += d.name + '=' + d.addr + ','; + }); + + return txt; + }; + + var renderosdmap = function(value) { + if (!value || !value.osdmap) { + return ''; + } + + var d = value.osdmap; + + var txt = 'e' + d.epoch + ': '; + + txt += d.num_osds + ' osds: ' + d.num_up_osds + ' up, ' + + d.num_in_osds + " in"; + + return txt; + }; + + var renderhealth = function(value) { + if (!value || !value.overall_status) { + return ''; + } + + var txt = value.overall_status; + + Ext.Array.each(value.summary, function(d) { + txt += " " + d.summary + ';'; + }); + + return txt; + }; + + var renderpgmap = function(d) { + if (!d) { + return ''; + } + + var txt = 'v' + d.version + ': '; + + txt += d.num_pgs + " pgs:"; + + Ext.Array.each(d.pgs_by_state, function(s) { + txt += " " + s.count + " " + s.state_name; + }); + txt += '; '; + + txt += PVE.Utils.format_size(d.data_bytes) + " data, "; + txt += PVE.Utils.format_size(d.bytes_used) + " used, "; + txt += PVE.Utils.format_size(d.bytes_avail) + " avail"; + + return txt; + }; + + Ext.applyIf(me, { + url: "/api2/json/nodes/" + nodename + "/ceph/status", + cwidth1: 150, + interval: 3000, + rows: { + health: { + header: 'health', + renderer: renderhealth, + required: true + }, + quorum_names: { + header: 'quorum', + renderer: renderquorum, + required: true + }, + fsid: { + header: 'cluster', + required: true + }, + monmap: { + header: 'monmap', + renderer: rendermonmap, + required: true + }, + osdmap: { + header: 'osdmap', + renderer: renderosdmap, + required: true + }, + pgmap: { + header: 'pgmap', + renderer: renderpgmap, + required: true + } + } + }); + + me.callParent(); + + me.on('show', me.rstore.startUpdate); + me.on('hide', me.rstore.stopUpdate); + me.on('destroy', me.rstore.stopUpdate); + } +}); + +Ext.define('PVE.node.Ceph', { + extend: 'Ext.tab.Panel', + alias: 'widget.pveNodeCeph', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + if (!me.phstateid) { + throw "no parent history state specified"; + } + + var sp = Ext.state.Manager.getProvider(); + var state = sp.get(me.phstateid); + var hsregex = /^ceph-(\S+)$/; + + if (state && state.value) { + var res = hsregex.exec(state.value); + if (res && res[1]) { + me.activeTab = res[1]; + } + } + + Ext.apply(me, { + plain: true, + tabPosition: 'bottom', + defaults: { + border: false, + pveSelNode: me.pveSelNode + }, + items: [ + { + xtype: 'pveNodeCephStatus', + title: 'Status', + itemId: 'status' + }, + { + xtype: 'pveNodeCephConfig', + title: 'Config', + itemId: 'config' + }, + { + xtype: 'pveNodeCephMonList', + title: 'Monitor', + itemId: 'monlist' + }, + { + xtype: 'pveNodeCephDiskList', + title: 'Disks', + itemId: 'disklist' + }, + { + xtype: 'pveNodeCephOsdTree', + title: 'OSD', + itemId: 'osdtree' + }, + { + xtype: 'pveNodeCephPoolList', + title: 'Pools', + itemId: 'pools' + }, + { + title: 'Crush', + xtype: 'pveNodeCephCrushMap', + itemId: 'crushmap' + }, + { + title: 'Log', + itemId: 'log', + xtype: 'pveLogView', + url: "/api2/extjs/nodes/" + nodename + "/ceph/log" + } + ], + listeners: { + afterrender: function(tp) { + var first = tp.items.get(0); + if (first) { + first.fireEvent('show', first); + } + }, + tabchange: function(tp, newcard, oldcard) { + var first = tp.items.get(0); + var ntab; + + // Note: '' is alias for first tab. + if (newcard.itemId === first.itemId) { + ntab = 'ceph'; + } else { + ntab = 'ceph-' + newcard.itemId; + } + + var state = { value: ntab }; + sp.set(me.phstateid, state); + } + } + }); + + me.callParent(); + + var statechange = function(sp, key, state) { + if ((key === me.phstateid) && state) { + var first = me.items.get(0); + var atab = me.getActiveTab().itemId; + var res = hsregex.exec(state.value); + var ntab = (res && res[1]) ? res[1] : first.itemId; + if (ntab && (atab != ntab)) { + me.setActiveTab(ntab); + } + } + }; + + me.mon(sp, 'statechange', statechange); + } +}); \ No newline at end of file diff --git a/www/manager/node/Config.js b/www/manager/node/Config.js index e8501e32..cc7f3188 100644 --- a/www/manager/node/Config.js +++ b/www/manager/node/Config.js @@ -147,6 +147,13 @@ Ext.define('PVE.node.Config', { xtype: 'pveNodeAPT', nodename: nodename }]); + me.items.push([{ + title: 'Ceph', + itemId: 'ceph', + xtype: 'pveNodeCeph', + phstateid: me.hstateid, + nodename: nodename + }]); } me.callParent(); diff --git a/www/manager/panel/ConfigPanel.js b/www/manager/panel/ConfigPanel.js index d7c3f3b0..36ecad3f 100644 --- a/www/manager/panel/ConfigPanel.js +++ b/www/manager/panel/ConfigPanel.js @@ -11,10 +11,15 @@ Ext.define('PVE.panel.Config', { var activeTab; + var hsregex = /^([^\-\s]+)(-\S+)?$/; + if (stateid) { var state = sp.get(stateid); if (state && state.value) { - activeTab = state.value; + var res = hsregex.exec(state.value); + if (res && res[1]) { + activeTab = res[1]; + } } } @@ -70,13 +75,14 @@ Ext.define('PVE.panel.Config', { }, tabchange: function(tp, newcard, oldcard) { var ntab = newcard.itemId; + // Note: '' is alias for first tab. // First tab can be 'search' or something else if (newcard.itemId === items[0].itemId) { ntab = ''; } var state = { value: ntab }; - if (stateid) { + if (stateid && !newcard.phstateid) { sp.set(stateid, state); } } @@ -91,10 +97,11 @@ Ext.define('PVE.panel.Config', { me.callParent(); var statechange = function(sp, key, state) { - if (stateid && key === stateid) { + if (stateid && (key === stateid) && state) { var atab = tab.getActiveTab().itemId; - var ntab = state.value || items[0].itemId; - if (state && ntab && (atab != ntab)) { + var res = hsregex.exec(state.value); + var ntab = (res && res[1]) ? res[1] : items[0].itemId; + if (ntab && (atab != ntab)) { tab.setActiveTab(ntab); } }