Commit e2ddeff9 authored by Dietmar Maurer's avatar Dietmar Maurer

Merge remote-tracking branch 'origin/pve-ceph'

parents 3acbeafa ad3fc0d6
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;
}});
include ../../defines.mk
PERLSOURCE = \
Ceph.pm \
APT.pm \
Subscription.pm \
VZDump.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' },
......
......@@ -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 >$@
......
#!/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
......@@ -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
......
......@@ -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 \
......
......@@ -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') ],
......
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
......@@ -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();
......
......@@ -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);
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment