pve-manager/www/manager6/node/ZFS.js
Thomas Lamprecht 87f43acb8e ui: improve ZFS pool name validator accuracy
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2022-11-19 15:56:46 +01:00

404 lines
8.1 KiB
JavaScript

Ext.define('PVE.node.CreateZFS', {
extend: 'Proxmox.window.Edit',
xtype: 'pveCreateZFS',
onlineHelp: 'chapter_zfs',
subject: 'ZFS',
showProgress: true,
isCreate: true,
width: 800,
viewModel: {
data: {
raidLevel: 'single',
},
formulas: {
isDraid: get => get('raidLevel')?.startsWith("draid"),
},
},
initComponent: function() {
let me = this;
if (!me.nodename) {
throw "no node name specified";
}
Ext.apply(me, {
url: `/nodes/${me.nodename}/disks/zfs`,
method: 'POST',
items: [
{
xtype: 'inputpanel',
onGetValues: function(values) {
if (values.draidData || values.draidSpares) {
let opt = { data: values.draidData, spares: values.draidSpares };
values['draid-config'] = PVE.Parser.printPropertyString(opt);
}
delete values.draidData;
delete values.draidSpares;
return values;
},
column1: [
{
xtype: 'proxmoxtextfield',
name: 'name',
fieldLabel: gettext('Name'),
allowBlank: false,
maxLength: 128, // ZFS_MAX_DATASET_NAME_LEN is (256 - some edge case)
validator: v => {
// see zpool_name_valid function in libzfs_zpool.c
if (v.match(/^(mirror|raidz|draid|spare)/) || v === 'log') {
return gettext('Cannot use reserved pool name');
} else if (!v.match(/^[a-zA-Z][a-zA-Z0-9\-_.]*$/)) {
// note: zfs would support also : and whitespace, but we don't
return gettext("Invalid characters in pool name");
}
return true;
},
},
{
xtype: 'proxmoxcheckbox',
name: 'add_storage',
fieldLabel: gettext('Add Storage'),
value: '1',
},
],
column2: [
{
xtype: 'proxmoxKVComboBox',
fieldLabel: gettext('RAID Level'),
name: 'raidlevel',
value: 'single',
comboItems: [
['single', gettext('Single Disk')],
['mirror', 'Mirror'],
['raid10', 'RAID10'],
['raidz', 'RAIDZ'],
['raidz2', 'RAIDZ2'],
['raidz3', 'RAIDZ3'],
['draid', 'dRAID'],
['draid2', 'dRAID2'],
['draid3', 'dRAID3'],
],
bind: {
value: '{raidLevel}',
},
},
{
xtype: 'proxmoxKVComboBox',
fieldLabel: gettext('Compression'),
name: 'compression',
value: 'on',
comboItems: [
['on', 'on'],
['off', 'off'],
['gzip', 'gzip'],
['lz4', 'lz4'],
['lzjb', 'lzjb'],
['zle', 'zle'],
['zstd', 'zstd'],
],
},
{
xtype: 'proxmoxintegerfield',
fieldLabel: gettext('ashift'),
minValue: 9,
maxValue: 16,
value: '12',
name: 'ashift',
},
],
columnB: [
{
xtype: 'fieldset',
title: gettext('dRAID Config'),
collapsible: false,
bind: {
hidden: '{!isDraid}',
},
layout: 'hbox',
padding: '5px 10px',
defaults: {
flex: 1,
layout: 'anchor',
},
items: [{
xtype: 'proxmoxintegerfield',
name: 'draidData',
fieldLabel: gettext('Data Devs'),
minValue: 1,
allowBlank: false,
disabled: true,
hidden: true,
bind: {
disabled: '{!isDraid}',
hidden: '{!isDraid}',
},
padding: '0 10 0 0',
},
{
xtype: 'proxmoxintegerfield',
name: 'draidSpares',
fieldLabel: gettext('Spares'),
minValue: 0,
allowBlank: false,
disabled: true,
hidden: true,
bind: {
disabled: '{!isDraid}',
hidden: '{!isDraid}',
},
padding: '0 0 0 10',
}],
},
{
xtype: 'pmxMultiDiskSelector',
name: 'devices',
nodename: me.nodename,
diskType: 'unused',
includePartitions: true,
height: 200,
emptyText: gettext('No Disks unused'),
itemId: 'disklist',
},
],
},
{
xtype: 'displayfield',
padding: '5 0 0 0',
userCls: 'pmx-hint',
value: 'Note: ZFS is not compatible with disks backed by a hardware ' +
'RAID controller. For details see <a target="_blank" href="' +
Proxmox.Utils.get_help_link('chapter_zfs') + '">the reference documentation</a>.',
},
],
});
me.callParent();
},
});
Ext.define('PVE.node.ZFSList', {
extend: 'Ext.grid.Panel',
xtype: 'pveZFSList',
viewModel: {
data: {
pool: '',
},
},
controller: {
xclass: 'Ext.app.ViewController',
destroyPool: function() {
let me = this;
let vm = me.getViewModel();
let view = me.getView();
const pool = vm.get('pool');
if (!view.nodename) {
throw "no node name specified";
}
if (!pool) {
throw "no pool specified";
}
Ext.create('PVE.window.SafeDestroyStorage', {
url: `/nodes/${view.nodename}/disks/zfs/${pool}`,
item: { id: pool },
taskName: 'zfsremove',
taskDone: () => { view.reload(); },
}).show();
},
},
stateful: true,
stateId: 'grid-node-zfs',
columns: [
{
text: gettext('Name'),
dataIndex: 'name',
flex: 1,
},
{
header: gettext('Size'),
renderer: Proxmox.Utils.format_size,
dataIndex: 'size',
},
{
header: gettext('Free'),
renderer: Proxmox.Utils.format_size,
dataIndex: 'free',
},
{
header: gettext('Allocated'),
renderer: Proxmox.Utils.format_size,
dataIndex: 'alloc',
},
{
header: gettext('Fragmentation'),
renderer: function(value) {
return value.toString() + '%';
},
dataIndex: 'frag',
},
{
header: gettext('Health'),
renderer: PVE.Utils.render_zfs_health,
dataIndex: 'health',
},
{
header: gettext('Deduplication'),
hidden: true,
renderer: function(value) {
return value.toFixed(2).toString() + 'x';
},
dataIndex: 'dedup',
},
],
rootVisible: false,
useArrows: true,
tbar: [
{
text: gettext('Reload'),
iconCls: 'fa fa-refresh',
handler: function() {
this.up('panel').reload();
},
},
{
text: gettext('Create') + ': ZFS',
handler: function() {
let view = this.up('panel');
Ext.create('PVE.node.CreateZFS', {
nodename: view.nodename,
listeners: {
destroy: () => view.reload(),
},
autoShow: true,
});
},
},
{
text: gettext('Detail'),
itemId: 'detailbtn',
disabled: true,
handler: function() {
let view = this.up('panel');
let selection = view.getSelection();
if (selection.length) {
view.show_detail(selection[0].get('name'));
}
},
},
'->',
{
xtype: 'tbtext',
data: {
pool: undefined,
},
bind: {
data: {
pool: "{pool}",
},
},
tpl: [
'<tpl if="pool">',
'Pool {pool}:',
'<tpl else>',
Ext.String.format(gettext('No {0} selected'), 'pool'),
'</tpl>',
],
},
{
text: gettext('More'),
iconCls: 'fa fa-bars',
disabled: true,
bind: {
disabled: '{!pool}',
},
menu: [
{
text: gettext('Destroy'),
itemId: 'remove',
iconCls: 'fa fa-fw fa-trash-o',
handler: 'destroyPool',
disabled: true,
bind: {
disabled: '{!pool}',
},
},
],
},
],
show_detail: function(zpool) {
let me = this;
Ext.create('Proxmox.window.ZFSDetail', {
zpool,
nodename: me.nodename,
}).show();
},
set_button_status: function() {
var me = this;
},
reload: function() {
var me = this;
me.store.load();
me.store.sort();
},
listeners: {
activate: function() {
this.reload();
},
selectionchange: function(model, selected) {
let me = this;
let vm = me.getViewModel();
me.down('#detailbtn').setDisabled(selected.length === 0);
vm.set('pool', selected[0]?.data.name || '');
},
itemdblclick: function(grid, record) {
this.show_detail(record.get('name'));
},
},
initComponent: function() {
let me = this;
me.nodename = me.pveSelNode.data.node;
if (!me.nodename) {
throw "no node name specified";
}
Ext.apply(me, {
store: {
fields: ['name', 'size', 'free', 'alloc', 'dedup', 'frag', 'health'],
proxy: {
type: 'proxmox',
url: `/api2/json/nodes/${me.nodename}/disks/zfs`,
},
sorters: 'name',
},
});
me.callParent();
Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
me.reload();
},
});