diff --git a/PVE/API2/Ceph.pm b/PVE/API2/Ceph.pm index 684c97c3..b26937bc 100644 --- a/PVE/API2/Ceph.pm +++ b/PVE/API2/Ceph.pm @@ -5,9 +5,11 @@ use warnings; use File::Basename; use File::Path; use POSIX qw (LONG_MAX); +use Cwd qw(abs_path); +use IO::Dir; use PVE::SafeSyslog; -use PVE::Tools qw(extract_param run_command); +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); @@ -246,6 +248,112 @@ my $ceph_service_cmd = sub { run_command(['service', 'ceph', '-c', $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 => '', @@ -279,11 +387,51 @@ __PACKAGE__->register_method ({ { 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', @@ -744,6 +892,18 @@ __PACKAGE__->register_method ({ -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}; diff --git a/www/manager/node/Ceph.js b/www/manager/node/Ceph.js index 0180c75d..c4fe5846 100644 --- a/www/manager/node/Ceph.js +++ b/www/manager/node/Ceph.js @@ -1,3 +1,122 @@ +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]; + + console.log("CREATEOSD " + rec.data.dev); + + 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'], @@ -107,7 +226,7 @@ Ext.define('PVE.node.CephMonList', { } }); - var add_btn = new Ext.Button({ + var create_btn = new Ext.Button({ text: gettext('Create'), handler: function(){ var win = Ext.create('PVE.CephCreateMon', { @@ -144,7 +263,7 @@ Ext.define('PVE.node.CephMonList', { store: store, selModel: sm, stateful: false, - tbar: [ start_btn, stop_btn, add_btn, remove_btn ], + tbar: [ start_btn, stop_btn, create_btn, remove_btn ], columns: [ { header: gettext('Name'), @@ -449,6 +568,11 @@ Ext.define('PVE.node.Ceph', { title: 'Monitor', itemId: 'monlist' }, + { + xtype: 'pveNodeCephDiskList', + title: 'Disks', + itemId: 'disklist' + }, { title: 'OSD', itemId: 'test3',