From b9dd1d6f897e41780514e177f93d269a98ef65b3 Mon Sep 17 00:00:00 2001 From: Kevin Scott Adams Date: Sat, 6 Jan 2024 13:43:35 -0500 Subject: [PATCH] Update for Proxmox 8 and Bearer Token - Patch updates for Proxmox VE 8 - Update to select Basic or Bearer authentication. --- perl5/PVE/Storage/LunCmd/FreeNAS.pm | 17 +- perl5/PVE/Storage/ZFSPlugin-8.1.3_1.pm.patch | 157 + perl5/PVE/Storage/ZFSPlugin.pm.patch | 22 +- pve-docs/api-viewer/apidoc-8.0.5_1.js.patch | 91 + pve-docs/api-viewer/apidoc.js.patch | 26 +- pve-manager/js/pvemanagerlib.js.patch | 258 +- stable-7/pve-manager/js/pvemanagerlib.js | 6881 +- .../pve-manager/js/pvemanagerlib.js.patch | 189 - .../PVE/Storage/ZFSPlugin-8.0.5_1.pm.patch | 157 + stable-8/perl5/PVE/Storage/ZFSPlugin.pm.orig | 422 + .../perl5/PVE/Storage/ZFSPlugin.pm.patch | 22 +- .../api-viewer/apidoc-8.0.5_1.js.patch | 91 + stable-8/pve-docs/api-viewer/apidoc.js.orig | 55548 ++++++++++++++++ .../pve-docs/api-viewer/apidoc.js.patch | 26 +- .../js/pvemanagerlib-8.0.5_1.js.patch | 289 + .../pve-manager/js/pvemanagerlib.js.patch | 289 + .../js/pvemanagerlib_working_copy.js | 54196 +++++++++++++++ 17 files changed, 117413 insertions(+), 1268 deletions(-) create mode 100644 perl5/PVE/Storage/ZFSPlugin-8.1.3_1.pm.patch create mode 100644 pve-docs/api-viewer/apidoc-8.0.5_1.js.patch delete mode 100644 stable-7/pve-manager/js/pvemanagerlib.js.patch create mode 100644 stable-8/perl5/PVE/Storage/ZFSPlugin-8.0.5_1.pm.patch create mode 100644 stable-8/perl5/PVE/Storage/ZFSPlugin.pm.orig rename {stable-7 => stable-8}/perl5/PVE/Storage/ZFSPlugin.pm.patch (89%) create mode 100644 stable-8/pve-docs/api-viewer/apidoc-8.0.5_1.js.patch create mode 100644 stable-8/pve-docs/api-viewer/apidoc.js.orig rename {stable-7 => stable-8}/pve-docs/api-viewer/apidoc.js.patch (81%) create mode 100644 stable-8/pve-manager/js/pvemanagerlib-8.0.5_1.js.patch create mode 100644 stable-8/pve-manager/js/pvemanagerlib.js.patch create mode 100644 stable-8/pve-manager/js/pvemanagerlib_working_copy.js diff --git a/perl5/PVE/Storage/LunCmd/FreeNAS.pm b/perl5/PVE/Storage/LunCmd/FreeNAS.pm index 02e6a4a..03e7a46 100644 --- a/perl5/PVE/Storage/LunCmd/FreeNAS.pm +++ b/perl5/PVE/Storage/LunCmd/FreeNAS.pm @@ -128,10 +128,13 @@ sub run_lun_command { syslog("info",(caller(0))[3] . " : $method(@params)"); - if(!defined($scfg->{'freenas_user'}) || !defined($scfg->{'freenas_password'})) { - die "Undefined freenas_user and/or freenas_password."; + if (defined($scfg->{'truenas_token_auth'}) && $scfg->{'truenas_token_auth'}) { + if (!defined($scfg->{'truenas_secret'})) { + die "Undefined `truenas_secret` variable."; + } + } elsif (!defined($scfg->{'freenas_user'}) || !defined($scfg->{'freenas_password'})) { + die "Undefined `freenas_user` and/or `freenas_password` variables."; } - if (!defined $freenas_server_list->{defined($scfg->{freenas_apiv4_host}) ? $scfg->{freenas_apiv4_host} : $scfg->{portal}}) { freenas_api_check($scfg); } @@ -341,7 +344,13 @@ sub freenas_api_connect { } $freenas_server_list->{$apihost}->setHost($scheme . '://' . $apihost); $freenas_server_list->{$apihost}->addHeader('Content-Type', 'application/json'); - $freenas_server_list->{$apihost}->addHeader('Authorization', 'Basic ' . encode_base64($scfg->{freenas_user} . ':' . $scfg->{freenas_password})); + if (defined($scfg->{'truenas_token_auth'})) { + syslog("info", (caller(0))[3] . " : Authentication using Bearer Token Auth"); + $freenas_server_list->{$apihost}->addHeader('Authorization', 'Bearer ' . $scfg->{truenas_secret}); + } else { + syslog("info", (caller(0))[3] . " : Authentication using Basic Auth"); + $freenas_server_list->{$apihost}->addHeader('Authorization', 'Basic ' . encode_base64($scfg->{freenas_user} . ':' . $scfg->{freenas_password})); + } # If using SSL, don't verify SSL certs if ($scfg->{freenas_use_ssl}) { $freenas_server_list->{$apihost}->getUseragent()->ssl_opts(verify_hostname => 0); diff --git a/perl5/PVE/Storage/ZFSPlugin-8.1.3_1.pm.patch b/perl5/PVE/Storage/ZFSPlugin-8.1.3_1.pm.patch new file mode 100644 index 0000000..613c882 --- /dev/null +++ b/perl5/PVE/Storage/ZFSPlugin-8.1.3_1.pm.patch @@ -0,0 +1,157 @@ +--- ZFSPlugin.pm.orig 2023-12-31 09:56:18.895228853 -0500 ++++ ZFSPlugin.pm 2023-12-31 09:57:08.830488875 -0500 +@@ -10,6 +10,7 @@ + + use base qw(PVE::Storage::ZFSPoolPlugin); + use PVE::Storage::LunCmd::Comstar; ++use PVE::Storage::LunCmd::FreeNAS; + use PVE::Storage::LunCmd::Istgt; + use PVE::Storage::LunCmd::Iet; + use PVE::Storage::LunCmd::LIO; +@@ -26,13 +27,14 @@ + modify_lu => 1, + add_view => 1, + list_view => 1, ++ list_extent => 1, + list_lu => 1, + }; + + my $zfs_unknown_scsi_provider = sub { + my ($provider) = @_; + +- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]"; ++ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]"; + }; + + my $zfs_get_base = sub { +@@ -40,6 +42,8 @@ + + if ($scfg->{iscsiprovider} eq 'comstar') { + return PVE::Storage::LunCmd::Comstar::get_base; ++ } elsif ($scfg->{iscsiprovider} eq 'freenas') { ++ return PVE::Storage::LunCmd::FreeNAS::get_base; + } elsif ($scfg->{iscsiprovider} eq 'istgt') { + return PVE::Storage::LunCmd::Istgt::get_base; + } elsif ($scfg->{iscsiprovider} eq 'iet') { +@@ -62,6 +66,8 @@ + if ($lun_cmds->{$method}) { + if ($scfg->{iscsiprovider} eq 'comstar') { + $msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params); ++ } elsif ($scfg->{iscsiprovider} eq 'freenas') { ++ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params); + } elsif ($scfg->{iscsiprovider} eq 'istgt') { + $msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params); + } elsif ($scfg->{iscsiprovider} eq 'iet') { +@@ -166,6 +172,15 @@ + die "lun_number for guid $guid is not a number"; + } + ++# Part of the multipath enhancement ++sub zfs_get_wwid_number { ++ my ($class, $scfg, $guid) = @_; ++ ++ die "could not find lun_number for guid $guid" if !$guid; ++ ++ return $class->zfs_request($scfg, undef, 'list_extent', $guid); ++} ++ + # Configuration + + sub type { +@@ -184,6 +199,32 @@ + description => "iscsi provider", + type => 'string', + }, ++ # This is for FreeNAS iscsi and API intergration ++ # And some enhancements asked by the community ++ freenas_user => { ++ description => "FreeNAS API Username", ++ type => 'string', ++ }, ++ freenas_password => { ++ description => "FreeNAS API Password (Deprecated)", ++ type => 'string', ++ }, ++ truenas_secret => { ++ description => "TrueNAS API Secret", ++ type => 'string', ++ }, ++ truenas_token_auth => { ++ description => "TrueNAS API Authentication with Token", ++ type => 'boolean', ++ }, ++ freenas_use_ssl => { ++ description => "FreeNAS API access via SSL", ++ type => 'boolean', ++ }, ++ freenas_apiv4_host => { ++ description => "FreeNAS API Host", ++ type => 'string', ++ }, + # this will disable write caching on comstar and istgt. + # it is not implemented for iet. iet blockio always operates with + # writethrough caching when not in readonly mode +@@ -211,14 +252,20 @@ + nodes => { optional => 1 }, + disable => { optional => 1 }, + portal => { fixed => 1 }, +- target => { fixed => 1 }, +- pool => { fixed => 1 }, ++ target => { fixed => 0 }, ++ pool => { fixed => 0 }, + blocksize => { fixed => 1 }, + iscsiprovider => { fixed => 1 }, + nowritecache => { optional => 1 }, + sparse => { optional => 1 }, + comstar_hg => { optional => 1 }, + comstar_tg => { optional => 1 }, ++ freenas_user => { optional => 1 }, ++ freenas_password => { optional => 1 }, ++ truenas_secret => { optional => 1 }, ++ truenas_token_auth => { optional => 1 }, ++ freenas_use_ssl => { optional => 1 }, ++ freenas_apiv4_host => { optional => 1 }, + lio_tpg => { optional => 1 }, + content => { optional => 1 }, + bwlimit => { optional => 1 }, +@@ -243,6 +290,40 @@ + + my $path = "iscsi://$portal/$target/$lun"; + ++ # Multipath enhancement ++ eval { ++ my $wwid = $class->zfs_get_wwid_number($scfg, $guid); ++# syslog(info,"JD: path get_lun_number guid $guid"); ++ ++ if ($wwid =~ /^([-\@\w.]+)$/) { ++ $wwid = $1; # $data now untainted ++ } else { ++ die "Bad data in '$wwid'"; # log this somewhere ++ } ++ my $wwid_end = substr $wwid, 16; ++ ++ my $mapper = ''; ++ sleep 3; ++ run_command("iscsiadm -m session --rescan"); ++ sleep 3; ++ my $line = `/usr/sbin/multipath -ll | grep \"$wwid_end\"`; ++ my ($mapper_device) = split(' ', $line); ++ $mapper_device = "" unless $mapper_device; ++ $mapper .= $mapper_device; ++ ++ if ($mapper =~ /^([-\@\w.]+)$/) { ++ $mapper = $1; # $data now untainted ++ } else { ++ $mapper = ''; ++ } ++ ++# syslog(info,"Multipath mapper found: $mapper\n"); ++ if ($mapper ne "") { ++ $path = "/dev/mapper/$mapper"; ++ sleep 5; ++ } ++ }; ++ + return ($path, $vmid, $vtype); + } + diff --git a/perl5/PVE/Storage/ZFSPlugin.pm.patch b/perl5/PVE/Storage/ZFSPlugin.pm.patch index d791749..613c882 100644 --- a/perl5/PVE/Storage/ZFSPlugin.pm.patch +++ b/perl5/PVE/Storage/ZFSPlugin.pm.patch @@ -1,5 +1,5 @@ ---- ZFSPlugin.pm.orig 2022-02-04 12:08:01.000000000 -0500 -+++ ZFSPlugin.pm 2022-03-26 13:51:40.660068908 -0400 +--- ZFSPlugin.pm.orig 2023-12-31 09:56:18.895228853 -0500 ++++ ZFSPlugin.pm 2023-12-31 09:57:08.830488875 -0500 @@ -10,6 +10,7 @@ use base qw(PVE::Storage::ZFSPoolPlugin); @@ -58,7 +58,7 @@ # Configuration sub type { -@@ -184,6 +199,24 @@ +@@ -184,6 +199,32 @@ description => "iscsi provider", type => 'string', }, @@ -69,9 +69,17 @@ + type => 'string', + }, + freenas_password => { -+ description => "FreeNAS API Password", ++ description => "FreeNAS API Password (Deprecated)", + type => 'string', + }, ++ truenas_secret => { ++ description => "TrueNAS API Secret", ++ type => 'string', ++ }, ++ truenas_token_auth => { ++ description => "TrueNAS API Authentication with Token", ++ type => 'boolean', ++ }, + freenas_use_ssl => { + description => "FreeNAS API access via SSL", + type => 'boolean', @@ -83,7 +91,7 @@ # this will disable write caching on comstar and istgt. # it is not implemented for iet. iet blockio always operates with # writethrough caching when not in readonly mode -@@ -211,14 +244,18 @@ +@@ -211,14 +252,20 @@ nodes => { optional => 1 }, disable => { optional => 1 }, portal => { fixed => 1 }, @@ -99,12 +107,14 @@ comstar_tg => { optional => 1 }, + freenas_user => { optional => 1 }, + freenas_password => { optional => 1 }, ++ truenas_secret => { optional => 1 }, ++ truenas_token_auth => { optional => 1 }, + freenas_use_ssl => { optional => 1 }, + freenas_apiv4_host => { optional => 1 }, lio_tpg => { optional => 1 }, content => { optional => 1 }, bwlimit => { optional => 1 }, -@@ -243,6 +280,40 @@ +@@ -243,6 +290,40 @@ my $path = "iscsi://$portal/$target/$lun"; diff --git a/pve-docs/api-viewer/apidoc-8.0.5_1.js.patch b/pve-docs/api-viewer/apidoc-8.0.5_1.js.patch new file mode 100644 index 0000000..281fcc3 --- /dev/null +++ b/pve-docs/api-viewer/apidoc-8.0.5_1.js.patch @@ -0,0 +1,91 @@ +--- apidoc.js.orig 2024-01-06 13:02:06.730512378 -0500 ++++ apidoc.js 2024-01-06 13:02:55.349787105 -0500 +@@ -50336,6 +50336,37 @@ + "type" : "string", + "typetext" : "" + }, ++ "freenas_user" : { ++ "description" : "FreeNAS user for API access", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "freenas_password" : { ++ "description" : "FreeNAS password for API access (Deprecated)", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "truenas_secret" : { ++ "description" : "TrueNAS Secret for API access", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "freenas_use_ssl" : { ++ "description" : "FreeNAS API access via SSL", ++ "optional" : 1, ++ "type" : "boolean", ++ "typetext" : "" ++ }, ++ "freenas_apiv4_host" : { ++ "description" : "FreeNAS API Host via IPv4", ++ "format" : "address", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, + "fuse" : { + "description" : "Mount CephFS through FUSE.", + "optional" : 1, +@@ -50555,6 +50586,12 @@ + "type" : "boolean", + "typetext" : "" + }, ++ "target" : { ++ "description" : "iSCSI target.", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, + "transport" : { + "description" : "Gluster transport: tcp or rdma", + "enum" : [ +@@ -50854,6 +50891,37 @@ + "optional" : 1, + "type" : "string", + "typetext" : "" ++ }, ++ "freenas_user" : { ++ "description" : "FreeNAS user for API access", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "freenas_password" : { ++ "description" : "FreeNAS password for API access (Deprecated)", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "truenas_secret" : { ++ "description" : "TrueNAS secret for API access", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "freenas_use_ssl" : { ++ "description" : "FreeNAS API access via SSL", ++ "optional" : 1, ++ "type" : "boolean", ++ "typetext" : "" ++ }, ++ "freenas_apiv4_host" : { ++ "description" : "FreeNAS API Host via IPv4", ++ "format" : "address", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" + }, + "fuse" : { + "description" : "Mount CephFS through FUSE.", diff --git a/pve-docs/api-viewer/apidoc.js.patch b/pve-docs/api-viewer/apidoc.js.patch index 477f9f4..30a3fb8 100644 --- a/pve-docs/api-viewer/apidoc.js.patch +++ b/pve-docs/api-viewer/apidoc.js.patch @@ -1,6 +1,6 @@ ---- apidoc.js.orig 2021-11-15 10:07:34.000000000 -0500 -+++ apidoc.js 2021-12-06 08:04:01.648822707 -0500 -@@ -44064,6 +44064,31 @@ +--- apidoc.js.orig 2023-08-16 06:03:55.000000000 -0400 ++++ apidoc.js 2023-12-26 14:45:47.202566775 -0500 +@@ -47579,6 +47579,37 @@ "type" : "string", "typetext" : "" }, @@ -11,7 +11,13 @@ + "typetext" : "" + }, + "freenas_password" : { -+ "description" : "FreeNAS password for API access", ++ "description" : "FreeNAS password for API access (Deprecated)", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "truenas_secret" : { ++ "description" : "TrueNAS Secret for API access", + "optional" : 1, + "type" : "string", + "typetext" : "" @@ -32,7 +38,7 @@ "fuse" : { "description" : "Mount CephFS through FUSE.", "optional" : 1, -@@ -44275,6 +44300,12 @@ +@@ -47798,6 +47829,12 @@ "type" : "boolean", "typetext" : "" }, @@ -45,7 +51,7 @@ "transport" : { "description" : "Gluster transport: tcp or rdma", "enum" : [ -@@ -44547,6 +44578,31 @@ +@@ -48097,6 +48134,37 @@ "optional" : 1, "type" : "string", "typetext" : "" @@ -57,7 +63,13 @@ + "typetext" : "" + }, + "freenas_password" : { -+ "description" : "FreeNAS password for API access", ++ "description" : "FreeNAS password for API access (Deprecated)", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "truenas_secret" : { ++ "description" : "TrueNAS secret for API access", + "optional" : 1, + "type" : "string", + "typetext" : "" diff --git a/pve-manager/js/pvemanagerlib.js.patch b/pve-manager/js/pvemanagerlib.js.patch index 52019ab..9fa45ec 100644 --- a/pve-manager/js/pvemanagerlib.js.patch +++ b/pve-manager/js/pvemanagerlib.js.patch @@ -1,58 +1,162 @@ ---- pvemanagerlib.js.orig 2022-03-17 09:08:40.000000000 -0400 -+++ pvemanagerlib.js 2022-04-03 08:54:10.229689187 -0400 -@@ -8068,6 +8068,7 @@ +--- pvemanagerlib.js.orig 2023-12-30 15:36:27.913505863 -0500 ++++ pvemanagerlib.js 2024-01-02 09:30:56.000000000 -0500 +@@ -9228,6 +9228,7 @@ alias: ['widget.pveiScsiProviderSelector'], comboItems: [ ['comstar', 'Comstar'], -+ ['freenas', 'FreeNAS-API'], ++ ['freenas', 'FreeNAS/TrueNAS API'], ['istgt', 'istgt'], ['iet', 'IET'], ['LIO', 'LIO'], -@@ -49636,6 +49637,7 @@ +@@ -58017,16 +58018,24 @@ + me.callParent(); + }, + }); ++ + Ext.define('PVE.storage.ZFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + viewModel: { + parent: null, data: { ++isComstar: true, ++ isFreeNAS: false, isLIO: false, - isComstar: true, -+ isFreeNAS: false, +- isComstar: true, ++ isToken: false, hasWriteCacheOption: true, }, ++formulas: { ++ hideUsername: function(get) { ++ return (!get('isFreeNAS') || !(get('isFreeNAS') && !get('isToken'))); ++ }, ++ }, }, -@@ -49648,10 +49650,26 @@ + + controller: { +@@ -58034,13 +58043,42 @@ + control: { + 'field[name=iscsiprovider]': { + change: 'changeISCSIProvider', ++}, ++ 'field[name=truenas_token_auth]': { ++ change: 'changeUsername', }, }, changeISCSIProvider: function(f, newVal, oldVal) { -+ var me = this; ++var me = this; var vm = this.getViewModel(); vm.set('isLIO', newVal === 'LIO'); vm.set('isComstar', newVal === 'comstar'); - vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt'); + vm.set('isFreeNAS', newVal === 'freenas'); -+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt'); -+ if (newVal !== 'freenas') { -+ me.lookupReference('freenas_use_ssl_field').setValue(false); -+ me.lookupReference('freenas_apiv4_host_field').setValue(''); -+ me.lookupReference('freenas_user_field').setValue(''); -+ me.lookupReference('freenas_user_field').allowBlank = true; -+ me.lookupReference('freenas_password_field').setValue(''); -+ me.lookupReference('freenas_password_field').allowBlank = true; -+ me.lookupReference('freenas_confirmpw_field').setValue(''); -+ me.lookupReference('freenas_confirmpw_field').allowBlank = true; -+ } else { -+ me.lookupReference('freenas_user_field').allowBlank = false; -+ me.lookupReference('freenas_password_field').allowBlank = false; -+ me.lookupReference('freenas_confirmpw_field').allowBlank = false; -+ } ++ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt'); ++ if (newVal !== 'freenas') { ++ me.lookupReference('freenas_use_ssl_field').setValue(false); ++ me.lookupReference('truenas_token_auth_field').setValue(false); ++ me.lookupReference('freenas_apiv4_host_field').setValue(''); ++ me.lookupReference('freenas_user_field').setValue(''); ++ me.lookupReference('freenas_user_field').allowBlank = true; ++ me.lookupReference('truenas_secret_field').setValue(''); ++ me.lookupReference('truenas_secret_field').allowBlank = true; ++ me.lookupReference('truenas_confirm_secret_field').setValue(''); ++ me.lookupReference('truenas_confirm_secret_field').allowBlank = true; ++ } else { ++ me.lookupReference('freenas_user_field').allowBlank = false; ++ me.lookupReference('truenas_secret_field').allowBlank = false; ++ me.lookupReference('truenas_confirm_secret_field').allowBlank = false; ++ } ++ }, ++ changeUsername: function(f, newVal, oldVal) { ++ var me = this; ++ var vm = me.getViewModel(); ++ vm.set('isToken', newVal); ++ me.lookupReference('freenas_user_field').allowBlank = newVal; ++ if (newVal) { ++ me.lookupReference('freenas_user_field').setValue(''); ++ } }, }, -@@ -49669,6 +49687,7 @@ +@@ -58053,28 +58091,78 @@ + + values.nowritecache = values.writecache ? 0 : 1; + delete values.writecache; ++ console.warn(values.freenas_password); ++ if (values.freenas_password) { ++ values.truenas_secret = values.freenas_password; ++ } ++ console.warn(values.truenas_secret); + + return me.callParent([values]); }, setValues: function(values) { -+ values.freenas_confirmpw = values.freenas_password; - values.writecache = values.nowritecache ? 0 : 1; - this.callParent([values]); +- values.writecache = values.nowritecache ? 0 : 1; +- this.callParent([values]); ++ if (values.freenas_password) { ++ values.truenas_secret = values.freenas_password; ++ } ++ values.truenas_confirm_secret = values.truenas_secret; ++ values.writecache = values.nowritecache ? 0 : 1; ++ this.callParent([values]); }, -@@ -49685,7 +49704,7 @@ + + initComponent: function() { +- var me = this; ++ var me = this; ++ ++ var tnsecret = Ext.create('Ext.form.TextField', { ++ xtype: 'proxmoxtextfield', ++ name: 'truenas_secret', ++ reference: 'truenas_secret_field', ++ inputType: me.isCreate ? '' : 'password', ++ value: '', ++ editable: true, ++ emptyText: Proxmox.Utils.noneText, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ fieldLabel: gettext('API Password'), ++ change: function(f, value) { ++ if (f.rendered) { ++ f.up().down('field[name=truenas_confirm_secret]').validate(); ++ } ++ }, ++ }); + +- me.column1 = [ +- { +- xtype: me.isCreate ? 'textfield' : 'displayfield', +- name: 'portal', ++ var tnconfirmsecret = Ext.create('Ext.form.TextField', { ++ xtype: 'proxmoxtextfield', ++ name: 'truenas_confirm_secret', ++ reference: 'truenas_confirm_secret_field', ++ inputType: me.isCreate ? '' : 'password', ++ value: '', ++ editable: true, ++ submitValue: false, ++ emptyText: Proxmox.Utils.noneText, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ fieldLabel: gettext('Confirm API Password'), ++ validator: function(value) { ++ var pw = me.up().down('field[name=truenas_secret]').getValue(); ++ if (pw !== value) { ++ return "Secrets do not match!"; ++ } ++ return true; ++ }, ++ }); ++ ++ me.column1 = [ ++ { ++ xtype: me.isCreate ? 'textfield' : 'displayfield', ++ name: 'portal', + value: '', + fieldLabel: gettext('Portal'), allowBlank: false, }, { @@ -61,7 +165,7 @@ name: 'pool', value: '', fieldLabel: gettext('Pool'), -@@ -49695,11 +49714,11 @@ +@@ -58084,11 +58172,11 @@ xtype: me.isCreate ? 'textfield' : 'displayfield', name: 'blocksize', value: '4k', @@ -75,7 +179,7 @@ name: 'target', value: '', fieldLabel: gettext('Target'), -@@ -49710,9 +49729,34 @@ +@@ -58099,8 +58187,59 @@ name: 'comstar_tg', value: '', fieldLabel: gettext('Target group'), @@ -84,7 +188,7 @@ + hidden: '{!isComstar}' + }, allowBlank: true, - }, ++}, + { + xtype: 'proxmoxcheckbox', + name: 'freenas_use_ssl', @@ -98,6 +202,32 @@ + fieldLabel: gettext('API use SSL'), + }, + { ++ xtype: 'proxmoxcheckbox', ++ name: 'truenas_token_auth', ++ reference: 'truenas_token_auth_field', ++ inputId: 'truenas_use_token_auth_field', ++ checked: false, ++ listeners: { ++ change: function(field, newValue) { ++ if (newValue === true) { ++ tnsecret.labelEl.update('API Token'); ++ tnconfirmsecret.labelEl.update('Confirm API Token'); ++ me.lookupReference('freenas_user_field').setValue(''); ++ me.lookupReference('freenas_user_field').allowBlank = true; ++ } else { ++ tnsecret.labelEl.update('API Password'); ++ tnconfirmsecret.labelEl.update('Confirm API Password'); ++ me.lookupReference('freenas_user_field').allowBlank = false; ++ } ++ }, ++ }, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ uncheckedValue: 0, ++ fieldLabel: gettext('API Token Auth'), ++ }, ++ { + xtype: 'textfield', + name: 'freenas_user', + reference: 'freenas_user_field', @@ -105,13 +235,12 @@ + value: '', + fieldLabel: gettext('API Username'), + bind: { -+ hidden: '{!isFreeNAS}' ++ hidden: '{hideUsername}' + }, -+ }, + }, ]; - me.column2 = [ -@@ -49742,7 +49786,9 @@ +@@ -58131,7 +58270,9 @@ xtype: me.isCreate ? 'textfield' : 'displayfield', name: 'comstar_hg', value: '', @@ -122,18 +251,19 @@ fieldLabel: gettext('Host group'), allowBlank: true, }, -@@ -49750,9 +49796,62 @@ +@@ -58139,15 +58280,32 @@ xtype: me.isCreate ? 'textfield' : 'displayfield', name: 'lio_tpg', value: '', - bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' }, - allowBlank: false, +- fieldLabel: gettext('Target portal group'), + bind: { + hidden: '{!isLIO}' + }, - fieldLabel: gettext('Target portal group'), -+ allowBlank: true -+ }, ++ fieldLabel: gettext('Target portal group'), ++ allowBlank: true + }, + { + xtype: 'proxmoxtextfield', + name: 'freenas_apiv4_host', @@ -146,44 +276,14 @@ + }, + fieldLabel: gettext('API IPv4 Host'), + }, -+ { -+ xtype: 'proxmoxtextfield', -+ name: 'freenas_password', -+ reference: 'freenas_password_field', -+ inputType: me.isCreate ? '' : 'password', -+ value: '', -+ editable: true, -+ emptyText: Proxmox.Utils.noneText, -+ bind: { -+ hidden: '{!isFreeNAS}' -+ }, -+ fieldLabel: gettext('API Password'), -+ change: function(f, value) { -+ if (f.rendered) { -+ f.up().down('field[name=freenas_confirmpw]').validate(); -+ } -+ }, -+ }, -+ { -+ xtype: 'proxmoxtextfield', -+ name: 'freenas_confirmpw', -+ reference: 'freenas_confirmpw_field', -+ inputType: me.isCreate ? '' : 'password', -+ value: '', -+ editable: true, -+ submitValue: false, -+ emptyText: Proxmox.Utils.noneText, -+ bind: { -+ hidden: '{!isFreeNAS}' -+ }, -+ fieldLabel: gettext('Confirm Password'), -+ validator: function(value) { -+ var pw = this.up().down('field[name=freenas_password]').getValue(); -+ if (pw !== value) { -+ return "Passwords do not match!"; -+ } -+ return true; -+ }, - }, ++ tnsecret, ++ tnconfirmsecret, ]; + me.callParent(); + }, + }); ++ + Ext.define('PVE.storage.ZFSPoolSelector', { + extend: 'PVE.form.ComboBoxSetStoreNode', + alias: 'widget.pveZFSPoolSelector', diff --git a/stable-7/pve-manager/js/pvemanagerlib.js b/stable-7/pve-manager/js/pvemanagerlib.js index 8470957..dc7f090 100644 --- a/stable-7/pve-manager/js/pvemanagerlib.js +++ b/stable-7/pve-manager/js/pvemanagerlib.js @@ -11,6 +11,10 @@ const pveOnlineHelpInfo = { "link" : "/pve-docs/chapter-sysadmin.html#chapter_lvm", "title" : "Logical Volume Manager (LVM)" }, + "chapter_notifications" : { + "link" : "/pve-docs/chapter-notifications.html#chapter_notifications", + "title" : "Notifications" + }, "chapter_pct" : { "link" : "/pve-docs/chapter-pct.html#chapter_pct", "title" : "Proxmox Container Toolkit" @@ -29,7 +33,7 @@ const pveOnlineHelpInfo = { }, "chapter_pvesdn" : { "link" : "/pve-docs/chapter-pvesdn.html#chapter_pvesdn", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "chapter_pvesr" : { "link" : "/pve-docs/chapter-pvesr.html#chapter_pvesr", @@ -120,6 +124,26 @@ const pveOnlineHelpInfo = { "subtitle" : "Influxdb plugin configuration", "title" : "External Metric Server" }, + "notification_matchers" : { + "link" : "/pve-docs/chapter-notifications.html#notification_matchers", + "subtitle" : "Notification Matchers", + "title" : "Notifications" + }, + "notification_targets_gotify" : { + "link" : "/pve-docs/chapter-notifications.html#notification_targets_gotify", + "subtitle" : "Gotify", + "title" : "Notifications" + }, + "notification_targets_sendmail" : { + "link" : "/pve-docs/chapter-notifications.html#notification_targets_sendmail", + "subtitle" : "Sendmail", + "title" : "Notifications" + }, + "notification_targets_smtp" : { + "link" : "/pve-docs/chapter-notifications.html#notification_targets_smtp", + "subtitle" : "SMTP", + "title" : "Notifications" + }, "pct_configuration" : { "link" : "/pve-docs/chapter-pct.html#pct_configuration", "subtitle" : "Configuration", @@ -254,67 +278,67 @@ const pveOnlineHelpInfo = { "pvesdn_config_controllers" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_controllers", "subtitle" : "Controllers", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesdn_config_vnet" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_vnet", "subtitle" : "VNets", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesdn_config_zone" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_zone", "subtitle" : "Zones", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesdn_controller_plugin_evpn" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_controller_plugin_evpn", "subtitle" : "EVPN Controller", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesdn_dns_plugin_powerdns" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_dns_plugin_powerdns", "subtitle" : "PowerDNS Plugin", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesdn_ipam_plugin_netbox" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_netbox", "subtitle" : "NetBox IPAM Plugin", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesdn_ipam_plugin_phpipam" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_phpipam", "subtitle" : "phpIPAM Plugin", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesdn_ipam_plugin_pveipam" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_pveipam", - "subtitle" : "Proxmox VE IPAM Plugin", - "title" : "Software Defined Network" + "subtitle" : "PVE IPAM Plugin", + "title" : "Software-Defined Network" }, "pvesdn_zone_plugin_evpn" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_evpn", "subtitle" : "EVPN Zones", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesdn_zone_plugin_qinq" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_qinq", "subtitle" : "QinQ Zones", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesdn_zone_plugin_simple" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_simple", "subtitle" : "Simple Zones", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesdn_zone_plugin_vlan" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_vlan", "subtitle" : "VLAN Zones", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesdn_zone_plugin_vxlan" : { "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_vxlan", "subtitle" : "VXLAN Zones", - "title" : "Software Defined Network" + "title" : "Software-Defined Network" }, "pvesr_schedule_time_format" : { "link" : "/pve-docs/chapter-pvesr.html#pvesr_schedule_time_format", @@ -475,6 +499,11 @@ const pveOnlineHelpInfo = { "subtitle" : "Virtual Machines Settings", "title" : "QEMU/KVM Virtual Machines" }, + "resource_mapping" : { + "link" : "/pve-docs/chapter-qm.html#resource_mapping", + "subtitle" : "Resource Mapping", + "title" : "QEMU/KVM Virtual Machines" + }, "storage_btrfs" : { "link" : "/pve-docs/chapter-pvesm.html#storage_btrfs", "title" : "BTRFS Backend" @@ -1162,6 +1191,10 @@ Ext.define('PVE.Parser', { }); return [res, extradata]; }, + + filterPropertyStringList: function(list, filterFn, defaultKey) { + return list.filter((entry) => filterFn(PVE.Parser.parsePropertyString(entry, defaultKey))); + }, }, }); /* This state provider keeps part of the state inside the browser history. @@ -1440,9 +1473,17 @@ Ext.define('PVE.Utils', { }, noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit ' - +'' + +'' +'www.proxmox.com to get a list of available options.', + getClusterSubscriptionLevel: async function() { + let { result } = await Proxmox.Async.api2({ url: '/cluster/status' }); + let levelMap = Object.fromEntries( + result.data.filter(v => v.type === 'node').map(v => [v.name, v.level]), + ); + return levelMap; + }, + kvm_ostypes: { 'Linux': [ { desc: '6.x - 2.6 Kernel', val: 'l26' }, @@ -1881,6 +1922,8 @@ Ext.define('PVE.Utils', { virtio: "VirtIO", }; displayText = map[value] || Proxmox.Utils.unknownText; + } else if (key === 'freeze-fs-on-backup' && PVE.Parser.parseBoolean(value)) { + continue; } else if (PVE.Parser.parseBoolean(value)) { displayText = Proxmox.Utils.enabledText; } @@ -2291,6 +2334,11 @@ Ext.define('PVE.Utils', { ipanel: 'BgpInputPanel', faIcon: 'crosshairs', }, + isis: { + name: 'isis', + ipanel: 'IsisInputPanel', + faIcon: 'crosshairs', + }, }, sdnipamSchema: { @@ -2399,15 +2447,18 @@ Ext.define('PVE.Utils', { }, render_storage_content: function(value, metaData, record) { - var data = record.data; + let data = record.data; + let result; if (Ext.isNumber(data.channel) && Ext.isNumber(data.id) && Ext.isNumber(data.lun)) { - return "CH " + + result = "CH " + Ext.String.leftPad(data.channel, 2, '0') + " ID " + data.id + " LUN " + data.lun; + } else { + result = data.volid.replace(/^.*?:(.*?\/)?/, ''); } - return data.volid.replace(/^.*?:(.*?\/)?/, ''); + return Ext.String.htmlEncode(result); }, render_serverity: function(value) { @@ -2794,10 +2845,11 @@ Ext.define('PVE.Utils', { css: 'display:none;visibility:hidden;height:0px;', }); - // Note: we need to tell Android and Chrome the correct file name extension + // Note: we need to tell Android, AppleWebKit and Chrome + // the correct file name extension // but we do not set 'download' tag for other environments, because // It can have strange side effects (additional user prompt on firefox) - if (navigator.userAgent.match(/Android|Chrome/i)) { + if (navigator.userAgent.match(/Android|AppleWebKit|Chrome/i)) { link.download = name; } @@ -3128,7 +3180,7 @@ Ext.define('PVE.Utils', { rstore, /not (installed|initialized)/i, (_, error) => { - nodename = nodename || 'localhost'; + nodename = nodename || Proxmox.NodeName; let maskTarget = maskOwnerCt ? view.ownerCt : view; rstore.stopUpdate(); PVE.Utils.showCephInstallOrMask(maskTarget, error.statusText, nodename, win => { @@ -3308,6 +3360,10 @@ Ext.define('PVE.Utils', { 'ok': 2, '__default__': 3, }, + + isStandaloneNode: function() { + return PVE.data.ResourceStore.getNodes().length < 2; + }, }, singleton: true, @@ -3354,7 +3410,7 @@ Ext.define('PVE.Utils', { lvmremove: ['Volume Group', gettext('Remove')], lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')], lvmthinremove: ['Thinpool', gettext('Remove')], - migrateall: ['', gettext('Migrate all VMs and Containers')], + migrateall: ['', gettext('Bulk migrate VMs and Containers')], 'move_volume': ['CT', gettext('Move Volume')], 'pbs-download': ['VM/CT', gettext('File Restore Download')], pull_file: ['CT', gettext('Pull file')], @@ -3378,10 +3434,12 @@ Ext.define('PVE.Utils', { qmstop: ['VM', gettext('Stop')], qmsuspend: ['VM', gettext('Hibernate')], qmtemplate: ['VM', gettext('Convert to template')], + resize: ['VM/CT', gettext('Resize')], spiceproxy: ['VM/CT', gettext('Console') + ' (Spice)'], spiceshell: ['', gettext('Shell') + ' (Spice)'], - startall: ['', gettext('Start all VMs and Containers')], - stopall: ['', gettext('Stop all VMs and Containers')], + startall: ['', gettext('Bulk start VMs and Containers')], + stopall: ['', gettext('Bulk shutdown VMs and Containers')], + suspendall: ['', gettext('Suspend all VMs')], unknownimgdel: ['', gettext('Destroy image from unknown guest')], wipedisk: ['Device', gettext('Wipe Disk')], vncproxy: ['VM/CT', gettext('Console')], @@ -3894,6 +3952,10 @@ Ext.define('PVE.data.PermPathStore', { { 'value': '/access' }, { 'value': '/access/groups' }, { 'value': '/access/realm' }, + { 'value': '/mapping' }, + { 'value': '/mapping/notifications' }, + { 'value': '/mapping/pci' }, + { 'value': '/mapping/usb' }, { 'value': '/nodes' }, { 'value': '/pool' }, { 'value': '/sdn/zones' }, @@ -4477,6 +4539,26 @@ Ext.define('PVE.form.AgentFeatureSelector', { }, disabled: true, }, + { + xtype: 'proxmoxcheckbox', + boxLabel: gettext('Freeze/thaw guest filesystems on backup for consistency'), + name: 'freeze-fs-on-backup', + reference: 'freeze_fs_on_backup', + bind: { + disabled: '{!enabled.checked}', + }, + disabled: true, + uncheckedValue: '0', + defaultValue: '1', + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext('Freeze/thaw for guest filesystems disabled. This can lead to inconsistent disk backups.'), + bind: { + hidden: '{freeze_fs_on_backup.checked}', + }, + }, { xtype: 'displayfield', userCls: 'pmx-hint', @@ -4503,15 +4585,33 @@ Ext.define('PVE.form.AgentFeatureSelector', { ], onGetValues: function(values) { - var agentstr = PVE.Parser.printPropertyString(values, 'enabled'); + if (PVE.Parser.parseBoolean(values['freeze-fs-on-backup'])) { + delete values['freeze-fs-on-backup']; + } + + const agentstr = PVE.Parser.printPropertyString(values, 'enabled'); return { agent: agentstr }; }, setValues: function(values) { let res = PVE.Parser.parsePropertyString(values.agent, 'enabled'); + if (!Ext.isDefined(res['freeze-fs-on-backup'])) { + res['freeze-fs-on-backup'] = 1; + } + this.callParent([res]); }, }); +Ext.define('PVE.form.BackupCompressionSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveBackupCompressionSelector'], + comboItems: [ + ['0', Proxmox.Utils.noneText], + ['lzo', 'LZO (' + gettext('fast') + ')'], + ['gzip', 'GZIP (' + gettext('good') + ')'], + ['zstd', 'ZSTD (' + gettext('fast and good') + ')'], + ], +}); Ext.define('PVE.form.BackupModeSelector', { extend: 'Proxmox.form.KVComboBox', alias: ['widget.pveBackupModeSelector'], @@ -5087,7 +5187,7 @@ Ext.define('PVE.form.ComboBoxSetStoreNode', { initComponent: function() { let me = this; - if (me.showNodeSelector && PVE.data.ResourceStore.getNodes().length > 1) { + if (me.showNodeSelector && !PVE.Utils.isStandaloneNode()) { me.errorHeight = 140; Ext.apply(me.listConfig ?? {}, { tbar: { @@ -5117,16 +5217,6 @@ Ext.define('PVE.form.ComboBoxSetStoreNode', { me.callParent(); }, }); -Ext.define('PVE.form.CompressionSelector', { - extend: 'Proxmox.form.KVComboBox', - alias: ['widget.pveCompressionSelector'], - comboItems: [ - ['0', Proxmox.Utils.noneText], - ['lzo', 'LZO (' + gettext('fast') + ')'], - ['gzip', 'GZIP (' + gettext('good') + ')'], - ['zstd', 'ZSTD (' + gettext('fast and good') + ')'], - ], -}); Ext.define('PVE.form.ContentTypeSelector', { extend: 'Proxmox.form.KVComboBox', alias: ['widget.pveContentTypeSelector'], @@ -5418,7 +5508,6 @@ Ext.define('PVE.form.DiskStorageSelector', { xtype: 'pveStorageSelector', itemId: 'hdstorage', name: 'hdstorage', - reference: 'hdstorage', fieldLabel: me.storageLabel, nodename: me.nodename, storageContent: me.storageContent, @@ -5436,7 +5525,6 @@ Ext.define('PVE.form.DiskStorageSelector', { { xtype: 'pveFileSelector', name: 'hdimage', - reference: 'hdimage', itemId: 'hdimage', fieldLabel: gettext('Disk image'), nodename: me.nodename, @@ -5446,9 +5534,8 @@ Ext.define('PVE.form.DiskStorageSelector', { { xtype: 'numberfield', itemId: 'disksize', - reference: 'disksize', name: 'disksize', - fieldLabel: gettext('Disk size') + ' (GiB)', + fieldLabel: `${gettext('Disk size')} (${gettext('GiB')})`, hidden: me.hideSize, disabled: me.hideSize, minValue: 0.001, @@ -5460,7 +5547,6 @@ Ext.define('PVE.form.DiskStorageSelector', { { xtype: 'pveDiskFormatSelector', itemId: 'diskformat', - reference: 'diskformat', name: 'diskformat', fieldLabel: gettext('Format'), nodename: me.nodename, @@ -5477,14 +5563,6 @@ Ext.define('PVE.form.DiskStorageSelector', { me.callParent(); }, }); -Ext.define('PVE.form.EmailNotificationSelector', { - extend: 'Proxmox.form.KVComboBox', - alias: ['widget.pveEmailNotificationSelector'], - comboItems: [ - ['always', gettext('Notify always')], - ['failure', gettext('On failure only')], - ], -}); Ext.define('PVE.form.FileSelector', { extend: 'Proxmox.form.ComboGrid', alias: 'widget.pveFileSelector', @@ -6172,7 +6250,7 @@ Ext.define('PVE.form.IPRefSelector', { ref_type: undefined, // undefined = any [undefined, 'ipset' or 'alias'] - valueField: 'ref', + valueField: 'scopedref', displayField: 'ref', notFoundIsValid: true, @@ -6190,7 +6268,23 @@ Ext.define('PVE.form.IPRefSelector', { var store = Ext.create('Ext.data.Store', { autoLoad: true, - fields: ['type', 'name', 'ref', 'comment'], + fields: [ + 'type', + 'name', + 'ref', + 'comment', + 'scope', + { + name: 'scopedref', + calculate: function(v) { + if (v.type === 'alias') { + return `${v.scope}/${v.name}`; + } else { + return `+${v.scope}/${v.name}`; + } + }, + }, + ], idProperty: 'ref', proxy: { type: 'proxmox', @@ -6229,17 +6323,30 @@ Ext.define('PVE.form.IPRefSelector', { hideable: false, width: 140, }, + { + header: gettext('Scope'), + dataIndex: 'scope', + hideable: false, + width: 140, + renderer: function(value) { + return value === 'dc' ? gettext("Datacenter") : gettext("Guest"); + }, + }, { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, + minWidth: 60, flex: 1, }, ); Ext.apply(me, { store: store, - listConfig: { columns: columns }, + listConfig: { + columns: columns, + width: 500, + }, }); me.on('change', disable_query_for_ips); @@ -6434,6 +6541,307 @@ Ext.define('PVE.form.MemoryField', { me.callParent(); }, }); +Ext.define('PVE.form.MultiPCISelector', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveMultiPCISelector', + + emptyText: gettext('No Devices found'), + + mixins: { + field: 'Ext.form.field.Field', + }, + + // will be called after loading finished + onLoadCallBack: Ext.emptyFn, + + getValue: function() { + let me = this; + return me.value ?? []; + }, + + getSubmitData: function() { + let me = this; + let res = {}; + res[me.name] = me.getValue(); + return res; + }, + + setValue: function(value) { + let me = this; + + value ??= []; + + me.updateSelectedDevices(value); + + return me.mixins.field.setValue.call(me, value); + }, + + getErrors: function() { + let me = this; + + let errorCls = ['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']; + + if (me.getValue().length < 1) { + let error = gettext("Must choose at least one device"); + me.addCls(errorCls); + me.getActionEl()?.dom.setAttribute('data-errorqtip', error); + + return [error]; + } + + me.removeCls(errorCls); + me.getActionEl()?.dom.setAttribute('data-errorqtip', ""); + + return []; + }, + + viewConfig: { + getRowClass: function(record) { + if (record.data.disabled === true) { + return 'x-item-disabled'; + } + return ''; + }, + }, + + updateSelectedDevices: function(value = []) { + let me = this; + + let recs = []; + let store = me.getStore(); + + for (const map of value) { + let parsed = PVE.Parser.parsePropertyString(map); + if (parsed.node !== me.nodename) { + continue; + } + + let rec = store.getById(parsed.path); + if (rec) { + recs.push(rec); + } + } + + me.suspendEvent('change'); + me.setSelection(); + me.setSelection(recs); + me.resumeEvent('change'); + }, + + setNodename: function(nodename) { + let me = this; + + if (!nodename || me.nodename === nodename) { + return; + } + + me.nodename = nodename; + + me.getStore().setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/hardware/pci?pci-class-blacklist=', + }); + + me.setSelection(); + + me.getStore().load({ + callback: (recs, op, success) => me.addSlotRecords(recs, op, success), + }); + }, + + setMdev: function(mdev) { + let me = this; + if (mdev) { + me.getStore().addFilter({ + id: 'mdev-filter', + property: 'mdev', + value: '1', + operator: '=', + }); + } else { + me.getStore().removeFilter('mdev-filter'); + } + me.setSelection(); + }, + + // adds the virtual 'slot' records (e.g. '0000:01:00') to the store + addSlotRecords: function(records, _op, success) { + let me = this; + if (!success) { + return; + } + + let slots = {}; + records.forEach((rec) => { + let slotname = rec.data.id.slice(0, -2); // remove function + if (slots[slotname] !== undefined) { + slots[slotname].count++; + rec.set('slot', slots[slotname]); + return; + } + slots[slotname] = { + count: 1, + }; + + rec.set('slot', slots[slotname]); + + if (rec.data.id.endsWith('.0')) { + slots[slotname].device = rec.data; + } + }); + + let store = me.getStore(); + + for (const [slot, { count, device }] of Object.entries(slots)) { + if (count === 1) { + continue; + } + store.add(Ext.apply({}, { + id: slot, + mdev: undefined, + device_name: gettext('Pass through all functions as one device'), + }, device)); + } + + me.updateSelectedDevices(me.value); + }, + + selectionChange: function(_grid, selection) { + let me = this; + + let ids = {}; + selection + .filter(rec => rec.data.id.indexOf('.') === -1) + .forEach((rec) => { ids[rec.data.id] = true; }); + + let to_disable = []; + + me.getStore().each(rec => { + let id = rec.data.id; + rec.set('disabled', false); + if (id.indexOf('.') === -1) { + return; + } + let slot = id.slice(0, -2); // remove function + + if (ids[slot]) { + to_disable.push(rec); + rec.set('disabled', true); + } + }); + + me.suspendEvent('selectionchange'); + me.getSelectionModel().deselect(to_disable); + me.resumeEvent('selectionchange'); + + me.value = me.getSelection().map((rec) => { + let res = { + path: rec.data.id, + node: me.nodename, + id: `${rec.data.vendor}:${rec.data.device}`.replace(/0x/g, ''), + 'subsystem-id': `${rec.data.subsystem_vendor}:${rec.data.subsystem_device}`.replace(/0x/g, ''), + }; + + if (rec.data.iommugroup !== -1) { + res.iommugroup = rec.data.iommugroup; + } + + return PVE.Parser.printPropertyString(res); + }); + me.checkChange(); + }, + + selModel: { + type: 'checkboxmodel', + mode: 'SIMPLE', + }, + + columns: [ + { + header: 'ID', + dataIndex: 'id', + renderer: function(value, _md, rec) { + if (value.match(/\.[0-9a-f]/i) && rec.data.slot?.count > 1) { + return ` ${value}`; + } + return value; + }, + width: 150, + }, + { + header: gettext('IOMMU Group'), + dataIndex: 'iommugroup', + renderer: (v, _md, rec) => rec.data.slot === rec.data.id ? '' : v === -1 ? '-' : v, + width: 50, + }, + { + header: gettext('Vendor'), + dataIndex: 'vendor_name', + flex: 3, + }, + { + header: gettext('Device'), + dataIndex: 'device_name', + flex: 6, + }, + { + header: gettext('Mediated Devices'), + dataIndex: 'mdev', + flex: 1, + renderer: function(val) { + return Proxmox.Utils.format_boolean(!!val); + }, + }, + ], + + listeners: { + selectionchange: function() { + this.selectionChange(...arguments); + }, + }, + + store: { + fields: [ + 'id', 'vendor_name', 'device_name', 'vendor', 'device', 'iommugroup', 'mdev', + 'subsystem_vendor', 'subsystem_device', 'disabled', + { + name: 'subsystem-vendor', + calculate: function(data) { + return data.subsystem_vendor; + }, + }, + { + name: 'subsystem-device', + calculate: function(data) { + return data.subsystem_device; + }, + }, + ], + sorters: [ + { + property: 'id', + direction: 'ASC', + }, + ], + }, + + initComponent: function() { + let me = this; + + let nodename = me.nodename; + me.nodename = undefined; + + me.callParent(); + + me.mon(me.getStore(), 'load', me.onLoadCallBack); + + Proxmox.Utils.monStoreErrors(me, me.getStore(), true); + + me.setNodename(nodename); + + me.initField(); + }, +}); Ext.define('PVE.form.NetworkCardSelector', { extend: 'Proxmox.form.KVComboBox', alias: 'widget.pveNetworkCardSelector', @@ -6458,10 +6866,7 @@ Ext.define('PVE.form.NodeSelector', { // only allow those nodes (array) allowedNodes: undefined, - // set default value to empty array, else it inits it with - // null and after the store load it is an empty array, - // triggering dirtychange - value: [], + valueField: 'node', displayField: 'node', store: { @@ -6550,6 +6955,76 @@ Ext.define('PVE.form.NodeSelector', { me.mon(me.getStore(), 'load', () => me.isValid()); }, }); +Ext.define('PVE.form.NotificationModeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveNotificationModeSelector'], + comboItems: [ + ['notification-target', gettext('Target')], + ['mailto', gettext('E-Mail')], + ], +}); +Ext.define('PVE.form.NotificationTargetSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveNotificationTargetSelector'], + + // set default value to empty array, else it inits it with + // null and after the store load it is an empty array, + // triggering dirtychange + value: [], + valueField: 'name', + displayField: 'name', + deleteEmpty: true, + skipEmptyText: true, + + store: { + fields: ['name', 'type', 'comment'], + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/notifications/targets', + }, + sorters: [ + { + property: 'name', + direction: 'ASC', + }, + ], + autoLoad: true, + }, + + listConfig: { + columns: [ + { + header: gettext('Target'), + dataIndex: 'name', + sortable: true, + hideable: false, + flex: 1, + }, + { + header: gettext('Type'), + dataIndex: 'type', + sortable: true, + hideable: false, + flex: 1, + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + sortable: true, + hideable: false, + flex: 2, + }, + ], + }, +}); +Ext.define('PVE.form.EmailNotificationSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveEmailNotificationSelector'], + comboItems: [ + ['always', gettext('Always')], + ['failure', gettext('On failure only')], + ], +}); Ext.define('PVE.form.PCISelector', { extend: 'Proxmox.form.ComboGrid', xtype: 'pvePCISelector', @@ -6574,6 +7049,7 @@ Ext.define('PVE.form.PCISelector', { onLoadCallBack: undefined, listConfig: { + minHeight: 80, width: 800, columns: [ { @@ -6641,6 +7117,119 @@ Ext.define('PVE.form.PCISelector', { }, }); +Ext.define('pve-mapped-pci-model', { + extend: 'Ext.data.Model', + + fields: ['id', 'path', 'vendor', 'device', 'iommugroup', 'mdev', 'description', 'map'], + idProperty: 'id', +}); + +Ext.define('PVE.form.PCIMapSelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pvePCIMapSelector', + + store: { + model: 'pve-mapped-pci-model', + filterOnLoad: true, + sorters: [ + { + property: 'id', + direction: 'ASC', + }, + ], + }, + + autoSelect: false, + valueField: 'id', + displayField: 'id', + + // can contain a load callback for the store + // useful to determine the state of the IOMMU + onLoadCallBack: undefined, + + listConfig: { + width: 800, + columns: [ + { + header: gettext('ID'), + dataIndex: 'id', + flex: 1, + }, + { + header: gettext('Description'), + dataIndex: 'description', + flex: 1, + renderer: Ext.String.htmlEncode, + }, + { + header: gettext('Status'), + dataIndex: 'checks', + renderer: function(value) { + let me = this; + + if (!Ext.isArray(value) || !value?.length) { + return ` ${gettext('Mapping matches host data')}`; + } + + let checks = []; + + value.forEach((check) => { + let iconCls; + switch (check?.severity) { + case 'warning': + iconCls = 'fa-exclamation-circle warning'; + break; + case 'error': + iconCls = 'fa-times-circle critical'; + break; + } + + let message = check?.message; + let icon = ``; + if (iconCls !== undefined) { + checks.push(`${icon} ${message}`); + } + }); + + return checks.join('
'); + }, + flex: 3, + }, + ], + }, + + setNodename: function(nodename) { + var me = this; + + if (!nodename || me.nodename === nodename) { + return; + } + + me.nodename = nodename; + + me.store.setProxy({ + type: 'proxmox', + url: `/api2/json/cluster/mapping/pci?check-node=${nodename}`, + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + me.nodename = undefined; + + me.callParent(); + + if (me.onLoadCallBack !== undefined) { + me.mon(me.getStore(), 'load', me.onLoadCallBack); + } + + me.setNodename(nodename); + }, +}); Ext.define('PVE.form.PermPathSelector', { extend: 'Ext.form.field.ComboBox', xtype: 'pvePermPathSelector', @@ -6649,6 +7238,7 @@ Ext.define('PVE.form.PermPathSelector', { displayField: 'value', typeAhead: true, queryMode: 'local', + width: 380, store: { type: 'pvePermPath', @@ -6888,7 +7478,7 @@ Ext.define('PVE.form.SDNVnetSelector', { listConfig: { columns: [ { - header: gettext('Vnet'), + header: gettext('VNet'), sortable: true, dataIndex: 'vnet', flex: 1, @@ -7693,25 +8283,39 @@ Ext.define('PVE.form.USBSelector', { return gettext("Invalid Value"); }, - initComponent: function() { + setNodename: function(nodename) { var me = this; - var nodename = me.pveSelNode.data.node; + if (!nodename || me.nodename === nodename) { + return; + } - if (!nodename) { - throw "no nodename specified"; + me.nodename = nodename; + + me.store.setProxy({ + type: 'proxmox', + url: `/api2/json/nodes/${me.nodename}/hardware/usb`, + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + if (me.pveSelNode) { + me.nodename = me.pveSelNode.data.node; } + var nodename = me.nodename; + me.nodename = undefined; + if (me.type !== 'device' && me.type !== 'port') { throw "no valid type specified"; } let store = new Ext.data.Store({ model: `pve-usb-${me.type}`, - proxy: { - type: 'proxmox', - url: `/api2/json/nodes/${nodename}/hardware/usb`, - }, filters: [ ({ data }) => !!data.usbpath && !!data.prodid && String(data.class) !== "9", ], @@ -7727,6 +8331,7 @@ Ext.define('PVE.form.USBSelector', { store: store, emptyText: emptyText, listConfig: { + minHeight: 80, width: 520, columns: [ { @@ -7769,7 +8374,7 @@ Ext.define('PVE.form.USBSelector', { me.callParent(); - store.load(); + me.setNodename(nodename); }, }, function() { @@ -7795,7 +8400,7 @@ Ext.define('PVE.form.USBSelector', { name: 'product_and_id', type: 'string', convert: (v, rec) => { - let res = rec.data.product || gettext('Unkown'); + let res = rec.data.product || gettext('Unknown'); res += " (" + rec.data.usbid + ")"; return res; }, @@ -7833,6 +8438,105 @@ Ext.define('PVE.form.USBSelector', { ], }); }); +Ext.define('PVE.form.USBMapSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveUSBMapSelector', + + store: { + fields: ['name', 'vendor', 'device', 'path'], + filterOnLoad: true, + sorters: [ + { + property: 'name', + direction: 'ASC', + }, + ], + }, + + allowBlank: false, + autoSelect: false, + displayField: 'id', + valueField: 'id', + + listConfig: { + width: 800, + columns: [ + { + header: gettext('Name'), + dataIndex: 'id', + flex: 1, + }, + { + header: gettext('Status'), + dataIndex: 'errors', + flex: 2, + renderer: function(value) { + let me = this; + + if (!Ext.isArray(value) || !value?.length) { + return ` ${gettext('Mapping matches host data')}`; + } + + let errors = []; + + value.forEach((error) => { + let iconCls; + switch (error?.severity) { + case 'warning': + iconCls = 'fa-exclamation-circle warning'; + break; + case 'error': + iconCls = 'fa-times-circle critical'; + break; + } + + let message = error?.message; + let icon = ``; + if (iconCls !== undefined) { + errors.push(`${icon} ${message}`); + } + }); + + return errors.join('
'); + }, + }, + { + header: gettext('Comment'), + dataIndex: 'description', + flex: 1, + renderer: Ext.String.htmlEncode, + }, + ], + }, + + setNodename: function(nodename) { + var me = this; + + if (!nodename || me.nodename === nodename) { + return; + } + + me.nodename = nodename; + + me.store.setProxy({ + type: 'proxmox', + url: `/api2/json/cluster/mapping/usb?check-node=${nodename}`, + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + me.nodename = undefined; + + me.callParent(); + + me.setNodename(nodename); + }, +}); Ext.define('pmx-users', { extend: 'Ext.data.Model', fields: [ @@ -7842,7 +8546,7 @@ Ext.define('pmx-users', { ], proxy: { type: 'proxmox', - url: "/api2/json/access/users", + url: "/api2/json/access/users?full=1", }, idProperty: 'userid', }); @@ -7852,7 +8556,7 @@ Ext.define('PVE.form.VlanField', { deleteEmpty: false, - emptyText: 'no VLAN', + emptyText: gettext('no VLAN'), fieldLabel: gettext('VLAN Tag'), @@ -8085,6 +8789,8 @@ Ext.define('PVE.form.VMSelector', { sorters: 'vmid', }, + userCls: 'proxmox-tags-circle', + columnsDeclaration: [ { header: 'ID', @@ -8147,6 +8853,12 @@ Ext.define('PVE.form.VMSelector', { }, }, }, + { + header: gettext('Tags'), + dataIndex: 'tags', + renderer: tags => PVE.Utils.renderTags(tags, PVE.UIOptions.tagOverrides), + flex: 1, + }, { header: 'HA ' + gettext('Status'), dataIndex: 'hastate', @@ -8203,7 +8915,11 @@ Ext.define('PVE.form.VMSelector', { let selection = value.map(item => { let found = store.findRecord('vmid', item, 0, false, true, true); if (!found) { - notFound.push(item); + if (Ext.isNumeric(item)) { + notFound.push(item); + } else { + console.warn(`invalid item in vm selection: ${item}`); + } } return found; }).filter(r => r); @@ -8228,8 +8944,9 @@ Ext.define('PVE.form.VMSelector', { setValue: function(value) { let me = this; + value ??= []; if (!Ext.isArray(value)) { - value = value.split(','); + value = value.split(',').filter(v => v !== ''); } let store = me.getStore(); @@ -8248,7 +8965,7 @@ Ext.define('PVE.form.VMSelector', { getErrors: function(value) { let me = this; if (!me.isDisabled() && me.allowBlank === false && - me.getSelectionModel().getCount() === 0) { + me.getValue().length === 0) { me.addBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']); return [gettext('No VM selected')]; } @@ -8511,6 +9228,7 @@ Ext.define('PVE.form.iScsiProviderSelector', { alias: ['widget.pveiScsiProviderSelector'], comboItems: [ ['comstar', 'Comstar'], + ['freenas', 'FreeNAS/TrueNAS API'], ['istgt', 'istgt'], ['iet', 'IET'], ['LIO', 'LIO'], @@ -9019,6 +9737,7 @@ Ext.define('PVE.form.ListField', { text: gettext('Add'), iconCls: 'fa fa-plus-circle', handler: 'addLine', + margin: '5 0 0 0', }, ], @@ -9075,6 +9794,15 @@ Ext.define('Proxmox.form.Tag', { '', ], + focusable: true, + getFocusEl: function() { + return Ext.get(this.tagEl()); + }, + + onFocus: function() { + this.selectText(); + }, + // contains tags not to show in the picker and not allowing to set filter: [], @@ -9292,6 +10020,7 @@ Ext.define('PVE.panel.TagEditContainer', { // set to false to hide the 'no tags' field and the edit button canEdit: true, + editOnly: false, controller: { xclass: 'Ext.app.ViewController', @@ -9499,6 +10228,9 @@ Ext.define('PVE.panel.TagEditContainer', { me.tagsChanged(); }, keypress: function(key) { + if (vm.get('hideFinishButtons')) { + return; + } if (key === 'Enter') { me.editClick(); } else if (key === 'Escape') { @@ -9536,20 +10268,40 @@ Ext.define('PVE.panel.TagEditContainer', { me.loadTags(view.tags); } me.getViewModel().set('canEdit', view.canEdit); + me.getViewModel().set('editOnly', view.editOnly); me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => { + let vm = me.getViewModel(); view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags()); - me.loadTags(me.oldTags, true); // refresh tag colors and order + me.loadTags(me.oldTags, !vm.get('editMode')); // refresh tag colors and order }); + + if (view.editOnly) { + me.toggleEdit(); + } }, }, + getTags: function() { + let me =this; + let controller = me.getController(); + let tags = []; + controller.forEachTag((cmp) => { + if (cmp.tag.length) { + tags.push(cmp.tag); + } + }); + + return tags; + }, + viewModel: { data: { tagCount: 0, editMode: false, canEdit: true, isDirty: false, + editOnly: true, }, formulas: { @@ -9559,6 +10311,9 @@ Ext.define('PVE.panel.TagEditContainer', { hideEditBtn: function(get) { return get('editMode') || !get('canEdit'); }, + hideFinishButtons: function(get) { + return !get('editMode') || get('editOnly'); + }, }, }, @@ -9594,7 +10349,7 @@ Ext.define('PVE.panel.TagEditContainer', { xtype: 'tbseparator', ui: 'horizontal', bind: { - hidden: '{!editMode}', + hidden: '{hideFinishButtons}', }, hidden: true, }, @@ -9603,7 +10358,7 @@ Ext.define('PVE.panel.TagEditContainer', { iconCls: 'fa fa-times', tooltip: gettext('Cancel Edit'), bind: { - hidden: '{!editMode}', + hidden: '{hideFinishButtons}', }, hidden: true, margin: '0 5 0 0', @@ -9615,7 +10370,7 @@ Ext.define('PVE.panel.TagEditContainer', { iconCls: 'fa fa-check', tooltip: gettext('Finish Edit'), bind: { - hidden: '{!editMode}', + hidden: '{hideFinishButtons}', disabled: '{!isDirty}', }, hidden: true, @@ -9647,6 +10402,241 @@ Ext.define('PVE.panel.TagEditContainer', { me.callParent(); }, }); +// mostly copied from ExtJS FileButton, but added 'multiple' at the relevant +// places so we have a file picker where one can select multiple files +// changes are marked with an 'pmx:' comment +Ext.define('PVE.form.MultiFileButton', { + extend: 'Ext.form.field.FileButton', + alias: 'widget.pveMultiFileButton', + + afterTpl: [ + 'accept="{accept}"', + 'tabindex="{tabIndex}"', + '>', + ], + + createFileInput: function(isTemporary) { + var me = this, + fileInputEl, listeners; + + fileInputEl = me.fileInputEl = me.el.createChild({ + name: me.inputName || me.id, + multiple: true, // pmx: added multiple option + id: !isTemporary ? me.id + '-fileInputEl' : undefined, + cls: me.inputCls + (me.getInherited().rtl ? ' ' + Ext.baseCSSPrefix + 'rtl' : ''), + tag: 'input', + type: 'file', + size: 1, + unselectable: 'on', + }, me.afterInputGuard); // Nothing special happens outside of IE/Edge + + // This is our focusEl + fileInputEl.dom.setAttribute('data-componentid', me.id); + + if (me.tabIndex !== null) { + me.setTabIndex(me.tabIndex); + } + + if (me.accept) { + fileInputEl.dom.setAttribute('accept', me.accept); + } + + // We place focus and blur listeners on fileInputEl to activate Button's + // focus and blur style treatment + listeners = { + scope: me, + change: me.fireChange, + mousedown: me.handlePrompt, + keydown: me.handlePrompt, + focus: me.onFileFocus, + blur: me.onFileBlur, + }; + + if (me.useTabGuards) { + listeners.keydown = me.onFileInputKeydown; + } + + fileInputEl.on(listeners); + }, +}); +Ext.define('PVE.form.TagFieldSet', { + extend: 'Ext.form.FieldSet', + alias: 'widget.pveTagFieldSet', + mixins: ['Ext.form.field.Field'], + + title: gettext('Tags'), + padding: '0 5 5 5', + + getValue: function() { + let me = this; + let tags = me.down('pveTagEditContainer').getTags().filter(t => t !== ''); + return tags.join(';'); + }, + + setValue: function(value) { + let me = this; + value ??= []; + if (!Ext.isArray(value)) { + value = value.split(/[;, ]/).filter(t => t !== ''); + } + me.down('pveTagEditContainer').loadTags(value.join(';')); + }, + + getErrors: function(value) { + value ??= []; + if (!Ext.isArray(value)) { + value = value.split(/[;, ]/).filter(t => t !== ''); + } + if (value.some(t => !t.match(PVE.Utils.tagCharRegex))) { + return [gettext("Tags contain invalid characters.")]; + } + return []; + }, + + getSubmitData: function() { + let me = this; + let value = me.getValue(); + if (me.disabled || !me.submitValue || value === '') { + return null; + } + let data = {}; + data[me.getName()] = value; + return data; + }, + + layout: 'fit', + + items: [ + { + xtype: 'pveTagEditContainer', + userCls: 'proxmox-tags-full proxmox-tag-fieldset', + editOnly: true, + allowBlank: true, + layout: 'column', + scrollable: true, + }, + ], + + initComponent: function() { + let me = this; + me.callParent(); + me.initField(); + }, +}); +Ext.define('PVE.form.IsoSelector', { + extend: 'Ext.container.Container', + alias: 'widget.pveIsoSelector', + mixins: [ + 'Ext.form.field.Field', + 'Proxmox.Mixin.CBind', + ], + + layout: { + type: 'vbox', + align: 'stretch', + }, + + nodename: undefined, + insideWizard: false, + + cbindData: function() { + let me = this; + return { + nodename: me.nodename, + insideWizard: me.insideWizard, + }; + }, + + getValue: function() { + return this.lookup('file').getValue(); + }, + + setValue: function(value) { + let me = this; + if (!value) { + me.lookup('file').reset(); + return; + } + var match = value.match(/^([^:]+):/); + if (match) { + me.lookup('storage').setValue(match[1]); + me.lookup('file').setValue(value); + } + }, + + getErrors: function() { + let me = this; + me.lookup('storage').validate(); + let file = me.lookup('file'); + file.validate(); + let value = file.getValue(); + if (!value || !value.length) { + return [""]; // for validation + } + return []; + }, + + setNodename: function(nodename) { + let me = this; + me.lookup('storage').setNodename(nodename); + me.lookup('file').setStorage(undefined, nodename); + }, + + setDisabled: function(disabled) { + let me = this; + me.lookup('storage').setDisabled(disabled); + me.lookup('file').setDisabled(disabled); + me.callParent(); + }, + + referenceHolder: true, + + items: [ + { + xtype: 'pveStorageSelector', + reference: 'storage', + isFormField: false, + fieldLabel: gettext('Storage'), + labelAlign: 'right', + storageContent: 'iso', + allowBlank: false, + cbind: { + nodename: '{nodename}', + autoSelect: '{insideWizard}', + insideWizard: '{insideWizard}', + disabled: '{disabled}', + }, + listeners: { + change: function(f, value) { + let me = this; + let selector = me.up('pveIsoSelector'); + selector.lookup('file').setStorage(value); + selector.checkChange(); + }, + }, + }, + { + xtype: 'pveFileSelector', + reference: 'file', + isFormField: false, + storageContent: 'iso', + fieldLabel: gettext('ISO image'), + labelAlign: 'right', + cbind: { + nodename: '{nodename}', + disabled: '{disabled}', + }, + allowBlank: false, + listeners: { + change: function() { + this.up('pveIsoSelector').checkChange(); + }, + }, + }, + ], +}); Ext.define('PVE.grid.BackupView', { extend: 'Ext.grid.GridPanel', @@ -10013,7 +11003,7 @@ Ext.define('PVE.grid.BackupView', { dataIndex: 'size', }, { - header: gettext('VMID'), + header: 'VMID', dataIndex: 'vmid', hidden: true, }, @@ -10139,6 +11129,9 @@ Ext.define('PVE.FirewallAliases', { let sm = Ext.create('Ext.selection.RowModel', {}); + let caps = Ext.state.Manager.get('GuiCap'); + let canEdit = !!caps.vms['VM.Config.Network'] || !!caps.dc['Sys.Modify'] || !!caps.nodes['Sys.Modify']; + let reload = function() { let oldrec = sm.getSelection()[0]; store.load(function(records, operation, success) { @@ -10153,7 +11146,7 @@ Ext.define('PVE.FirewallAliases', { let run_editor = function() { let rec = me.getSelectionModel().getSelection()[0]; - if (!rec) { + if (!rec || !canEdit) { return; } let win = Ext.create('PVE.FirewallAliasEdit', { @@ -10168,11 +11161,13 @@ Ext.define('PVE.FirewallAliases', { text: gettext('Edit'), disabled: true, selModel: sm, + enableFn: rec => canEdit, handler: run_editor, }); me.addBtn = Ext.create('Ext.Button', { text: gettext('Add'), + disabled: !caps.vms['VM.Config.Network'] && !caps.dc['Sys.Modify'] && !caps.nodes['Sys.Modify'], handler: function() { var win = Ext.create('PVE.FirewallAliasEdit', { base_url: me.base_url, @@ -10183,7 +11178,9 @@ Ext.define('PVE.FirewallAliases', { }); me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + disabled: true, selModel: sm, + enableFn: rec => !!caps.vms['VM.Config.Network'] || !!caps.dc['Sys.Modify'] || !!caps.nodes['Sys.Modify'], baseurl: me.base_url + '/', callback: reload, }); @@ -10243,6 +11240,9 @@ Ext.define('PVE.FirewallOptions', { throw "unknown firewall option type"; } + let caps = Ext.state.Manager.get('GuiCap'); + let canEdit = caps.vms['VM.Config.Network'] || caps.dc['Sys.Modify'] || caps.nodes['Sys.Modify']; + me.rows = {}; var add_boolean_row = function(name, text, defaultValue) { @@ -10383,7 +11383,9 @@ Ext.define('PVE.FirewallOptions', { return; } var rowdef = me.rows[rec.data.key]; - edit_btn.setDisabled(!rowdef.editor); + if (canEdit) { + edit_btn.setDisabled(!rowdef.editor); + } }; Ext.apply(me, { @@ -10393,7 +11395,7 @@ Ext.define('PVE.FirewallOptions', { url: '/api2/extjs/' + me.base_url, }, listeners: { - itemdblclick: me.run_editor, + itemdblclick: () => { if (canEdit) { me.run_editor(); } }, selectionchange: set_button_status, }, }); @@ -10654,7 +11656,6 @@ Ext.define('PVE.FirewallRulePanel', { autoSelect: false, editable: true, base_url: me.list_refs_url, - value: '', fieldLabel: gettext('Source'), maxLength: 512, maxLengthText: gettext('Too long, consider using IP sets.'), @@ -10665,7 +11666,6 @@ Ext.define('PVE.FirewallRulePanel', { autoSelect: false, editable: true, base_url: me.list_refs_url, - value: '', fieldLabel: gettext('Destination'), maxLength: 512, maxLengthText: gettext('Too long, consider using IP sets.'), @@ -10989,11 +11989,14 @@ Ext.define('PVE.FirewallRules', { } me.store.removeAll(); } else { - me.addBtn.setDisabled(false); - me.removeBtn.baseurl = url + '/'; - if (me.groupBtn) { - me.groupBtn.setDisabled(false); + if (me.canEdit) { + me.addBtn.setDisabled(false); + if (me.groupBtn) { + me.groupBtn.setDisabled(false); + } } + me.removeBtn.baseurl = url + '/'; + me.store.setProxy({ type: 'proxmox', url: '/api2/json' + url, @@ -11069,9 +12072,12 @@ Ext.define('PVE.FirewallRules', { var sm = Ext.create('Ext.selection.RowModel', {}); + me.caps = Ext.state.Manager.get('GuiCap'); + me.canEdit = !!me.caps.vms['VM.Config.Network'] || !!me.caps.dc['Sys.Modify'] || !!me.caps.nodes['Sys.Modify']; + var run_editor = function() { var rec = sm.getSelection()[0]; - if (!rec) { + if (!rec || !me.canEdit) { return; } var type = rec.data.type; @@ -11100,6 +12106,7 @@ Ext.define('PVE.FirewallRules', { me.editBtn = Ext.create('Proxmox.button.Button', { text: gettext('Edit'), disabled: true, + enableFn: rec => me.canEdit, selModel: sm, handler: run_editor, }); @@ -11141,7 +12148,7 @@ Ext.define('PVE.FirewallRules', { me.copyBtn = Ext.create('Proxmox.button.Button', { text: gettext('Copy'), selModel: sm, - enableFn: ({ data }) => data.type === 'in' || data.type === 'out', + enableFn: ({ data }) => (data.type === 'in' || data.type === 'out') && me.canEdit, disabled: true, handler: run_copy_editor, }); @@ -11163,6 +12170,7 @@ Ext.define('PVE.FirewallRules', { } me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + enableFn: rec => me.canEdit, selModel: sm, baseurl: me.base_url + '/', confirmMsg: false, @@ -11404,10 +12412,16 @@ Ext.define('PVE.FirewallRules', { }); Ext.define('PVE.pool.AddVM', { extend: 'Proxmox.window.Edit', - width: 600, - height: 400, + + width: 640, + height: 480, isAdd: true, isCreate: true, + + extraRequestParams: { + 'allow-move': 1, + }, + initComponent: function() { var me = this; @@ -11415,8 +12429,9 @@ Ext.define('PVE.pool.AddVM', { throw "no pool specified"; } - me.url = "/pools/" + me.pool; + me.url = '/pools/'; me.method = 'PUT'; + me.extraRequestParams.poolid = me.pool; var vmsField = Ext.create('Ext.form.field.Text', { name: 'vms', @@ -11434,7 +12449,7 @@ Ext.define('PVE.pool.AddVM', { ], filters: [ function(item) { - return (item.data.type === 'lxc' || item.data.type === 'qemu') && item.data.pool === ''; + return (item.data.type === 'lxc' || item.data.type === 'qemu') &&item.data.pool !== me.pool; }, ], }); @@ -11442,7 +12457,7 @@ Ext.define('PVE.pool.AddVM', { var vmGrid = Ext.create('widget.grid', { store: vmStore, border: true, - height: 300, + height: 360, scrollable: true, selModel: { selType: 'checkboxmodel', @@ -11467,16 +12482,14 @@ Ext.define('PVE.pool.AddVM', { header: gettext('Node'), dataIndex: 'node', }, + { + header: gettext('Current Pool'), + dataIndex: 'pool', + }, { header: gettext('Status'), dataIndex: 'uptime', - renderer: function(value) { - if (value) { - return Proxmox.Utils.runningText; - } else { - return Proxmox.Utils.stoppedText; - } - }, + renderer: v => v ? Proxmox.Utils.runningText : Proxmox.Utils.stoppedText, }, { header: gettext('Name'), @@ -11489,9 +12502,18 @@ Ext.define('PVE.pool.AddVM', { }, ], }); + Ext.apply(me, { subject: gettext('Virtual Machine'), - items: [vmsField, vmGrid], + items: [ + vmsField, + vmGrid, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext('Selected guests who are already part of a pool will be removed from it first.'), + }, + ], }); me.callParent(); @@ -11511,8 +12533,9 @@ Ext.define('PVE.pool.AddStorage', { me.isCreate = true; me.isAdd = true; - me.url = "/pools/" + me.pool; + me.url = "/pools/"; me.method = 'PUT'; + me.extraRequestParams.poolid = me.pool; Ext.apply(me, { subject: gettext('Storage'), @@ -11559,8 +12582,8 @@ Ext.define('PVE.grid.PoolMembers', { ], proxy: { type: 'proxmox', - root: 'data.members', - url: "/api2/json/pools/" + me.pool, + root: 'data[0].members', + url: "/api2/json/pools/?poolid=" + me.pool, }, }); @@ -11583,7 +12606,7 @@ Ext.define('PVE.grid.PoolMembers', { "'" + rec.data.id + "'"); }, handler: function(btn, event, rec) { - var params = { 'delete': 1 }; + var params = { 'delete': 1, poolid: me.pool }; if (rec.data.type === 'storage') { params.storage = rec.data.storage; } else if (rec.data.type === 'qemu' || rec.data.type === 'lxc' || rec.data.type === 'openvz') { @@ -11593,7 +12616,7 @@ Ext.define('PVE.grid.PoolMembers', { } Proxmox.Utils.API2Request({ - url: '/pools/' + me.pool, + url: '/pools/', method: 'PUT', params: params, waitMsgTarget: me, @@ -11877,7 +12900,7 @@ Ext.define('PVE.grid.ReplicaView', { // currently replication is for cluster only, so disable the whole component for non-cluster checkPrerequisites: function() { let view = this.getView(); - if (PVE.data.ResourceStore.getNodes().length < 2) { + if (PVE.Utils.isStandaloneNode()) { view.mask(gettext("Replication needs at least two nodes"), ['pve-static-mask']); } }, @@ -11940,8 +12963,7 @@ Ext.define('PVE.grid.ReplicaView', { width: 80, dataIndex: 'enabled', align: 'center', - // TODO: switch to Proxmox.Utils.renderEnabledIcon once available - renderer: enabled => ``, + renderer: Proxmox.Utils.renderEnabledIcon, sortable: true, }, { @@ -12108,7 +13130,7 @@ Ext.define('PVE.grid.ReplicaView', { // if we set the warning mask, we do not want to load // or set the mask on store errors - if (PVE.data.ResourceStore.getNodes().length < 2) { + if (PVE.Utils.isStandaloneNode()) { return; } @@ -12809,6 +13831,9 @@ Ext.define('PVE.IPSetList', { }, }); + var caps = Ext.state.Manager.get('GuiCap'); + let canEdit = !!caps.vms['VM.Config.Network'] || !!caps.dc['Sys.Modify'] || !!caps.nodes['Sys.Modify']; + var sm = Ext.create('Ext.selection.RowModel', {}); var reload = function() { @@ -12825,7 +13850,7 @@ Ext.define('PVE.IPSetList', { var run_editor = function() { var rec = sm.getSelection()[0]; - if (!rec) { + if (!rec || !canEdit) { return; } var win = Ext.create('Proxmox.window.Edit', { @@ -12861,6 +13886,7 @@ Ext.define('PVE.IPSetList', { me.editBtn = new Proxmox.button.Button({ text: gettext('Edit'), disabled: true, + enableFn: rec => canEdit, selModel: sm, handler: run_editor, }); @@ -12895,6 +13921,7 @@ Ext.define('PVE.IPSetList', { }); me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + enableFn: rec => canEdit, selModel: sm, baseurl: me.base_url + '/', callback: reload, @@ -12921,6 +13948,10 @@ Ext.define('PVE.IPSetList', { }, }); + if (!canEdit) { + me.addBtn.setDisabled(true); + } + me.callParent(); store.load(); @@ -12960,7 +13991,7 @@ Ext.define('PVE.IPSetCidrEdit', { autoSelect: false, editable: true, base_url: me.list_refs_url, - value: '', + allowBlank: false, fieldLabel: gettext('IP/CIDR'), }); } else { @@ -13035,7 +14066,9 @@ Ext.define('PVE.IPSetGrid', { me.addBtn.setDisabled(true); me.store.removeAll(); } else { - me.addBtn.setDisabled(false); + if (me.canEdit) { + me.addBtn.setDisabled(false); + } me.removeBtn.baseurl = url + '/'; me.store.setProxy({ type: 'proxmox', @@ -13063,9 +14096,12 @@ Ext.define('PVE.IPSetGrid', { var sm = Ext.create('Ext.selection.RowModel', {}); + me.caps = Ext.state.Manager.get('GuiCap'); + me.canEdit = !!me.caps.vms['VM.Config.Network'] || !!me.caps.dc['Sys.Modify'] || !!me.caps.nodes['Sys.Modify']; + var run_editor = function() { var rec = sm.getSelection()[0]; - if (!rec) { + if (!rec || !me.canEdit) { return; } var win = Ext.create('PVE.IPSetCidrEdit', { @@ -13079,6 +14115,7 @@ Ext.define('PVE.IPSetGrid', { me.editBtn = new Proxmox.button.Button({ text: gettext('Edit'), disabled: true, + enableFn: rec => me.canEdit, selModel: sm, handler: run_editor, }); @@ -13086,6 +14123,7 @@ Ext.define('PVE.IPSetGrid', { me.addBtn = new Proxmox.button.Button({ text: gettext('Add'), disabled: true, + enableFn: rec => me.canEdit, handler: function() { if (!me.base_url) { return; @@ -13100,6 +14138,8 @@ Ext.define('PVE.IPSetGrid', { }); me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + disabled: true, + enableFn: rec => me.canEdit, selModel: sm, baseurl: me.base_url + '/', callback: reload, @@ -13479,6 +14519,30 @@ Ext.define('PVE.panel.GuestStatusView', { }; }, + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + if (view.pveSelNode.data.type !== 'lxc') { + return; + } + + const nodename = view.pveSelNode.data.node; + const vmid = view.pveSelNode.data.vmid; + + Proxmox.Utils.API2Request({ + url: `/api2/extjs/nodes/${nodename}/lxc/${vmid}/config`, + waitMsgTargetView: view, + method: 'GET', + success: ({ result }) => { + view.down('#unprivileged').updateValue( + Proxmox.Utils.format_boolean(result.data.unprivileged)); + view.ostype = Ext.htmlEncode(result.data.ostype); + }, + }); + }, + }, + layout: { type: 'vbox', align: 'stretch', @@ -13526,6 +14590,15 @@ Ext.define('PVE.panel.GuestStatusView', { }, printBar: false, }, + { + itemId: 'unprivileged', + iconCls: 'fa fa-lock fa-fw', + title: gettext('Unprivileged'), + printBar: false, + cbind: { + hidden: '{isQemu}', + }, + }, { xtype: 'box', height: 15, @@ -13602,7 +14675,23 @@ Ext.define('PVE.panel.GuestStatusView', { + ')'; } - me.setTitle(me.getRecordValue('name') + text); + let title = `
${me.getRecordValue('name') + text}
`; + + if (me.pveSelNode.data.type === 'lxc' && me.ostype && me.ostype !== 'unmanaged') { + // Manual mappings for distros with special casing + const namemap = { + 'archlinux': 'Arch Linux', + 'nixos': 'NixOS', + 'opensuse': 'openSUSE', + 'centos': 'CentOS', + }; + + const distro = namemap[me.ostype] ?? Ext.String.capitalize(me.ostype); + title += `
+  ${distro}
`; + } + + me.setTitle(title); }, }); Ext.define('PVE.guest.Summary', { @@ -14219,7 +15308,6 @@ Ext.define('PVE.tree.ResourceTree', { } }, - // add additional elements to text. Currently only the usage indicator for storages setText: function(info) { let me = this; @@ -14240,15 +15328,13 @@ Ext.define('PVE.tree.ResourceTree', { info.text = `${info.name} (${String(info.vmid)})`; } } - + info.text = `${status}${info.text}`; info.text += PVE.Utils.renderTags(info.tags, PVE.UIOptions.tagOverrides); - - info.text = status + info.text; }, - setToolTip: function(info) { + getToolTip: function(info) { if (info.type === 'pool' || info.groupbyid !== undefined) { - return; + return undefined; } let qtips = [gettext('Status') + ': ' + (info.qmpstatus || info.status)]; @@ -14258,8 +15344,16 @@ Ext.define('PVE.tree.ResourceTree', { if (info.hastate !== 'unmanaged') { qtips.push(gettext('HA State') + ": " + info.hastate); } + if (info.type === 'storage') { + let usage = info.disk / info.maxdisk; + if (usage >= 0.0 && usage <= 1.0) { + qtips.push(Ext.String.format(gettext("Usage: {0}%"), (usage*100).toFixed(2))); + } + } - info.qtip = qtips.join(', '); + let tip = qtips.join(', '); + info.tip = tip; + return tip; }, // private @@ -14268,7 +15362,6 @@ Ext.define('PVE.tree.ResourceTree', { me.setIconCls(info); me.setText(info); - me.setToolTip(info); if (info.groupbyid) { info.text = info.groupbyid; @@ -14425,7 +15518,6 @@ Ext.define('PVE.tree.ResourceTree', { Ext.apply(info, item.data); me.setIconCls(info); me.setText(info); - me.setToolTip(info); olditem.commit(); } if ((!item || moved) && olditem.isLeaf()) { @@ -14513,6 +15605,32 @@ Ext.define('PVE.tree.ResourceTree', { return allow; }, itemdblclick: PVE.Utils.openTreeConsole, + afterrender: function() { + if (me.tip) { + return; + } + let selectors = [ + '.x-tree-node-text > span:not(.proxmox-tag-dark):not(.proxmox-tag-light)', + '.x-tree-icon', + ]; + me.tip = Ext.create('Ext.tip.ToolTip', { + target: me.el, + delegate: selectors.join(', '), + trackMouse: true, + renderTo: Ext.getBody(), + listeners: { + beforeshow: function(tip) { + let rec = me.getView().getRecord(tip.triggerElement); + let tipText = me.getToolTip(rec.data); + if (tipText) { + tip.update(tipText); + return true; + } + return false; + }, + }, + }); + }, }, setViewFilter: function(view) { me.viewFilter = view; @@ -14950,6 +16068,647 @@ Ext.define('PVE.guest.SnapshotTree', { ], }); +Ext.define('PVE.tree.ResourceMapTree', { + extend: 'Ext.tree.Panel', + alias: 'widget.pveResourceMapTree', + mixins: ['Proxmox.Mixin.CBind'], + + rootVisible: false, + + emptyText: gettext('No Mapping found'), + + // will be opened on edit + editWindowClass: undefined, + + // The base url of the resource + baseUrl: undefined, + + // icon class to show on the entries + mapIconCls: undefined, + + // if given, should be a function that takes a nodename and returns + // the url for getting the data to check the status + getStatusCheckUrl: undefined, + + // the result of above api call and the nodename is passed and can set the status + checkValidity: undefined, + + // the property that denotes a single map entry for a node + entryIdProperty: undefined, + + cbindData: function(initialConfig) { + let me = this; + const caps = Ext.state.Manager.get('GuiCap'); + me.canConfigure = !!caps.mapping['Mapping.Modify']; + + return {}; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + addMapping: function() { + let me = this; + let view = me.getView(); + Ext.create(view.editWindowClass, { + url: view.baseUrl, + autoShow: true, + listeners: { + destroy: () => me.load(), + }, + }); + }, + + add: function(_grid, _rI, _cI, _item, _e, rec) { + let me = this; + if (rec.data.type !== 'entry') { + return; + } + + me.openMapEditWindow(rec.data.name); + }, + + editDblClick: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (!selection || selection.length < 1) { + return; + } + + me.edit(selection[0]); + }, + + editAction: function(_grid, _rI, _cI, _item, _e, rec) { + this.edit(rec); + }, + + edit: function(rec) { + let me = this; + if (rec.data.type === 'map') { + return; + } + + me.openMapEditWindow(rec.data.name, rec.data.node, rec.data.type === 'entry'); + }, + + openMapEditWindow: function(name, nodename, entryOnly) { + let me = this; + let view = me.getView(); + + Ext.create(view.editWindowClass, { + url: `${view.baseUrl}/${name}`, + autoShow: true, + autoLoad: true, + entryOnly, + nodename, + name, + listeners: { + destroy: () => me.load(), + }, + }); + }, + + remove: function(_grid, _rI, _cI, _item, _e, rec) { + let me = this; + let msg, id; + let view = me.getView(); + let confirmMsg; + switch (rec.data.type) { + case 'entry': + msg = gettext("Are you sure you want to remove '{0}'"); + confirmMsg = Ext.String.format(msg, rec.data.name); + break; + case 'node': + msg = gettext("Are you sure you want to remove '{0}' entries for '{1}'"); + confirmMsg = Ext.String.format(msg, rec.data.node, rec.data.name); + break; + case 'map': + msg = gettext("Are you sure you want to remove '{0}' on '{1}' for '{2}'"); + id = rec.data[view.entryIdProperty]; + confirmMsg = Ext.String.format(msg, id, rec.data.node, rec.data.name); + break; + default: + throw "invalid type"; + } + Ext.Msg.confirm(gettext('Confirm'), confirmMsg, function(btn) { + if (btn === 'yes') { + me.executeRemove(rec.data); + } + }); + }, + + executeRemove: function(data) { + let me = this; + let view = me.getView(); + + let url = `${view.baseUrl}/${data.name}`; + let method = 'PUT'; + let params = { + digest: me.lookup[data.name].digest, + }; + let map = me.lookup[data.name].map; + switch (data.type) { + case 'entry': + method = 'DELETE'; + params = undefined; + break; + case 'node': + params.map = PVE.Parser.filterPropertyStringList(map, (e) => e.node !== data.node); + break; + case 'map': + params.map = PVE.Parser.filterPropertyStringList(map, (e) => + Object.entries(e).some(([key, value]) => data[key] !== value)); + break; + default: + throw "invalid type"; + } + if (!params?.map.length) { + method = 'DELETE'; + params = undefined; + } + Proxmox.Utils.API2Request({ + url, + method, + params, + success: function() { + me.load(); + }, + }); + }, + + load: function() { + let me = this; + let view = me.getView(); + Proxmox.Utils.API2Request({ + url: view.baseUrl, + method: 'GET', + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: function({ result: { data } }) { + let lookup = {}; + data.forEach((entry) => { + lookup[entry.id] = Ext.apply({}, entry); + entry.iconCls = 'fa fa-fw fa-folder-o'; + entry.name = entry.id; + entry.text = entry.id; + entry.type = 'entry'; + + let nodes = {}; + for (const map of entry.map) { + let parsed = PVE.Parser.parsePropertyString(map); + parsed.iconCls = view.mapIconCls; + parsed.leaf = true; + parsed.name = entry.id; + parsed.text = parsed[view.entryIdProperty]; + parsed.type = 'map'; + + if (nodes[parsed.node] === undefined) { + nodes[parsed.node] = { + children: [], + expanded: true, + iconCls: 'fa fa-fw fa-building-o', + leaf: false, + name: entry.id, + node: parsed.node, + text: parsed.node, + type: 'node', + }; + } + nodes[parsed.node].children.push(parsed); + } + delete entry.id; + entry.children = Object.values(nodes); + entry.leaf = entry.children.length === 0; + }); + me.lookup = lookup; + if (view.getStatusCheckUrl !== undefined && view.checkValidity !== undefined) { + me.loadStatusData(); + } + view.setRootNode({ + children: data, + }); + let root = view.getRootNode(); + root.expand(); + root.childNodes.forEach(node => node.expand()); + }, + }); + }, + + nodeLoadingState: {}, + + loadStatusData: function() { + let me = this; + let view = me.getView(); + PVE.data.ResourceStore.getNodes().forEach(({ node }) => { + me.nodeLoadingState[node] = true; + let url = view.getStatusCheckUrl(node); + Proxmox.Utils.API2Request({ + url, + method: 'GET', + failure: function(response) { + me.nodeLoadingState[node] = false; + view.getRootNode()?.cascade(function(rec) { + if (rec.data.node !== node) { + return; + } + + rec.set('valid', 0); + rec.set('errmsg', response.htmlStatus); + rec.commit(); + }); + }, + success: function({ result: { data } }) { + me.nodeLoadingState[node] = false; + view.checkValidity(data, node); + }, + }); + }); + }, + + renderStatus: function(value, _metadata, record) { + let me = this; + if (record.data.type !== 'map') { + return ''; + } + let iconCls; + let status; + if (value === undefined) { + if (me.nodeLoadingState[record.data.node]) { + iconCls = 'fa-spinner fa-spin'; + status = gettext('Loading...'); + } else { + iconCls = 'fa-question-circle'; + status = gettext('Unknown Node'); + } + } else { + let state = value ? 'good' : 'critical'; + iconCls = PVE.Utils.get_health_icon(state, true); + status = value ? gettext("Mapping matches host data") : record.data.errmsg || Proxmox.Utils.unknownText; + } + return ` ${status}`; + }, + + getAddClass: function(v, mD, rec) { + let cls = 'fa fa-plus-circle'; + if (rec.data.type !== 'entry' || rec.data.children?.length >= PVE.data.ResourceStore.getNodes().length) { + cls += ' pmx-action-hidden'; + } + return cls; + }, + + isAddDisabled: function(v, r, c, i, rec) { + return rec.data.type !== 'entry' || rec.data.children?.length >= PVE.data.ResourceStore.getNodes().length; + }, + + init: function(view) { + let me = this; + + ['editWindowClass', 'baseUrl', 'mapIconCls', 'entryIdProperty'].forEach((property) => { + if (view[property] === undefined) { + throw `No ${property} defined`; + } + }); + + me.load(); + }, + }, + + store: { + sorters: 'text', + data: {}, + }, + + + tbar: [ + { + text: gettext('Add'), + handler: 'addMapping', + cbind: { + disabled: '{!canConfigure}', + }, + }, + ], + + listeners: { + itemdblclick: 'editDblClick', + }, + + initComponent: function() { + let me = this; + + let columns = [...me.columns]; + columns.splice(1, 0, { + xtype: 'actioncolumn', + text: gettext('Actions'), + width: 80, + items: [ + { + getTip: (v, m, { data }) => + Ext.String.format(gettext("Add new host mapping for '{0}'"), data.name), + getClass: 'getAddClass', + isActionDisabled: 'isAddDisabled', + handler: 'add', + }, + { + iconCls: 'fa fa-pencil', + getTip: (v, m, { data }) => data.type === 'entry' + ? Ext.String.format(gettext("Edit Mapping '{0}'"), data.name) + : Ext.String.format(gettext("Edit Mapping '{0}' for '{1}'"), data.name, data.node), + getClass: (v, m, { data }) => data.type !== 'map' ? 'fa fa-pencil' : 'pmx-hidden', + isActionDisabled: (v, r, c, i, rec) => rec.data.type === 'map', + handler: 'editAction', + }, + { + iconCls: 'fa fa-trash-o', + getTip: (v, m, { data }) => data.type === 'entry' + ? Ext.String.format(gettext("Remove '{0}'"), data.name) + : data.type === 'node' + ? Ext.String.format(gettext("Remove mapping for '{0}'"), data.node) + : Ext.String.format(gettext("Remove mapping '{0}'"), data.path), + handler: 'remove', + }, + ], + }); + me.columns = columns; + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.DhcpTree', { + extend: 'Ext.tree.Panel', + xtype: 'pveDhcpTree', + + layout: 'fit', + rootVisible: false, + animate: false, + + store: { + sorters: ['ip', 'name'], + }, + + controller: { + xclass: 'Ext.app.ViewController', + + reload: function() { + let me = this; + + Proxmox.Utils.API2Request({ + url: `/cluster/sdn/ipams/pve/status`, + method: 'GET', + success: function(response, opts) { + let root = { + name: '__root', + expanded: true, + children: [], + }; + + let zones = {}; + let vnets = {}; + let subnets = {}; + + response.result.data.forEach((element) => { + element.leaf = true; + + if (!(element.zone in zones)) { + let zone = { + name: element.zone, + type: 'zone', + iconCls: 'fa fa-th', + expanded: true, + children: [], + }; + + zones[element.zone] = zone; + root.children.push(zone); + } + + if (!(element.vnet in vnets)) { + let vnet = { + name: element.vnet, + zone: element.zone, + type: 'vnet', + iconCls: 'fa fa-network-wired x-fa-treepanel', + expanded: true, + children: [], + }; + + vnets[element.vnet] = vnet; + zones[element.zone].children.push(vnet); + } + + if (!(element.subnet in subnets)) { + let subnet = { + name: element.subnet, + zone: element.zone, + vnet: element.vnet, + type: 'subnet', + iconCls: 'x-tree-icon-none', + expanded: true, + children: [], + }; + + subnets[element.subnet] = subnet; + vnets[element.vnet].children.push(subnet); + } + + element.type = 'mapping'; + element.iconCls = 'x-tree-icon-none'; + subnets[element.subnet].children.push(element); + }); + + me.getView().setRootNode(root); + }, + }); + }, + + init: function(view) { + let me = this; + me.reload(); + }, + + onDelete: function(table, rI, cI, item, e, { data }) { + let me = this; + let view = me.getView(); + + Ext.Msg.show({ + title: gettext('Confirm'), + icon: Ext.Msg.WARNING, + message: Ext.String.format(gettext('Are you sure you want to remove DHCP mapping {0}'), `${data.mac} / ${data.ip}`), + buttons: Ext.Msg.YESNO, + defaultFocus: 'no', + callback: function(btn) { + if (btn !== 'yes') { + return; + } + + let params = { + zone: data.zone, + mac: data.mac, + ip: data.ip, + }; + + let encodedParams = Ext.Object.toQueryString(params); + + let url = `/cluster/sdn/vnets/${data.vnet}/ips?${encodedParams}`; + + Proxmox.Utils.API2Request({ + url, + method: 'DELETE', + waitMsgTarget: view, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: me.reload.bind(me), + }); + }, + }); + }, + + editAction: function(_grid, _rI, _cI, _item, _e, rec) { + this.edit(rec); + }, + + editDblClick: function() { + let me = this; + + let view = me.getView(); + let selection = view.getSelection(); + + if (!selection || selection.length < 1) { + return; + } + + me.edit(selection[0]); + }, + + edit: function(rec) { + let me = this; + + if (rec.data.type === 'mapping' && !rec.data.gateway) { + me.openEditWindow(rec.data); + } + }, + + openEditWindow: function(data) { + let me = this; + + Ext.create('PVE.sdn.IpamEdit', { + autoShow: true, + mapping: data, + extraRequestParams: { + vmid: data.vmid, + mac: data.mac, + zone: data.zone, + vnet: data.vnet, + }, + listeners: { + destroy: () => me.reload(), + }, + }); + }, + }, + + listeners: { + itemdblclick: 'editDblClick', + }, + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Reload'), + handler: 'reload', + }, + ], + + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name / VMID'), + dataIndex: 'name', + width: 200, + renderer: function(value, meta, record) { + if (record.get('gateway')) { + return gettext('Gateway'); + } + + return record.get('name') ?? record.get('vmid') ?? ' '; + }, + }, + { + text: gettext('IP Address'), + dataIndex: 'ip', + width: 200, + }, + { + text: 'MAC', + dataIndex: 'mac', + width: 200, + }, + { + text: gettext('Gateway'), + dataIndex: 'gateway', + width: 200, + }, + { + header: gettext('Actions'), + xtype: 'actioncolumn', + dataIndex: 'text', + width: 150, + items: [ + { + handler: function(table, rI, cI, item, e, { data }) { + let me = this; + + Ext.create('PVE.sdn.IpamEdit', { + autoShow: true, + mapping: {}, + isCreate: true, + extraRequestParams: { + vnet: data.name, + zone: data.zone, + }, + listeners: { + destroy: () => { + me.up('pveDhcpTree').controller.reload(); + }, + }, + }); + }, + getTip: (v, m, rec) => gettext('Add'), + getClass: (v, m, { data }) => { + if (data.type === 'vnet') { + return 'fa fa-plus-square'; + } + + return 'pmx-hidden'; + }, + }, + { + handler: 'editAction', + getTip: (v, m, rec) => gettext('Edit'), + getClass: (v, m, { data }) => { + if (data.type === 'mapping' && !data.gateway) { + return 'fa fa-pencil fa-fw'; + } + + return 'pmx-hidden'; + }, + }, + { + handler: 'onDelete', + getTip: (v, m, rec) => gettext('Delete'), + getClass: (v, m, { data }) => { + if (data.type === 'mapping' && !data.gateway) { + return 'fa critical fa-trash-o'; + } + + return 'pmx-hidden'; + }, + }, + ], + }, + ], +}); Ext.define('PVE.window.Backup', { extend: 'Ext.window.Window', @@ -14970,7 +16729,7 @@ Ext.define('PVE.window.Backup', { throw "no VM type specified"; } - let compressionSelector = Ext.create('PVE.form.CompressionSelector', { + let compressionSelector = Ext.create('PVE.form.BackupCompressionSelector', { name: 'compress', value: 'zstd', fieldLabel: gettext('Compression'), @@ -14988,6 +16747,23 @@ Ext.define('PVE.window.Backup', { emptyText: Proxmox.Utils.noneText, }); + let notificationModeSelector = Ext.create({ + xtype: 'proxmoxKVComboBox', + comboItems: [ + ['auto', gettext('Auto')], + ['legacy-sendmail', gettext('Email (legacy)')], + ['notification-system', gettext('Notification system')], + ], + fieldLabel: gettext('Notification mode'), + name: 'notification-mode', + value: 'auto', + listeners: { + change: function(field, value) { + mailtoField.setDisabled(value === 'notification-system'); + }, + }, + }); + const keepNames = [ ['keep-last', gettext('Keep Last')], ['keep-hourly', gettext('Keep Hourly')], @@ -15062,6 +16838,9 @@ Ext.define('PVE.window.Backup', { if (!initialDefaults && data.mailto !== undefined) { mailtoField.setValue(data.mailto); } + if (!initialDefaults && data['notification-mode'] !== undefined) { + notificationModeSelector.setValue(data['notification-mode']); + } if (!initialDefaults && data.mode !== undefined) { modeSelector.setValue(data.mode); } @@ -15128,6 +16907,7 @@ Ext.define('PVE.window.Backup', { ], column2: [ compressionSelector, + notificationModeSelector, mailtoField, removeCheckbox, ], @@ -15208,6 +16988,10 @@ Ext.define('PVE.window.Backup', { params.mailto = values.mailto; } + if (values['notification-mode']) { + params['notification-mode'] = values['notification-mode']; + } + if (values.compress) { params.compress = values.compress; } @@ -15340,7 +17124,7 @@ Ext.define('PVE.window.BulkAction', { }, border: false, - // the action to set, currently there are: `startall`, `migrateall`, `stopall` + // the action to set, currently there are: `startall`, `migrateall`, `stopall`, `suspendall` action: undefined, submit: function(params) { @@ -15385,51 +17169,52 @@ Ext.define('PVE.window.BulkAction', { if (me.action === 'migrateall') { items.push( { - xtype: 'pveNodeSelector', - name: 'target', - disallowedNodes: [me.nodename], - fieldLabel: gettext('Target node'), - allowBlank: false, - onlineValidator: true, - }, - { - xtype: 'proxmoxintegerfield', - name: 'maxworkers', - minValue: 1, - maxValue: 100, - value: 1, - fieldLabel: gettext('Parallel jobs'), - allowBlank: false, + xtype: 'fieldcontainer', + layout: 'hbox', + items: [{ + flex: 1, + xtype: 'pveNodeSelector', + name: 'target', + disallowedNodes: [me.nodename], + fieldLabel: gettext('Target node'), + labelWidth: 200, + allowBlank: false, + onlineValidator: true, + padding: '0 10 0 0', + }, + { + xtype: 'proxmoxintegerfield', + name: 'maxworkers', + minValue: 1, + maxValue: 100, + value: 1, + fieldLabel: gettext('Parallel jobs'), + allowBlank: false, + flex: 1, + }], }, { xtype: 'fieldcontainer', - fieldLabel: gettext('Allow local disk migration'), layout: 'hbox', items: [{ xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Allow local disk migration'), name: 'with-local-disks', + labelWidth: 200, checked: true, uncheckedValue: 0, - listeners: { - change: (cb, val) => me.down('#localdiskwarning').setVisible(val), - }, + flex: 1, + padding: '0 10 0 0', }, { - itemId: 'localdiskwarning', + itemId: 'lxcwarning', xtype: 'displayfield', - flex: 1, - padding: '0 0 0 10', userCls: 'pmx-hint', - value: 'Note: Migration with local disks might take long.', + value: 'Warning: Running CTs will be migrated in Restart Mode.', + hidden: true, // only visible if running container chosen + flex: 1, }], }, - { - itemId: 'lxcwarning', - xtype: 'displayfield', - userCls: 'pmx-hint', - value: 'Warning: Running CTs will be migrated in Restart Mode.', - hidden: true, // only visible if running container chosen - }, ); } else if (me.action === 'startall') { items.push({ @@ -15438,27 +17223,327 @@ Ext.define('PVE.window.BulkAction', { value: 1, }); } else if (me.action === 'stopall') { - items.push( - { + items.push({ + xtype: 'fieldcontainer', + layout: 'hbox', + items: [{ xtype: 'proxmoxcheckbox', name: 'force-stop', + labelWidth: 120, fieldLabel: gettext('Force Stop'), boxLabel: gettext('Force stop guest if shutdown times out.'), checked: true, uncheckedValue: 0, + flex: 1, }, { xtype: 'proxmoxintegerfield', name: 'timeout', fieldLabel: gettext('Timeout (s)'), + labelWidth: 120, emptyText: '180', minValue: 0, maxValue: 7200, allowBlank: true, - }, - ); + flex: 1, + }], + }); } + let refreshLxcWarning = function(vmids, records) { + let showWarning = records.some( + item => vmids.includes(item.data.vmid) && item.data.type === 'lxc' && item.data.status === 'running', + ); + me.down('#lxcwarning').setVisible(showWarning); + }; + + let defaultStatus = me.action === 'migrateall' ? '' : me.action === 'startall' ? 'stopped' : 'running'; + let defaultType = me.action === 'suspendall' ? 'qemu' : ''; + + let statusMap = []; + let poolMap = []; + let haMap = []; + let tagMap = []; + PVE.data.ResourceStore.each((rec) => { + if (['qemu', 'lxc'].indexOf(rec.data.type) !== -1) { + statusMap[rec.data.status] = true; + } + if (rec.data.type === 'pool') { + poolMap[rec.data.pool] = true; + } + if (rec.data.hastate !== "") { + haMap[rec.data.hastate] = true; + } + if (rec.data.tags !== "") { + rec.data.tags.split(/[,; ]/).forEach((tag) => { + if (tag !== '') { + tagMap[tag] = true; + } + }); + } + }); + + let statusList = Object.keys(statusMap).map(key => [key, key]); + statusList.unshift(['', gettext('All')]); + let poolList = Object.keys(poolMap).map(key => [key, key]); + let tagList = Object.keys(tagMap).map(key => ({ value: key })); + let haList = Object.keys(haMap).map(key => [key, key]); + + let clearFilters = function() { + me.down('#namefilter').setValue(''); + ['name', 'status', 'pool', 'type', 'hastate', 'includetag', 'excludetag', 'vmid'].forEach((filter) => { + me.down(`#${filter}filter`).setValue(''); + }); + }; + + let filterChange = function() { + let nameValue = me.down('#namefilter').getValue(); + let filterCount = 0; + + if (nameValue !== '') { + filterCount++; + } + + let arrayFiltersData = []; + ['pool', 'hastate'].forEach((filter) => { + let selected = me.down(`#${filter}filter`).getValue() ?? []; + if (selected.length) { + filterCount++; + arrayFiltersData.push([filter, [...selected]]); + } + }); + + let singleFiltersData = []; + ['status', 'type'].forEach((filter) => { + let selected = me.down(`#${filter}filter`).getValue() ?? ''; + if (selected.length) { + filterCount++; + singleFiltersData.push([filter, selected]); + } + }); + + let includeTags = me.down('#includetagfilter').getValue() ?? []; + if (includeTags.length) { + filterCount++; + } + let excludeTags = me.down('#excludetagfilter').getValue() ?? []; + if (excludeTags.length) { + filterCount++; + } + + let fieldSet = me.down('#filters'); + let clearBtn = me.down('#clearBtn'); + if (filterCount) { + fieldSet.setTitle(Ext.String.format(gettext('Filters ({0})'), filterCount)); + clearBtn.setDisabled(false); + } else { + fieldSet.setTitle(gettext('Filters')); + clearBtn.setDisabled(true); + } + + let filterFn = function(value) { + let name = value.data.name.toLowerCase().indexOf(nameValue.toLowerCase()) !== -1; + let arrayFilters = arrayFiltersData.every(([filter, selected]) => + !selected.length || selected.indexOf(value.data[filter]) !== -1); + let singleFilters = singleFiltersData.every(([filter, selected]) => + !selected.length || value.data[filter].indexOf(selected) !== -1); + let tags = value.data.tags.split(/[;, ]/).filter(t => !!t); + let includeFilter = !includeTags.length || tags.some(tag => includeTags.indexOf(tag) !== -1); + let excludeFilter = !excludeTags.length || tags.every(tag => excludeTags.indexOf(tag) === -1); + + return name && arrayFilters && singleFilters && includeFilter && excludeFilter; + }; + let vmselector = me.down('#vms'); + vmselector.getStore().setFilters({ + id: 'customFilter', + filterFn, + }); + vmselector.checkChange(); + if (me.action === 'migrateall') { + let records = vmselector.getSelection(); + refreshLxcWarning(vmselector.getValue(), records); + } + }; + + items.push({ + xtype: 'fieldset', + itemId: 'filters', + collapsible: true, + title: gettext('Filters'), + layout: 'hbox', + items: [ + { + xtype: 'container', + flex: 1, + padding: 5, + layout: { + type: 'vbox', + align: 'stretch', + }, + defaults: { + listeners: { + change: filterChange, + }, + isFormField: false, + }, + items: [ + { + fieldLabel: gettext("Name"), + itemId: 'namefilter', + xtype: 'textfield', + }, + { + xtype: 'combobox', + itemId: 'statusfilter', + fieldLabel: gettext("Status"), + emptyText: gettext('All'), + editable: false, + value: defaultStatus, + store: statusList, + }, + { + xtype: 'combobox', + itemId: 'poolfilter', + fieldLabel: gettext("Pool"), + emptyText: gettext('All'), + editable: false, + multiSelect: true, + store: poolList, + }, + ], + }, + { + xtype: 'container', + layout: { + type: 'vbox', + align: 'stretch', + }, + flex: 1, + padding: 5, + defaults: { + listeners: { + change: filterChange, + }, + isFormField: false, + }, + items: [ + { + xtype: 'combobox', + itemId: 'typefilter', + fieldLabel: gettext("Type"), + emptyText: gettext('All'), + editable: false, + value: defaultType, + store: [ + ['', gettext('All')], + ['lxc', gettext('CT')], + ['qemu', gettext('VM')], + ], + }, + { + xtype: 'proxmoxComboGrid', + itemId: 'includetagfilter', + fieldLabel: gettext("Include Tags"), + emptyText: gettext('All'), + editable: false, + multiSelect: true, + valueField: 'value', + displayField: 'value', + listConfig: { + userCls: 'proxmox-tags-full', + columns: [ + { + dataIndex: 'value', + flex: 1, + renderer: value => + PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides), + }, + ], + }, + store: { + data: tagList, + }, + listeners: { + change: filterChange, + }, + }, + { + xtype: 'proxmoxComboGrid', + itemId: 'excludetagfilter', + fieldLabel: gettext("Exclude Tags"), + emptyText: gettext('None'), + multiSelect: true, + editable: false, + valueField: 'value', + displayField: 'value', + listConfig: { + userCls: 'proxmox-tags-full', + columns: [ + { + dataIndex: 'value', + flex: 1, + renderer: value => + PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides), + }, + ], + }, + store: { + data: tagList, + }, + listeners: { + change: filterChange, + }, + }, + ], + }, + { + xtype: 'container', + layout: { + type: 'vbox', + align: 'stretch', + }, + flex: 1, + padding: 5, + defaults: { + listeners: { + change: filterChange, + }, + isFormField: false, + }, + items: [ + { + xtype: 'combobox', + itemId: 'hastatefilter', + fieldLabel: gettext("HA status"), + emptyText: gettext('All'), + multiSelect: true, + editable: false, + store: haList, + listeners: { + change: filterChange, + }, + }, + { + xtype: 'container', + layout: { + type: 'vbox', + align: 'end', + }, + items: [ + { + xtype: 'button', + itemId: 'clearBtn', + text: gettext('Clear Filters'), + disabled: true, + handler: clearFilters, + }, + ], + }, + ], + }, + ], + }); + items.push({ xtype: 'vmselector', itemId: 'vms', @@ -15467,15 +17552,13 @@ Ext.define('PVE.window.BulkAction', { height: 300, selectAll: true, allowBlank: false, + plugins: '', nodename: me.nodename, - action: me.action, listeners: { selectionchange: function(vmselector, records) { if (me.action === 'migrateall') { - let showWarning = records.some( - item => item.data.type === 'lxc' && item.data.status === 'running', - ); - me.down('#lxcwarning').setVisible(showWarning); + let vmids = me.down('#vms').getValue(); + refreshLxcWarning(vmids, records); } }, }, @@ -15489,7 +17572,6 @@ Ext.define('PVE.window.BulkAction', { align: 'stretch', }, fieldDefaults: { - labelWidth: me.action === 'migrateall' ? 300 : 120, anchor: '100%', }, items: items, @@ -15517,6 +17599,8 @@ Ext.define('PVE.window.BulkAction', { submitBtn.setDisabled(!valid); }); form.isValid(); + + filterChange(); }, }); Ext.define('PVE.ceph.Install', { @@ -15772,7 +17856,7 @@ Ext.define('PVE.window.Clone', { onlineValidator: true, listeners: { change: function(f, value) { - me.lookupReference('hdstorage').setTargetNode(value); + me.lookup('diskselector').getComponent('hdstorage').setTargetNode(value); }, }, }); @@ -15793,6 +17877,7 @@ Ext.define('PVE.window.Clone', { { xtype: 'textfield', name: 'name', + vtype: 'DnsName', allowBlank: true, fieldLabel: me.guestType === 'lxc' ? gettext('Hostname') : gettext('Name'), }, @@ -16616,7 +18701,7 @@ Ext.define('PVE.window.Migrate', { }); }, - checkMigratePreconditions: function(resetMigrationPossible) { + checkMigratePreconditions: async function(resetMigrationPossible) { var me = this, vm = me.getViewModel(); @@ -16626,12 +18711,13 @@ Ext.define('PVE.window.Migrate', { vm.set('running', true); } + me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')]; + if (vm.get('vmtype') === 'qemu') { - me.checkQemuPreconditions(resetMigrationPossible); + await me.checkQemuPreconditions(resetMigrationPossible); } else { me.checkLxcPreconditions(resetMigrationPossible); } - me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')]; // Only allow nodes where the local storage is available in case of offline migration // where storage migration is not possible @@ -16679,37 +18765,69 @@ Ext.define('PVE.window.Migrate', { migration.allowedNodes = migrateStats.allowed_nodes; let target = me.lookup('pveNodeSelector').value; if (target.length && !migrateStats.allowed_nodes.includes(target)) { - let disallowed = migrateStats.not_allowed_nodes[target]; - let missingStorages = disallowed.unavailable_storages.join(', '); + let disallowed = migrateStats.not_allowed_nodes[target] ?? {}; + if (disallowed.unavailable_storages !== undefined) { + let missingStorages = disallowed.unavailable_storages.join(', '); - migration.possible = false; - migration.preconditions.push({ - text: 'Storage (' + missingStorages + ') not available on selected target. ' + - 'Start VM to use live storage migration or select other target node', - severity: 'error', - }); + migration.possible = false; + migration.preconditions.push({ + text: 'Storage (' + missingStorages + ') not available on selected target. ' + + 'Start VM to use live storage migration or select other target node', + severity: 'error', + }); + } + + if (disallowed['unavailable-resources'] !== undefined) { + let unavailableResources = disallowed['unavailable-resources'].join(', '); + + migration.possible = false; + migration.preconditions.push({ + text: 'Mapped Resources (' + unavailableResources + ') not available on selected target. ', + severity: 'error', + }); + } + } + } + + let blockingResources = []; + let mappedResources = migrateStats['mapped-resources'] ?? []; + + for (const res of migrateStats.local_resources) { + if (mappedResources.indexOf(res) === -1) { + blockingResources.push(res); } } - if (migrateStats.local_resources.length) { + if (blockingResources.length) { migration.hasLocalResources = true; if (!migration.overwriteLocalResourceCheck || vm.get('running')) { migration.possible = false; migration.preconditions.push({ text: Ext.String.format('Can\'t migrate VM with local resources: {0}', - migrateStats.local_resources.join(', ')), + blockingResources.join(', ')), severity: 'error', }); } else { migration.preconditions.push({ text: Ext.String.format('Migrate VM with local resources: {0}. ' + 'This might fail if resources aren\'t available on the target node.', - migrateStats.local_resources.join(', ')), + blockingResources.join(', ')), severity: 'warning', }); } } + if (mappedResources && mappedResources.length) { + if (vm.get('running')) { + migration.possible = false; + migration.preconditions.push({ + text: Ext.String.format('Can\'t migrate running VM with mapped resources: {0}', + mappedResources.join(', ')), + severity: 'error', + }); + } + } + if (migrateStats.local_disks.length) { migrateStats.local_disks.forEach(function(disk) { if (disk.cdrom && disk.cdrom === 1) { @@ -17457,6 +19575,7 @@ Ext.define('PVE.window.Restore', { xtype: 'textfield', fieldLabel: me.vmtype === 'lxc' ? gettext('Hostname') : gettext('Name'), name: 'name', + vtype: 'DnsName', reference: 'nameField', allowBlank: true, }, { @@ -18369,8 +20488,20 @@ Ext.define('PVE.window.DownloadUrlToStorage', { urlField.validate(); let data = res.result.data; + + let filename = data.filename || ""; + let compression = '__default__'; + if (view.content === 'iso') { + const matches = filename.match(/^(.+)\.(gz|lzo|zst)$/i); + if (matches) { + filename = matches[1]; + compression = matches[2].toLowerCase(); + } + } + view.setValues({ - filename: data.filename || "", + filename, + compression, size: (data.size && Proxmox.Utils.format_size(data.size)) || gettext("Unknown"), mimetype: data.mimetype || gettext("Unknown"), }); @@ -18492,6 +20623,24 @@ Ext.define('PVE.window.DownloadUrlToStorage', { change: 'setQueryEnabled', }, }, + { + xtype: 'proxmoxKVComboBox', + name: 'compression', + fieldLabel: gettext('Decompression algorithm'), + allowBlank: true, + hasNoneOption: true, + deleteEmpty: false, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.NoneText], + ['lzo', 'LZO'], + ['gz', 'GZIP'], + ['zst', 'ZSTD'], + ], + cbind: { + hidden: get => get('content') !== 'iso', + }, + }, ], }, { @@ -18504,7 +20653,7 @@ Ext.define('PVE.window.DownloadUrlToStorage', { ], initComponent: function() { - var me = this; + var me = this; if (!me.nodename) { throw "no node name specified"; @@ -18512,8 +20661,7 @@ Ext.define('PVE.window.DownloadUrlToStorage', { if (!me.storage) { throw "no storage ID specified"; } - - me.callParent(); + me.callParent(); }, }); @@ -18973,7 +21121,7 @@ Ext.define('PVE.window.Wizard', { activeTitle: '', // used for automated testing width: 720, - height: 510, + height: 540, modal: true, border: false, @@ -19555,6 +21703,531 @@ Ext.define('PVE.window.TreeSettingsEdit', { }, }); +Ext.define('PVE.window.PCIMapEditWindow', { + extend: 'Proxmox.window.Edit', + + mixins: ['Proxmox.Mixin.CBind'], + + width: 800, + + subject: gettext('PCI mapping'), + + onlineHelp: 'resource_mapping', + + method: 'POST', + + cbindData: function(initialConfig) { + let me = this; + me.isCreate = (!me.name || !me.nodename) && !me.entryOnly; + me.method = me.name ? 'PUT' : 'POST'; + me.hideMapping = !!me.entryOnly; + me.hideComment = me.name && !me.entryOnly; + me.hideNodeSelector = me.nodename || me.entryOnly; + me.hideNode = !me.nodename || !me.hideNodeSelector; + return { + name: me.name, + nodename: me.nodename, + }; + }, + + submitUrl: function(_url, data) { + let me = this; + let name = me.method === 'PUT' ? me.name : ''; + return `/cluster/mapping/pci/${name}`; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + onGetValues: function(values) { + let me = this; + let view = me.getView(); + if (view.method === "POST") { + delete me.digest; + } + + if (values.iommugroup === -1) { + delete values.iommugroup; + } + + let nodename = values.node ?? view.nodename; + delete values.node; + if (me.originalMap) { + let otherMaps = PVE.Parser + .filterPropertyStringList(me.originalMap, (e) => e.node !== nodename); + if (otherMaps.length) { + values.map = values.map.concat(otherMaps); + } + } + + return values; + }, + + onSetValues: function(values) { + let me = this; + let view = me.getView(); + me.originalMap = [...values.map]; + let configuredNodes = []; + values.map = PVE.Parser.filterPropertyStringList(values.map, (e) => { + configuredNodes.push(e.node); + return e.node === view.nodename; + }); + + me.lookup('nodeselector').disallowedNodes = configuredNodes; + return values; + }, + + checkIommu: function(store, records, success) { + let me = this; + if (!success || !records.length) { + return; + } + me.lookup('iommu_warning').setVisible( + records.every((val) => val.data.iommugroup === -1), + ); + + let value = me.lookup('pciselector').getValue(); + me.checkIsolated(value); + }, + + checkIsolated: function(value) { + let me = this; + + let store = me.lookup('pciselector').getStore(); + + let isIsolated = function(entry) { + let isolated = true; + let parsed = PVE.Parser.parsePropertyString(entry); + parsed.iommugroup = parseInt(parsed.iommugroup, 10); + if (!parsed.iommugroup) { + return isolated; + } + store.each(({ data }) => { + let isSubDevice = data.id.startsWith(parsed.path); + if (data.iommugroup === parsed.iommugroup && data.id !== parsed.path && !isSubDevice) { + isolated = false; + return false; + } + return true; + }); + return isolated; + }; + + let showWarning = false; + if (Ext.isArray(value)) { + for (const entry of value) { + if (!isIsolated(entry)) { + showWarning = true; + break; + } + } + } else { + showWarning = isIsolated(value); + } + me.lookup('group_warning').setVisible(showWarning); + }, + + mdevChange: function(mdevField, value) { + this.lookup('pciselector').setMdev(value); + }, + + nodeChange: function(_field, value) { + this.lookup('pciselector').setNodename(value); + }, + + pciChange: function(_field, value) { + let me = this; + me.lookup('multiple_warning').setVisible(Ext.isArray(value) && value.length > 1); + me.checkIsolated(value); + }, + + control: { + 'field[name=mdev]': { + change: 'mdevChange', + }, + 'pveNodeSelector': { + change: 'nodeChange', + }, + 'pveMultiPCISelector': { + change: 'pciChange', + }, + }, + }, + + items: [ + { + xtype: 'inputpanel', + onGetValues: function(values) { + return this.up('window').getController().onGetValues(values); + }, + + onSetValues: function(values) { + return this.up('window').getController().onSetValues(values); + }, + + columnT: [ + { + xtype: 'displayfield', + reference: 'iommu_warning', + hidden: true, + columnWidth: 1, + padding: '0 0 10 0', + value: gettext('No IOMMU detected, please activate it. See Documentation for further information.'), + userCls: 'pmx-hint', + }, + { + xtype: 'displayfield', + reference: 'multiple_warning', + hidden: true, + columnWidth: 1, + padding: '0 0 10 0', + value: gettext('When multiple devices are selected, the first free one will be chosen on guest start.'), + userCls: 'pmx-hint', + }, + { + xtype: 'displayfield', + reference: 'group_warning', + hidden: true, + columnWidth: 1, + padding: '0 0 10 0', + itemId: 'iommuwarning', + value: gettext('A selected device is not in a separate IOMMU group, make sure this is intended.'), + userCls: 'pmx-hint', + }, + ], + + column1: [ + { + xtype: 'pmxDisplayEditField', + fieldLabel: gettext('Name'), + labelWidth: 120, + cbind: { + editable: '{!name}', + value: '{name}', + submitValue: '{isCreate}', + }, + name: 'id', + allowBlank: false, + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Mapping on Node'), + labelWidth: 120, + name: 'node', + cbind: { + value: '{nodename}', + disabled: '{hideNode}', + hidden: '{hideNode}', + }, + allowBlank: false, + }, + { + xtype: 'pveNodeSelector', + reference: 'nodeselector', + fieldLabel: gettext('Mapping on Node'), + labelWidth: 120, + name: 'node', + cbind: { + disabled: '{hideNodeSelector}', + hidden: '{hideNodeSelector}', + }, + allowBlank: false, + }, + ], + + column2: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Use with Mediated Devices'), + labelWidth: 200, + reference: 'mdev', + name: 'mdev', + cbind: { + deleteEmpty: '{!isCreate}', + disabled: '{hideComment}', + }, + }, + ], + + columnB: [ + { + xtype: 'pveMultiPCISelector', + fieldLabel: gettext('Device'), + labelWidth: 120, + height: 300, + reference: 'pciselector', + name: 'map', + cbind: { + nodename: '{nodename}', + disabled: '{hideMapping}', + hidden: '{hideMapping}', + }, + allowBlank: false, + onLoadCallBack: 'checkIommu', + margin: '0 0 10 0', + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Comment'), + labelWidth: 120, + submitValue: true, + name: 'description', + cbind: { + deleteEmpty: '{!isCreate}', + disabled: '{hideComment}', + hidden: '{hideComment}', + }, + }, + ], + }, + ], +}); +Ext.define('PVE.window.USBMapEditWindow', { + extend: 'Proxmox.window.Edit', + + mixins: ['Proxmox.Mixin.CBind'], + + cbindData: function(initialConfig) { + let me = this; + me.isCreate = !me.name; + me.method = me.isCreate ? 'POST' : 'PUT'; + me.hideMapping = !!me.entryOnly; + me.hideComment = me.name && !me.entryOnly; + me.hideNodeSelector = me.nodename || me.entryOnly; + me.hideNode = !me.nodename || !me.hideNodeSelector; + return { + name: me.name, + nodename: me.nodename, + }; + }, + + submitUrl: function(_url, data) { + let me = this; + let name = me.isCreate ? '' : me.name; + return `/cluster/mapping/usb/${name}`; + }, + + title: gettext('Add USB mapping'), + + onlineHelp: 'resource_mapping', + + method: 'POST', + + controller: { + xclass: 'Ext.app.ViewController', + + onGetValues: function(values) { + let me = this; + let view = me.getView(); + values.node ??= view.nodename; + + let type = me.getView().down('radiofield').getGroupValue(); + let name = values.name; + let description = values.description; + delete values.description; + delete values.name; + + if (type === 'path') { + let usbsel = me.lookup(type); + let usbDev = usbsel.getStore().findRecord('usbid', values[type], 0, false, true, true); + + if (!usbDev) { + return {}; + } + values.id = `${usbDev.data.vendid}:${usbDev.data.prodid}`; + } + + let map = []; + if (me.originalMap) { + map = PVE.Parser.filterPropertyStringList(me.originalMap, (e) => e.node !== values.node); + } + if (values.id) { + map.push(PVE.Parser.printPropertyString(values)); + } + + values = { map }; + if (description) { + values.description = description; + } + + if (view.isCreate) { + values.id = name; + } + + return values; + }, + + onSetValues: function(values) { + let me = this; + let view = me.getView(); + me.originalMap = [...values.map]; + let configuredNodes = []; + PVE.Parser.filterPropertyStringList(values.map, (e) => { + configuredNodes.push(e.node); + if (e.node === view.nodename) { + values = e; + } + return false; + }); + + me.lookup('nodeselector').disallowedNodes = configuredNodes; + if (values.path) { + values.usb = 'path'; + } + + return values; + }, + + modeChange: function(field, value) { + let me = this; + let type = field.inputValue; + let usbsel = me.lookup(type); + usbsel.setDisabled(!value); + }, + + nodeChange: function(_field, value) { + this.lookup('id').setNodename(value); + this.lookup('path').setNodename(value); + }, + + + init: function(view) { + let me = this; + + if (!view.nodename) { + //throw "no nodename given"; + } + }, + + control: { + 'radiofield': { + change: 'modeChange', + }, + 'pveNodeSelector': { + change: 'nodeChange', + }, + }, + }, + + items: [ + { + xtype: 'inputpanel', + onGetValues: function(values) { + return this.up('window').getController().onGetValues(values); + }, + + onSetValues: function(values) { + return this.up('window').getController().onSetValues(values); + }, + + column1: [ + { + xtype: 'pmxDisplayEditField', + fieldLabel: gettext('Name'), + cbind: { + editable: '{!name}', + value: '{name}', + submitValue: '{isCreate}', + }, + name: 'name', + allowBlank: false, + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Mapping on Node'), + labelWidth: 120, + name: 'node', + cbind: { + value: '{nodename}', + disabled: '{hideNode}', + hidden: '{hideNode}', + }, + allowBlank: false, + }, + { + xtype: 'pveNodeSelector', + reference: 'nodeselector', + fieldLabel: gettext('Mapping on Node'), + labelWidth: 120, + name: 'node', + cbind: { + disabled: '{hideNodeSelector}', + hidden: '{hideNodeSelector}', + }, + allowBlank: false, + }, + ], + + column2: [ + { + xtype: 'fieldcontainer', + defaultType: 'radiofield', + layout: 'fit', + cbind: { + disabled: '{hideMapping}', + hidden: '{hideMapping}', + }, + items: [ + { + name: 'usb', + inputValue: 'id', + checked: true, + boxLabel: gettext('Use USB Vendor/Device ID'), + submitValue: false, + }, + { + xtype: 'pveUSBSelector', + type: 'device', + reference: 'id', + name: 'id', + cbind: { + nodename: '{nodename}', + disabled: '{hideMapping}', + }, + editable: true, + allowBlank: false, + fieldLabel: gettext('Choose Device'), + labelAlign: 'right', + }, + { + name: 'usb', + inputValue: 'path', + boxLabel: gettext('Use USB Port'), + submitValue: false, + }, + { + xtype: 'pveUSBSelector', + disabled: true, + name: 'path', + reference: 'path', + cbind: { + nodename: '{nodename}', + }, + editable: true, + type: 'port', + allowBlank: false, + fieldLabel: gettext('Choose Port'), + labelAlign: 'right', + }, + ], + }, + ], + + columnB: [ + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Comment'), + submitValue: true, + name: 'description', + cbind: { + disabled: '{hideComment}', + hidden: '{hideComment}', + }, + }, + ], + }, + ], +}); Ext.define('PVE.ha.FencingView', { extend: 'Ext.grid.GridPanel', alias: ['widget.pveFencingView'], @@ -19575,11 +22248,11 @@ Ext.define('PVE.ha.FencingView', { viewConfig: { trackOver: false, deferEmptyText: false, - emptyText: 'Use watchdog based fencing.', + emptyText: gettext('Use watchdog based fencing.'), }, columns: [ { - header: 'Node', + header: gettext('Node'), width: 100, sortable: true, dataIndex: 'node', @@ -19673,7 +22346,7 @@ Ext.define('PVE.ha.GroupInputPanel', { dataIndex: 'cpu', }, { - header: 'Priority', + header: gettext('Priority'), xtype: 'widgetcolumn', dataIndex: 'priority', sortable: true, @@ -19823,7 +22496,6 @@ Ext.define('PVE.ha.GroupSelector', { extend: 'Proxmox.form.ComboGrid', alias: ['widget.pveHAGroupSelector'], - value: [], autoSelect: false, valueField: 'group', displayField: 'group', @@ -21408,6 +24080,153 @@ Ext.define('PVE.panel.ADInputPanel', { emptyText: 'company.net', allowBlank: false, }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Case-Sensitive'), + name: 'case-sensitive', + uncheckedValue: 0, + checked: true, + }, + ]; + + me.column2 = [ + { + xtype: 'textfield', + fieldLabel: gettext('Server'), + name: 'server1', + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Fallback Server'), + deleteEmpty: !me.isCreate, + name: 'server2', + }, + { + xtype: 'proxmoxintegerfield', + name: 'port', + fieldLabel: gettext('Port'), + minValue: 1, + maxValue: 65535, + emptyText: gettext('Default'), + submitEmptyText: false, + }, + { + xtype: 'proxmoxKVComboBox', + name: 'mode', + fieldLabel: gettext('Mode'), + editable: false, + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (LDAP)'], + ['ldap', 'LDAP'], + ['ldap+starttls', 'STARTTLS'], + ['ldaps', 'LDAPS'], + ], + value: '__default__', + deleteEmpty: !me.isCreate, + listeners: { + change: function(field, newValue) { + let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]'); + if (newValue === 'ldap' || newValue === '__default__') { + verifyCheckbox.disable(); + verifyCheckbox.setValue(0); + } else { + verifyCheckbox.enable(); + } + }, + }, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Verify Certificate'), + name: 'verify', + uncheckedValue: 0, + disabled: true, + checked: false, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Verify TLS certificate of the server'), + }, + }, + ]; + + me.advancedItems = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Check connection'), + name: 'check-connection', + uncheckedValue: 0, + checked: true, + autoEl: { + tag: 'div', + 'data-qtip': + gettext('Verify connection parameters and bind credentials on save'), + }, + }, + ]; + + me.callParent(); + }, + onGetValues: function(values) { + let me = this; + + if (!values.verify) { + if (!me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'verify' }); + } + delete values.verify; + } + + if (!me.isCreate) { + // Delete old `secure` parameter. It has been deprecated in favor to the + // `mode` parameter. Migration happens automatically in `onSetValues`. + Proxmox.Utils.assemble_field_data(values, { 'delete': 'secure' }); + } + + + return me.callParent([values]); + }, + + onSetValues(values) { + let me = this; + + if (values.secure !== undefined && !values.mode) { + // If `secure` is set, use it to determine the correct setting for `mode` + // `secure` is later deleted by `onSetValues` . + // In case *both* are set, we simply ignore `secure` and use + // whatever `mode` is set to. + values.mode = values.secure ? 'ldaps' : 'ldap'; + } + + return me.callParent([values]); + }, +}); +Ext.define('PVE.panel.LDAPInputPanel', { + extend: 'PVE.panel.AuthBase', + xtype: 'pveAuthLDAPPanel', + + initComponent: function() { + let me = this; + + if (me.type !== 'ldap') { + throw 'invalid type'; + } + + me.column1 = [ + { + xtype: 'textfield', + name: 'base_dn', + fieldLabel: gettext('Base Domain Name'), + emptyText: 'CN=Users,DC=Company,DC=net', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'user_attr', + emptyText: 'uid / sAMAccountName', + fieldLabel: gettext('User Attribute Name'), + allowBlank: false, + }, ]; me.column2 = [ @@ -21433,18 +24252,26 @@ Ext.define('PVE.panel.ADInputPanel', { submitEmptyText: false, }, { - xtype: 'proxmoxcheckbox', - fieldLabel: 'SSL', - name: 'secure', - uncheckedValue: 0, + xtype: 'proxmoxKVComboBox', + name: 'mode', + fieldLabel: gettext('Mode'), + editable: false, + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (LDAP)'], + ['ldap', 'LDAP'], + ['ldap+starttls', 'STARTTLS'], + ['ldaps', 'LDAPS'], + ], + value: '__default__', + deleteEmpty: !me.isCreate, listeners: { change: function(field, newValue) { let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]'); - if (newValue === true) { - verifyCheckbox.enable(); - } else { + if (newValue === 'ldap' || newValue === '__default__') { verifyCheckbox.disable(); verifyCheckbox.setValue(0); + } else { + verifyCheckbox.enable(); } }, }, @@ -21453,108 +24280,27 @@ Ext.define('PVE.panel.ADInputPanel', { xtype: 'proxmoxcheckbox', fieldLabel: gettext('Verify Certificate'), name: 'verify', - unceckedValue: 0, + uncheckedValue: 0, disabled: true, checked: false, autoEl: { tag: 'div', - 'data-qtip': gettext('Verify SSL certificate of the server'), + 'data-qtip': gettext('Verify TLS certificate of the server'), }, }, ]; - me.callParent(); - }, - onGetValues: function(values) { - let me = this; - - if (!values.verify) { - if (!me.isCreate) { - Proxmox.Utils.assemble_field_data(values, { 'delete': 'verify' }); - } - delete values.verify; - } - - return me.callParent([values]); - }, -}); -Ext.define('PVE.panel.LDAPInputPanel', { - extend: 'PVE.panel.AuthBase', - xtype: 'pveAuthLDAPPanel', - - initComponent: function() { - let me = this; - - if (me.type !== 'ldap') { - throw 'invalid type'; - } - - me.column1 = [ - { - xtype: 'textfield', - name: 'base_dn', - fieldLabel: gettext('Base Domain Name'), - emptyText: 'CN=Users,DC=Company,DC=net', - allowBlank: false, - }, - { - xtype: 'textfield', - name: 'user_attr', - emptyText: 'uid / sAMAccountName', - fieldLabel: gettext('User Attribute Name'), - allowBlank: false, - }, - ]; - - me.column2 = [ - { - xtype: 'textfield', - fieldLabel: gettext('Server'), - name: 'server1', - allowBlank: false, - }, - { - xtype: 'proxmoxtextfield', - fieldLabel: gettext('Fallback Server'), - deleteEmpty: !me.isCreate, - name: 'server2', - }, - { - xtype: 'proxmoxintegerfield', - name: 'port', - fieldLabel: gettext('Port'), - minValue: 1, - maxValue: 65535, - emptyText: gettext('Default'), - submitEmptyText: false, - }, + me.advancedItems = [ { xtype: 'proxmoxcheckbox', - fieldLabel: 'SSL', - name: 'secure', + fieldLabel: gettext('Check connection'), + name: 'check-connection', uncheckedValue: 0, - listeners: { - change: function(field, newValue) { - let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]'); - if (newValue === true) { - verifyCheckbox.enable(); - } else { - verifyCheckbox.disable(); - verifyCheckbox.setValue(0); - } - }, - }, - }, - { - xtype: 'proxmoxcheckbox', - fieldLabel: gettext('Verify Certificate'), - name: 'verify', - unceckedValue: 0, - disabled: true, - checked: false, + checked: true, autoEl: { tag: 'div', - 'data-qtip': gettext('Verify SSL certificate of the server'), + 'data-qtip': + gettext('Verify connection parameters and bind credentials on save'), }, }, ]; @@ -21571,6 +24317,26 @@ Ext.define('PVE.panel.LDAPInputPanel', { delete values.verify; } + if (!me.isCreate) { + // Delete old `secure` parameter. It has been deprecated in favor to the + // `mode` parameter. Migration happens automatically in `onSetValues`. + Proxmox.Utils.assemble_field_data(values, { 'delete': 'secure' }); + } + + return me.callParent([values]); + }, + + onSetValues(values) { + let me = this; + + if (values.secure !== undefined && !values.mode) { + // If `secure` is set, use it to determine the correct setting for `mode` + // `secure` is later deleted by `onSetValues` . + // In case *both* are set, we simply ignore `secure` and use + // whatever `mode` is set to. + values.mode = values.secure ? 'ldaps' : 'ldap'; + } + return me.callParent([values]); }, }); @@ -22045,12 +24811,12 @@ Ext.define('PVE.dc.AuthView', { }, ], listeners: { - activate: () => me.reload(), itemdblclick: () => me.run_editor(), }, }); me.callParent(); + me.reload(); }, }); Ext.define('PVE.dc.BackupDiskTree', { @@ -22257,15 +25023,27 @@ Ext.define('PVE.dc.BackupInfo', { column2: [ { xtype: 'displayfield', - name: 'mailnotification', + name: 'notification-policy', fieldLabel: gettext('Notification'), renderer: function(value) { - let mailto = this.up('pveBackupInfo')?.record?.mailto || 'root@localhost'; + let record = this.up('pveBackupInfo')?.record; + + // Fall back to old value, in case this option is not migrated yet. + let policy = value || record?.mailnotification || 'always'; + let when = gettext('Always'); - if (value === 'failure') { + if (policy === 'failure') { when = gettext('On failure only'); + } else if (policy === 'never') { + when = gettext('Never'); } - return `${when} (${mailto})`; + + // Notification-target takes precedence + let target = record?.['notification-target'] || + record?.mailto || + gettext('No target configured'); + + return `${when} (${target})`; }, }, { @@ -22304,6 +25082,7 @@ Ext.define('PVE.dc.BackupInfo', { xtype: 'displayfield', name: 'comment', fieldLabel: gettext('Comment'), + renderer: Ext.String.htmlEncode, }, { xtype: 'fieldset', @@ -22476,7 +25255,7 @@ Ext.define('PVE.dc.BackedGuests', { sortable: true, }, { - header: gettext('VMID'), + header: 'VMID', dataIndex: 'vmid', flex: 1, sortable: true, @@ -22574,6 +25353,14 @@ Ext.define('PVE.dc.BackupEdit', { delete values.node; } + // Get rid of new-old parameters for notification settings. + // These should only be set for those selected few who ran + // pve-manager from pvetest. + if (!isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-policy' }); + Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-target' }); + } + if (!values.id && isCreate) { values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13); } @@ -22634,6 +25421,12 @@ Ext.define('PVE.dc.BackupEdit', { selectPoolMembers: function() { let me = this; + let mode = me.lookup('modeSelector').getValue(); + + if (mode !== 'pool') { + return; + } + let vmgrid = me.lookup('vmgrid'); let poolid = me.lookup('poolSelector').getValue(); @@ -22678,6 +25471,16 @@ Ext.define('PVE.dc.BackupEdit', { success: function(response, _options) { let data = response.result.data; + // Migrate 'new'-old notification-policy back to + // old-old mailnotification. Only should affect + // users who used pve-manager from pvetest. + // This was a remnant of notifications before the + // overhaul. + let policy = data['notification-policy']; + if (policy === 'always' || policy === 'failure') { + data.mailnotification = policy; + } + if (data.exclude) { data.vmid = data.exclude; data.selMode = 'exclude'; @@ -22720,11 +25523,14 @@ Ext.define('PVE.dc.BackupEdit', { viewModel: { data: { selMode: 'include', + notificationMode: '__default__', }, formulas: { poolMode: (get) => get('selMode') === 'pool', disableVMSelection: (get) => get('selMode') !== 'include' && get('selMode') !== 'exclude', + showMailtoFields: (get) => + ['auto', 'legacy-sendmail', '__default__'].includes(get('notificationMode')), }, }, @@ -22815,21 +25621,49 @@ Ext.define('PVE.dc.BackupEdit', { ], column2: [ { - xtype: 'textfield', - fieldLabel: gettext('Send email to'), - name: 'mailto', + xtype: 'proxmoxKVComboBox', + comboItems: [ + [ + '__default__', + Ext.String.format( + gettext('{0} (Auto)'), Proxmox.Utils.defaultText, + ), + ], + ['auto', gettext('Auto')], + ['legacy-sendmail', gettext('Email (legacy)')], + ['notification-system', gettext('Notification system')], + ], + fieldLabel: gettext('Notification mode'), + name: 'notification-mode', + cbind: { + deleteEmpty: '{!isCreate}', + }, + bind: { + value: '{notificationMode}', + }, }, { xtype: 'pveEmailNotificationSelector', - fieldLabel: gettext('Email'), + fieldLabel: gettext('Send email'), name: 'mailnotification', cbind: { value: (get) => get('isCreate') ? 'always' : '', deleteEmpty: '{!isCreate}', }, + bind: { + disabled: '{!showMailtoFields}', + }, }, { - xtype: 'pveCompressionSelector', + xtype: 'textfield', + fieldLabel: gettext('Send email to'), + name: 'mailto', + bind: { + disabled: '{!showMailtoFields}', + }, + }, + { + xtype: 'pveBackupCompressionSelector', reference: 'compressionSelector', fieldLabel: gettext('Compression'), name: 'compress', @@ -23254,8 +26088,7 @@ Ext.define('PVE.dc.BackupView', { width: 80, dataIndex: 'enabled', align: 'center', - // TODO: switch to Proxmox.Utils.renderEnabledIcon once available - renderer: enabled => ``, + renderer: Proxmox.Utils.renderEnabledIcon, sortable: true, }, { @@ -24208,11 +27041,30 @@ Ext.define('PVE.dc.Config', { itemId: 'roles', }, { - xtype: 'pveAuthView', title: gettext('Realms'), + xtype: 'panel', + layout: { + type: 'border', + }, groups: ['permissions'], iconCls: 'fa fa-address-book-o', itemId: 'domains', + items: [ + { + xtype: 'pveAuthView', + region: 'center', + border: false, + }, + { + xtype: 'pveRealmSyncJobView', + title: gettext('Realm Sync Jobs'), + region: 'south', + collapsible: true, + animCollapse: false, + border: false, + height: '50%', + }, + ], }, { xtype: 'pveHAStatus', @@ -24240,7 +27092,7 @@ Ext.define('PVE.dc.Config', { me.items.push({ xtype: 'pveSDNStatus', title: gettext('SDN'), - iconCls: 'fa fa-sdn', + iconCls: 'fa fa-sdn x-fa-sdn-treelist', hidden: true, itemId: 'sdn', expandedOnInit: true, @@ -24256,9 +27108,9 @@ Ext.define('PVE.dc.Config', { { xtype: 'pveSDNVnet', groups: ['sdn'], - title: gettext('Vnets'), + title: 'VNets', hidden: true, - iconCls: 'fa fa-network-wired', + iconCls: 'fa fa-network-wired x-fa-sdn-treelist', itemId: 'sdnvnet', }, { @@ -24268,6 +27120,14 @@ Ext.define('PVE.dc.Config', { hidden: true, iconCls: 'fa fa-gear', itemId: 'sdnoptions', + }, + { + xtype: 'pveDhcpTree', + groups: ['sdn'], + title: gettext('IPAM'), + hidden: true, + iconCls: 'fa fa-map-signs', + itemId: 'sdnmappings', }); } @@ -24329,8 +27189,65 @@ Ext.define('PVE.dc.Config', { iconCls: 'fa fa-bar-chart', itemId: 'metricservers', onlineHelp: 'external_metric_server', - }, - { + }); + } + + if (caps.mapping['Mapping.Audit'] || + caps.mapping['Mapping.Use'] || + caps.mapping['Mapping.Modify']) { + me.items.push( + { + xtype: 'container', + onlineHelp: 'resource_mapping', + title: gettext('Resource Mappings'), + itemId: 'resources', + iconCls: 'fa fa-folder-o', + layout: { + type: 'vbox', + align: 'stretch', + multi: true, + }, + scrollable: true, + defaults: { + border: false, + }, + items: [ + { + xtype: 'pveDcPCIMapView', + title: gettext('PCI Devices'), + flex: 1, + }, + { + xtype: 'splitter', + collapsible: false, + performCollapse: false, + }, + { + xtype: 'pveDcUSBMapView', + title: gettext('USB Devices'), + flex: 1, + }, + ], + }, + ); + } + + if (caps.mapping['Mapping.Audit'] || + caps.mapping['Mapping.Use'] || + caps.mapping['Mapping.Modify']) { + me.items.push( + { + xtype: 'pmxNotificationConfigView', + title: gettext('Notifications'), + itemId: 'notification-targets', + iconCls: 'fa fa-bell-o', + baseUrl: '/cluster/notifications', + }, + ); + } + + if (caps.dc['Sys.Audit']) { + me.items.push({ xtype: 'pveDcSupport', title: gettext('Support'), itemId: 'support', @@ -25536,30 +28453,10 @@ Ext.define('PVE.dc.OptionView', { vtype: 'proxmoxMail', defaultValue: 'root@$hostname', }); - me.add_inputpanel_row('notify', gettext('Notify'), { - renderer: v => !v ? 'package-updates=auto' : PVE.Parser.printPropertyString(v), - labelWidth: 120, - url: "/api2/extjs/cluster/options", - //onlineHelp: 'ha_manager_shutdown_policy', - items: [{ - xtype: 'proxmoxKVComboBox', - name: 'package-updates', - fieldLabel: gettext('Package Updates'), - deleteEmpty: false, - value: '__default__', - comboItems: [ - ['__default__', Proxmox.Utils.defaultText + ' (auto)'], - ['auto', gettext('Automatically')], - ['always', gettext('Always')], - ['never', gettext('Never')], - ], - defaultValue: '__default__', - }], - }); me.add_text_row('mac_prefix', gettext('MAC address prefix'), { deleteEmpty: true, vtype: 'MacPrefix', - defaultValue: Proxmox.Utils.noneText, + defaultValue: 'BC:24:11', }); me.add_inputpanel_row('migration', gettext('Migration Settings'), { renderer: PVE.Utils.render_as_property_string, @@ -26191,8 +29088,7 @@ Ext.define('PVE.dc.PoolEdit', { }, cbind: { - autoLoad: get => !get('isCreate'), - url: get => `/api2/extjs/pools/${get('poolid')}`, + url: get => `/api2/extjs/pools/${!get('isCreate') ? '?poolid=' + get('poolid') : ''}`, method: get => get('isCreate') ? 'POST' : 'PUT', }, @@ -26214,6 +29110,23 @@ Ext.define('PVE.dc.PoolEdit', { allowBlank: true, }, ], + + initComponent: function() { + let me = this; + me.callParent(); + if (me.poolid) { + me.load({ + success: function(response) { + let data = response.result.data; + if (Ext.isArray(data)) { + me.setValues(data[0]); + } else { + me.setValues(data); + } + }, + }); + } + }, }); Ext.define('PVE.dc.PoolView', { extend: 'Ext.grid.GridPanel', @@ -26248,6 +29161,9 @@ Ext.define('PVE.dc.PoolView', { callback: function() { reload(); }, + getUrl: function(rec) { + return '/pools/?poolid=' + rec.getId(); + }, }); var run_editor = function() { @@ -26595,6 +29511,9 @@ Ext.define('PVE.SecurityGroupList', { let sm = Ext.create('Ext.selection.RowModel', {}); + let caps = Ext.state.Manager.get('GuiCap'); + let canEdit = !!caps.dc['Sys.Modify']; + let reload = function() { let oldrec = sm.getSelection()[0]; store.load((records, operation, success) => { @@ -26609,7 +29528,7 @@ Ext.define('PVE.SecurityGroupList', { let run_editor = function() { let rec = sm.getSelection()[0]; - if (!rec) { + if (!rec || !canEdit) { return; } Ext.create('PVE.SecurityGroupEdit', { @@ -26625,12 +29544,14 @@ Ext.define('PVE.SecurityGroupList', { me.editBtn = new Proxmox.button.Button({ text: gettext('Edit'), + enableFn: rec => canEdit, disabled: true, selModel: sm, handler: run_editor, }); me.addBtn = new Proxmox.button.Button({ text: gettext('Create'), + disabled: !canEdit, handler: function() { sm.deselectAll(); var win = Ext.create('PVE.SecurityGroupEdit', {}); @@ -26642,9 +29563,7 @@ Ext.define('PVE.SecurityGroupList', { me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { selModel: sm, baseurl: me.base_url + '/', - enableFn: function(rec) { - return rec && me.base_url; - }, + enableFn: (rec) => canEdit && rec && me.base_url, callback: () => reload(), }); @@ -26953,14 +29872,14 @@ Ext.define('PVE.dc.Summary', { height: 220, items: [ { - itemId: 'subscriptions', xtype: 'pveHealthWidget', + itemId: 'subscriptions', userCls: 'pointer', listeners: { element: 'el', click: function() { if (this.component.userCls === 'pointer') { - window.open('https://www.proxmox.com/en/proxmox-ve/pricing', '_blank'); + window.open('https://www.proxmox.com/en/proxmox-virtual-environment/pricing', '_blank'); } }, }, @@ -28288,6 +31207,35 @@ Ext.define('PVE.dc.UserView', { }, }); + let unlock_btn = new Proxmox.button.Button({ + text: gettext('Unlock TFA'), + disabled: true, + selModel: sm, + enableFn: rec => !!(caps.access['User.Modify'] && + (rec.data['totp-locked'] || rec.data['tfa-locked-until'])), + handler: function(btn, event, rec) { + Ext.Msg.confirm( + Ext.String.format(gettext('Unlock TFA authentication for {0}'), rec.data.userid), + gettext("Locked 2nd factors can happen if the user's password was leaked. Are you sure you want to unlock the user?"), + function(btn_response) { + if (btn_response === 'yes') { + Proxmox.Utils.API2Request({ + url: `/access/users/${rec.data.userid}/unlock-tfa`, + waitMsgTarget: me, + method: 'PUT', + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + reload(); + }, + }); + } + }, + ); + }, + }); + Ext.apply(me, { store: store, selModel: sm, @@ -28311,6 +31259,8 @@ Ext.define('PVE.dc.UserView', { pwchange_btn, '-', perm_btn, + '-', + unlock_btn, ], viewConfig: { trackOver: false, @@ -28353,26 +31303,46 @@ Ext.define('PVE.dc.UserView', { }, { header: 'TFA', - width: 50, + width: 120, sortable: true, - renderer: function(v) { + renderer: function(v, metaData, record) { let tfa_type = PVE.Parser.parseTfaType(v); if (tfa_type === undefined) { return Proxmox.Utils.noText; - } else if (tfa_type === 1) { - return Proxmox.Utils.yesText; - } else { + } + + if (tfa_type !== 1) { return tfa_type; } + + let locked_until = record.data['tfa-locked-until']; + if (locked_until !== undefined) { + let now = new Date().getTime() / 1000; + if (locked_until > now) { + return gettext('Locked'); + } + } + + if (record.data['totp-locked']) { + return gettext('TOTP Locked'); + } + + return Proxmox.Utils.yesText; }, dataIndex: 'keys', }, + { + header: gettext('Groups'), + dataIndex: 'groups', + renderer: Ext.htmlEncode, + flex: 2, + }, { header: gettext('Comment'), sortable: false, renderer: Ext.String.htmlEncode, dataIndex: 'comment', - flex: 1, + flex: 3, }, ], listeners: { @@ -29046,6 +32016,628 @@ Ext.define('PVE.dc.RegisteredTagsEdit', { }, ], }); +Ext.define('PVE.dc.RealmSyncJobView', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveRealmSyncJobView', + + stateful: true, + stateId: 'grid-realmsyncjobs', + + emptyText: Ext.String.format(gettext('No {0} configured'), gettext('Realm Sync Job')), + + controller: { + xclass: 'Ext.app.ViewController', + + addRealmSyncJob: function(button) { + let me = this; + Ext.create(`PVE.dc.RealmSyncJobEdit`, { + autoShow: true, + listeners: { + destroy: () => me.reload(), + }, + }); + }, + + editRealmSyncJob: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (!selection || selection.length < 1) { + return; + } + + Ext.create(`PVE.dc.RealmSyncJobEdit`, { + jobid: selection[0].data.id, + autoShow: true, + listeners: { + destroy: () => me.reload(), + }, + }); + }, + + runNow: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (!selection || selection.length < 1) { + return; + } + + let params = selection[0].data; + let realm = params.realm; + + let propertiesToDelete = ['comment', 'realm', 'id', 'type', 'schedule', 'last-run', 'next-run', 'enabled']; + for (const prop of propertiesToDelete) { + delete params[prop]; + } + + Proxmox.Utils.API2Request({ + url: `/access/domains/${realm}/sync`, + params, + waitMsgTarget: view, + method: 'POST', + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: function(response, options) { + Ext.create('Proxmox.window.TaskProgress', { + autoShow: true, + upid: response.result.data, + taskDone: () => { me.reload(); }, + }); + }, + }); + }, + + reload: function() { + this.getView().getStore().load(); + }, + }, + + store: { + autoLoad: true, + id: 'realm-syncs', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/jobs/realm-sync', + }, + }, + + viewConfig: { + getRowClass: (record, _index) => record.get('enabled') ? '' : 'proxmox-disabled-row', + }, + + columns: [ + { + header: gettext('Enabled'), + width: 80, + dataIndex: 'enabled', + sortable: true, + align: 'center', + stopSelection: false, + renderer: Proxmox.Utils.renderEnabledIcon, + }, + { + text: gettext('Name'), + flex: 1, + dataIndex: 'id', + hidden: true, + }, + { + text: gettext('Realm'), + width: 200, + dataIndex: 'realm', + }, + { + header: gettext('Schedule'), + width: 150, + dataIndex: 'schedule', + }, + { + text: gettext('Next Run'), + dataIndex: 'next-run', + width: 150, + renderer: PVE.Utils.render_next_event, + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.htmlEncode, + sorter: (a, b) => (a.data.comment || '').localeCompare(b.data.comment || ''), + flex: 1, + }, + ], + + tbar: [ + { + text: gettext('Add'), + handler: 'addRealmSyncJob', + }, + { + text: gettext('Edit'), + xtype: 'proxmoxButton', + handler: 'editRealmSyncJob', + disabled: true, + }, + { + xtype: 'proxmoxStdRemoveButton', + baseurl: `/api2/extjs/cluster/jobs/realm-sync`, + callback: 'reload', + }, + { + xtype: 'proxmoxButton', + handler: 'runNow', + disabled: true, + text: gettext('Run Now'), + }, + ], + + listeners: { + itemdblclick: 'editRealmSyncJob', + }, + + initComponent: function() { + var me = this; + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore()); + }, +}); + +Ext.define('PVE.dc.RealmSyncJobEdit', { + extend: 'Proxmox.window.Edit', + mixins: ['Proxmox.Mixin.CBind'], + + subject: gettext('Realm Sync Job'), + onlineHelp: 'pveum_ldap_sync', + + // don't focus the schedule field on edit + defaultFocus: 'field[name=id]', + + cbindData: function() { + let me = this; + me.isCreate = !me.jobid; + me.jobid = me.jobid || ""; + let url = '/api2/extjs/cluster/jobs/realm-sync'; + me.url = me.jobid ? `${url}/${me.jobid}` : url; + me.method = me.isCreate ? 'POST' : 'PUT'; + if (!me.isCreate) { + me.subject = `${me.subject}: ${me.jobid}`; + } + return {}; + }, + + submitUrl: function(url, values) { + return this.isCreate ? `${url}/${values.id}` : url; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + updateDefaults: function(_field, newValue) { + let me = this; + + ['scope', 'enable-new', 'schedule'].forEach((reference) => { + me.lookup(reference)?.setDisabled(false); + }); + + // only update on create + if (!me.getView().isCreate) { + return; + } + Proxmox.Utils.API2Request({ + url: `/access/domains/${newValue}`, + success: function(response) { + // first reset the fields to their default + ['acl', 'entry', 'properties'].forEach(opt => { + me.lookup(`remove-vanished-${opt}`)?.setValue(false); + }); + me.lookup('enable-new')?.setValue('1'); + me.lookup('scope')?.setValue(undefined); + + let options = response?.result?.data?.['sync-defaults-options']; + if (options) { + let parsed = PVE.Parser.parsePropertyString(options); + if (parsed['remove-vanished']) { + let opts = parsed['remove-vanished'].split(';'); + for (const opt of opts) { + me.lookup(`remove-vanished-${opt}`)?.setValue(true); + } + delete parsed['remove-vanished']; + } + for (const [name, value] of Object.entries(parsed)) { + me.lookup(name)?.setValue(value); + } + } + }, + }); + }, + }, + + items: [ + { + xtype: 'inputpanel', + + cbind: { + isCreate: '{isCreate}', + }, + + onGetValues: function(values) { + let me = this; + + let vanished_opts = []; + ['acl', 'entry', 'properties'].forEach((prop) => { + if (values[`remove-vanished-${prop}`]) { + vanished_opts.push(prop); + } + delete values[`remove-vanished-${prop}`]; + }); + + if (!values.id && me.isCreate) { + values.id = 'realmsync-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13); + } + + if (vanished_opts.length > 0) { + values['remove-vanished'] = vanished_opts.join(';'); + } else { + values['remove-vanished'] = 'none'; + } + + PVE.Utils.delete_if_default(values, 'node', ''); + + if (me.isCreate) { + delete values.delete; // on create we cannot delete values + } + + return values; + }, + + column1: [ + { + xtype: 'pmxDisplayEditField', + editConfig: { + xtype: 'pmxRealmComboBox', + storeFilter: rec => rec.data.type === 'ldap' || rec.data.type === 'ad', + }, + listConfig: { + emptyText: `
${gettext('No LDAP/AD Realm found')}
`, + }, + cbind: { + editable: '{isCreate}', + }, + listeners: { + change: 'updateDefaults', + }, + fieldLabel: gettext('Realm'), + name: 'realm', + reference: 'realm', + }, + { + xtype: 'pveCalendarEvent', + fieldLabel: gettext('Schedule'), + disabled: true, + allowBlank: false, + name: 'schedule', + reference: 'schedule', + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enable Job'), + name: 'enabled', + reference: 'enabled', + uncheckedValue: 0, + defaultValue: 1, + checked: true, + }, + ], + + column2: [ + { + xtype: 'proxmoxKVComboBox', + name: 'scope', + reference: 'scope', + disabled: true, + fieldLabel: gettext('Scope'), + value: '', + emptyText: gettext('No default available'), + deleteEmpty: false, + allowBlank: false, + comboItems: [ + ['users', gettext('Users')], + ['groups', gettext('Groups')], + ['both', gettext('Users and Groups')], + ], + }, + { + xtype: 'proxmoxKVComboBox', + value: '1', + deleteEmpty: false, + disabled: true, + allowBlank: false, + comboItems: [ + ['1', Proxmox.Utils.yesText], + ['0', Proxmox.Utils.noText], + ], + name: 'enable-new', + reference: 'enable-new', + fieldLabel: gettext('Enable New'), + }, + ], + + columnB: [ + { + xtype: 'fieldset', + title: gettext('Remove Vanished Options'), + items: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('ACL'), + name: 'remove-vanished-acl', + reference: 'remove-vanished-acl', + boxLabel: gettext('Remove ACLs of vanished users and groups.'), + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Entry'), + name: 'remove-vanished-entry', + reference: 'remove-vanished-entry', + boxLabel: gettext('Remove vanished user and group entries.'), + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Properties'), + name: 'remove-vanished-properties', + reference: 'remove-vanished-properties', + boxLabel: gettext('Remove vanished properties from synced users.'), + }, + ], + }, + { + xtype: 'proxmoxtextfield', + name: 'comment', + fieldLabel: gettext('Job Comment'), + cbind: { + deleteEmpty: '{!isCreate}', + }, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Description of the job'), + }, + }, + { + xtype: 'displayfield', + reference: 'defaulthint', + value: gettext('Default sync options can be set by editing the realm.'), + userCls: 'pmx-hint', + hidden: true, + }, + ], + }, + ], + + initComponent: function() { + let me = this; + me.callParent(); + if (me.jobid) { + me.load({ + success: function(response, options) { + let values = response.result.data; + + if (values['remove-vanished']) { + let opts = values['remove-vanished'].split(';'); + for (const opt of opts) { + values[`remove-vanished-${opt}`] = 1; + } + } + me.down('inputpanel').setValues(values); + }, + }); + } + }, +}); +Ext.define('pve-resource-pci-tree', { + extend: 'Ext.data.Model', + idProperty: 'internalId', + fields: ['type', 'text', 'path', 'id', 'subsystem-id', 'iommugroup', 'description', 'digest'], +}); + +Ext.define('PVE.dc.PCIMapView', { + extend: 'PVE.tree.ResourceMapTree', + alias: 'widget.pveDcPCIMapView', + + editWindowClass: 'PVE.window.PCIMapEditWindow', + baseUrl: '/cluster/mapping/pci', + mapIconCls: 'pve-itype-icon-pci', + getStatusCheckUrl: (node) => `/nodes/${node}/hardware/pci?pci-class-blacklist=`, + entryIdProperty: 'path', + + checkValidity: function(data, node) { + let me = this; + let ids = {}; + data.forEach((entry) => { + ids[entry.id] = entry; + }); + me.getRootNode()?.cascade(function(rec) { + if (rec.data.node !== node || rec.data.type !== 'map') { + return; + } + + let id = rec.data.path; + if (!id.match(/\.\d$/)) { + id += '.0'; + } + let device = ids[id]; + if (!device) { + rec.set('valid', 0); + rec.set('errmsg', Ext.String.format(gettext("Cannot find PCI id {0}"), id)); + rec.commit(); + return; + } + + + let deviceId = `${device.vendor}:${device.device}`.replace(/0x/g, ''); + let subId = `${device.subsystem_vendor}:${device.subsystem_device}`.replace(/0x/g, ''); + + let toCheck = { + id: deviceId, + 'subsystem-id': subId, + iommugroup: device.iommugroup !== -1 ? device.iommugroup : undefined, + }; + + let valid = 1; + let errors = []; + let errText = gettext("Configuration for {0} not correct ('{1}' != '{2}')"); + for (const [key, validValue] of Object.entries(toCheck)) { + if (`${rec.data[key]}` !== `${validValue}`) { + errors.push(Ext.String.format(errText, key, rec.data[key] ?? '', validValue)); + valid = 0; + } + } + + rec.set('valid', valid); + rec.set('errmsg', errors.join('
')); + rec.commit(); + }); + }, + + store: { + sorters: 'text', + model: 'pve-resource-pci-tree', + data: {}, + }, + + columns: [ + { + xtype: 'treecolumn', + text: gettext('ID/Node/Path'), + dataIndex: 'text', + width: 200, + }, + { + text: gettext('Vendor/Device'), + dataIndex: 'id', + }, + { + text: gettext('Subsystem Vendor/Device'), + dataIndex: 'subsystem-id', + }, + { + text: gettext('IOMMU-Group'), + dataIndex: 'iommugroup', + }, + { + header: gettext('Status'), + dataIndex: 'valid', + flex: 1, + renderer: 'renderStatus', + }, + { + header: gettext('Comment'), + dataIndex: 'description', + renderer: function(value, _meta, record) { + return Ext.String.htmlEncode(value ?? record.data.comment); + }, + flex: 1, + }, + ], +}); +Ext.define('pve-resource-usb-tree', { + extend: 'Ext.data.Model', + idProperty: 'internalId', + fields: ['type', 'text', 'path', 'id', 'description', 'digest'], +}); + +Ext.define('PVE.dc.USBMapView', { + extend: 'PVE.tree.ResourceMapTree', + alias: 'widget.pveDcUSBMapView', + + editWindowClass: 'PVE.window.USBMapEditWindow', + baseUrl: '/cluster/mapping/usb', + mapIconCls: 'fa fa-usb', + getStatusCheckUrl: (node) => `/nodes/${node}/hardware/usb`, + entryIdProperty: 'id', + + checkValidity: function(data, node) { + let me = this; + let ids = {}; + let paths = {}; + data.forEach((entry) => { + ids[`${entry.vendid}:${entry.prodid}`] = entry; + paths[`${entry.busnum}-${entry.usbpath}`] = entry; + }); + me.getRootNode()?.cascade(function(rec) { + if (rec.data.node !== node || rec.data.type !== 'map') { + return; + } + + let device; + if (rec.data.path) { + device = paths[rec.data.path]; + } + device ??= ids[rec.data.id]; + + if (!device) { + rec.set('valid', 0); + rec.set('errmsg', Ext.String.format(gettext("Cannot find USB device {0}"), rec.data.id)); + rec.commit(); + return; + } + + + let deviceId = `${device.vendid}:${device.prodid}`.replace(/0x/g, ''); + + let toCheck = { + id: deviceId, + }; + + let valid = 1; + let errors = []; + let errText = gettext("Configuration for {0} not correct ('{1}' != '{2}')"); + for (const [key, validValue] of Object.entries(toCheck)) { + if (rec.data[key] !== validValue) { + errors.push(Ext.String.format(errText, key, rec.data[key] ?? '', validValue)); + valid = 0; + } + } + + rec.set('valid', valid); + rec.set('errmsg', errors.join('
')); + rec.commit(); + }); + }, + + store: { + sorters: 'text', + model: 'pve-resource-usb-tree', + data: {}, + }, + + columns: [ + { + xtype: 'treecolumn', + text: gettext('ID/Node/Vendor&Device'), + dataIndex: 'text', + width: 200, + }, + { + text: gettext('Path'), + dataIndex: 'path', + }, + { + header: gettext('Status'), + dataIndex: 'valid', + flex: 1, + renderer: 'renderStatus', + }, + { + header: gettext('Comment'), + dataIndex: 'description', + renderer: function(value, _meta, record) { + return Ext.String.htmlEncode(value ?? record.data.comment); + }, + flex: 1, + }, + ], +}); Ext.define('PVE.lxc.CmdMenu', { extend: 'Ext.menu.Menu', @@ -29079,7 +32671,7 @@ Ext.define('PVE.lxc.CmdMenu', { }; let caps = Ext.state.Manager.get('GuiCap'); - let standalone = PVE.data.ResourceStore.getNodes().length < 2; + let standalone = PVE.Utils.isStandaloneNode(); let running = false, stopped = true, suspended = false; switch (info.status) { @@ -29268,7 +32860,7 @@ Ext.define('PVE.lxc.Config', { var migrateBtn = Ext.create('Ext.Button', { text: gettext('Migrate'), disabled: !caps.vms['VM.Migrate'], - hidden: PVE.data.ResourceStore.getNodes().length < 2, + hidden: PVE.Utils.isStandaloneNode(), handler: function() { var win = Ext.create('PVE.window.Migrate', { vmtype: 'lxc', @@ -29480,7 +33072,7 @@ Ext.define('PVE.lxc.Config', { }); } - if (caps.vms['VM.Console']) { + if (caps.vms['VM.Audit']) { me.items.push( { xtype: 'pveFirewallRules', @@ -29518,6 +33110,11 @@ Ext.define('PVE.lxc.Config', { list_refs_url: base_url + '/firewall/refs', itemId: 'firewall-ipset', }, + ); + } + + if (caps.vms['VM.Console']) { + me.items.push( { title: gettext('Log'), groups: ['firewall'], @@ -29710,16 +33307,16 @@ Ext.define('PVE.lxc.CreateWizard', { }, }, { - xtype: 'proxmoxtextfield', + xtype: 'textarea', name: 'ssh-public-keys', value: '', - fieldLabel: gettext('SSH public key'), + fieldLabel: gettext('SSH public key(s)'), allowBlank: true, validator: function(value) { let pwfield = this.up().down('field[name=password]'); if (value.length) { - let key = PVE.Parser.parseSSHKey(value); - if (!key) { + let keys = value.indexOf('\n') !== -1 ? value.split('\n') : [value]; + if (keys.some(key => key !== '' && !PVE.Parser.parseSSHKey(key))) { return "Failed to recognize ssh key"; } pwfield.allowBlank = true; @@ -29749,20 +33346,32 @@ Ext.define('PVE.lxc.CreateWizard', { }, }, { - xtype: 'filebutton', + xtype: 'pveMultiFileButton', name: 'file', hidden: !window.FileReader, text: gettext('Load SSH Key File'), listeners: { change: function(btn, e, value) { e = e.event; - let field = this.up().down('proxmoxtextfield[name=ssh-public-keys]'); - PVE.Utils.loadSSHKeyFromFile(e.target.files[0], v => field.setValue(v)); + let field = this.up().down('textarea[name=ssh-public-keys]'); + for (const file of e?.target?.files ?? []) { + PVE.Utils.loadSSHKeyFromFile(file, v => { + let oldValue = field.getValue(); + field.setValue(oldValue ? `${oldValue}\n${v.trim()}` : v.trim()); + }); + } btn.reset(); }, }, }, ], + advancedColumnB: [ + { + xtype: 'pveTagFieldSet', + name: 'tags', + maxHeight: 150, + }, + ], }, { xtype: 'inputpanel', @@ -30759,7 +34368,7 @@ Ext.define('PVE.window.MPResize', { maxValue: 128*1024, decimalPrecision: 3, value: '0', - fieldLabel: gettext('Size Increment') + ' (GiB)', + fieldLabel: `${gettext('Size Increment')} (${gettext('GiB')})`, allowBlank: false, }); @@ -32326,7 +35935,7 @@ Ext.define('PVE.menu.TemplateMenu', { me.title = (guestType === 'qemu' ? 'VM ' : 'CT ') + info.vmid; let caps = Ext.state.Manager.get('GuiCap'); - let standaloneNode = PVE.data.ResourceStore.getNodes().length < 2; + let standaloneNode = PVE.Utils.isStandaloneNode(); me.items = [ { @@ -32412,9 +36021,8 @@ Ext.define('PVE.ceph.CephVersionSelector', { }, }, data: [ - { release: "octopus", version: "15.2" }, - { release: "pacific", version: "16.2" }, { release: "quincy", version: "17.2" }, + { release: "reef", version: "18.2" }, ], }, }); @@ -32472,6 +36080,8 @@ Ext.define('PVE.ceph.CephHighestVersionDisplay', { 15: 'octopus', 16: 'pacific', 17: 'quincy', + 18: 'reef', + 19: 'squid', }; let release = major2release[maxversion[0]] || 'unknown'; let newestVersionTxt = `${Ext.String.capitalize(release)} (${maxversiontext})`; @@ -32503,12 +36113,40 @@ Ext.define('PVE.ceph.CephInstallWizard', { resizable: false, nodename: undefined, + width: 760, // 4:3 + height: 570, + viewModel: { data: { nodename: '', - cephRelease: 'quincy', + cephRelease: 'reef', + cephRepo: 'enterprise', configuration: true, isInstalled: false, + nodeHasSubscription: true, // avoid warning hint until fully loaded + allHaveSubscription: true, // avoid warning hint until fully loaded + }, + formulas: { + repoHintHidden: get => get('allHaveSubscription') && get('cephRepo') === 'enterprise', + repoHint: function(get) { + let repo = get('cephRepo'); + let nodeSub = get('nodeHasSubscription'), allSub = get('allHaveSubscription'); + + if (repo === 'enterprise') { + if (!nodeSub) { + return gettext('The enterprise repository is enabled, but there is no active subscription!'); + } else if (!allSub) { + return gettext('Not all nodes have an active subscription, which is required for cluster-wide enterprise repo access'); + } + return ''; // should be hidden + } else if (repo === 'no-subscription') { + return allSub + ? gettext("Cluster has active subscriptions and would be elligible for using the enterprise repository.") + : gettext("The no-subscription repository is not the best choice for production setups."); + } else { + return gettext('The test repository should only be used for test setups or after consulting the official Proxmox support!'); + } + }, }, }, cbindData: { @@ -32534,12 +36172,20 @@ Ext.define('PVE.ceph.CephInstallWizard', { tp.setActiveTab(initialTab); }, onShow: function() { - this.callParent(arguments); - var isInstalled = this.getViewModel().get('isInstalled'); - if (isInstalled) { - this.getViewModel().set('configuration', false); - this.setInitialTab(2); - } + this.callParent(arguments); + let viewModel = this.getViewModel(); + var isInstalled = this.getViewModel().get('isInstalled'); + if (isInstalled) { + viewModel.set('configuration', false); + this.setInitialTab(2); + } + + PVE.Utils.getClusterSubscriptionLevel().then(subcriptionMap => { + viewModel.set('nodeHasSubscription', !!subcriptionMap[this.nodename]); + + let allHaveSubscription = Object.values(subcriptionMap).every(level => !!level); + viewModel.set('allHaveSubscription', allHaveSubscription); + }); }, items: [ { @@ -32564,9 +36210,20 @@ Ext.define('PVE.ceph.CephInstallWizard', { { flex: 1, }, + { + xtype: 'displayfield', + fieldLabel: gettext('Hint'), + labelClsExtra: 'pmx-hint', + submitValue: false, + labelWidth: 50, + bind: { + value: '{repoHint}', + hidden: '{repoHintHidden}', + }, + }, { xtype: 'pveCephHighestVersionDisplay', - labelWidth: 180, + labelWidth: 150, cbind: { nodename: '{nodename}', }, @@ -32579,20 +36236,46 @@ Ext.define('PVE.ceph.CephInstallWizard', { }, }, { - xtype: 'pveCephVersionSelector', - labelWidth: 180, - submitValue: false, - bind: { - value: '{cephRelease}', + xtype: 'container', + layout: 'hbox', + defaults: { + border: false, + layout: 'anchor', + flex: 1, }, - listeners: { - change: function(field, release) { - let wizard = this.up('pveCephInstallWizard'); - wizard.down('#next').setText( - Ext.String.format(gettext('Start {0} installation'), release), - ); + items: [{ + xtype: 'pveCephVersionSelector', + labelWidth: 150, + padding: '0 10 0 0', + submitValue: false, + bind: { + value: '{cephRelease}', + }, + listeners: { + change: function(field, release) { + let wizard = this.up('pveCephInstallWizard'); + wizard.down('#next').setText( + Ext.String.format(gettext('Start {0} installation'), release), + ); + }, }, }, + { + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Repository'), + padding: '0 0 0 10', + comboItems: [ + ['enterprise', gettext('Enterprise (recommended)')], + ['no-subscription', gettext('No-Subscription')], + ['test', gettext('Test')], + ], + labelWidth: 150, + submitValue: false, + value: 'enterprise', + bind: { + value: '{cephRepo}', + }, + }], }, ], listeners: { @@ -32684,7 +36367,8 @@ Ext.define('PVE.ceph.CephInstallWizard', { let me = this; let wizard = me.up('pveCephInstallWizard'); let release = wizard.getViewModel().get('cephRelease'); - me.cmdOpts = `--version\0${release}`; + let repo = wizard.getViewModel().get('cephRepo'); + me.cmdOpts = `--version\0${release}\0--repository\0${repo}`; }, cmd: 'ceph_install', }, @@ -33259,12 +36943,12 @@ Ext.define('PVE.NodeCephFSPanel', { dataIndex: 'name', }, { - header: 'Data Pool', + header: gettext('Data Pool'), flex: 1, dataIndex: 'data_pool', }, { - header: 'Metadata Pool', + header: gettext('Metadata Pool'), flex: 1, dataIndex: 'metadata_pool', }, @@ -33450,7 +37134,7 @@ Ext.define('PVE.CephCreateOsd', { value: '', autoSelect: false, allowBlank: true, - emptyText: 'use OSD disk', + emptyText: gettext('use OSD disk'), listeners: { change: function(field, val) { me.down('field[name=db_dev_size]').setDisabled(!val); @@ -33460,7 +37144,7 @@ Ext.define('PVE.CephCreateOsd', { { xtype: 'numberfield', name: 'db_dev_size', - fieldLabel: gettext('DB size') + ' (GiB)', + fieldLabel: `${gettext('DB size')} (${gettext('GiB')})`, minValue: 1, maxValue: 128*1024, decimalPrecision: 2, @@ -33489,7 +37173,7 @@ Ext.define('PVE.CephCreateOsd', { autoSelect: false, allowBlank: true, editable: true, - emptyText: 'auto detect', + emptyText: gettext('auto detect'), deleteEmpty: !me.isCreate, }, ], @@ -33504,7 +37188,7 @@ Ext.define('PVE.CephCreateOsd', { value: '', autoSelect: false, allowBlank: true, - emptyText: 'use OSD/DB disk', + emptyText: gettext('use OSD/DB disk'), listeners: { change: function(field, val) { me.down('field[name=wal_dev_size]').setDisabled(!val); @@ -33514,7 +37198,7 @@ Ext.define('PVE.CephCreateOsd', { { xtype: 'numberfield', name: 'wal_dev_size', - fieldLabel: gettext('WAL size') + ' (GiB)', + fieldLabel: `${gettext('WAL size')} (${gettext('GiB')})`, minValue: 0.5, maxValue: 128*1024, decimalPrecision: 2, @@ -34515,7 +38199,7 @@ Ext.define('PVE.CephOsdDetails', { { xtype: 'text', name: 'mem_usage', - text: gettext('Memory usage'), + text: gettext('Memory usage (PSS)'), renderer: Proxmox.Utils.render_size, }, { @@ -34563,7 +38247,7 @@ Ext.define('PVE.CephOsdDetails', { }, { xtype: 'panel', - title: 'Devices', + title: gettext('Devices'), tooltip: gettext('Physical devices used by the OSD'), items: [ { @@ -34654,6 +38338,52 @@ Ext.define('PVE.CephPoolInputPanel', { onlineHelp: 'pve_ceph_pools', subject: 'Ceph Pool', + + defaultSize: undefined, + defaultMinSize: undefined, + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + let vm = this.getViewModel(); + vm.set('size', Number(view.defaultSize)); + vm.set('minSize', Number(view.defaultMinSize)); + }, + sizeChange: function(field, val) { + let vm = this.getViewModel(); + let minSize = Math.round(val / 2); + if (minSize > 1) { + vm.set('minSize', minSize); + } + vm.set('size', val); // bind does not work in a pmxDisplayEditField, update manually + }, + }, + + viewModel: { + data: { + minSize: null, + size: null, + }, + formulas: { + minSizeLabel: (get) => { + if (get('showMinSizeOneWarning') || get('showMinSizeHalfWarning')) { + return `${gettext('Min. Size')} `; + } + return gettext('Min. Size'); + }, + showMinSizeOneWarning: (get) => get('minSize') === 1, + showMinSizeHalfWarning: (get) => { + let minSize = get('minSize'); + let size = get('size'); + if (minSize === 1) { + return false; + } + return minSize < (size / 2) && minSize !== size; + }, + }, + }, + column1: [ { xtype: 'pmxDisplayEditField', @@ -34674,20 +38404,16 @@ Ext.define('PVE.CephPoolInputPanel', { name: 'size', editConfig: { xtype: 'proxmoxintegerfield', - value: 3, + cbind: { + value: (get) => get('defaultSize'), + }, minValue: 2, maxValue: 7, allowBlank: false, listeners: { - change: function(field, val) { - let size = Math.round(val / 2); - if (size > 1) { - field.up('inputpanel').down('field[name=min_size]').setValue(size); - } - }, + change: 'sizeChange', }, }, - }, ], column2: [ @@ -34723,36 +38449,41 @@ Ext.define('PVE.CephPoolInputPanel', { advancedColumn1: [ { xtype: 'proxmoxintegerfield', - fieldLabel: gettext('Min. Size'), + bind: { + fieldLabel: '{minSizeLabel}', + value: '{minSize}', + }, name: 'min_size', - value: 2, cbind: { - minValue: (get) => get('isCreate') ? 2 : 1, - }, - maxValue: 7, - allowBlank: false, - listeners: { - change: function(field, minSize) { - let panel = field.up('inputpanel'); - let size = panel.down('field[name=size]').getValue(); - - let showWarning = minSize < (size / 2) && minSize !== size; - - let fieldLabel = gettext('Min. Size'); - if (showWarning) { - fieldLabel = gettext('Min. Size') + ' '; + value: (get) => get('defaultMinSize'), + minValue: (get) => { + if (Number(get('defaultMinSize')) === 1) { + return 1; + } else { + return get('isCreate') ? 2 : 1; } - panel.down('field[name=min_size-warning]').setHidden(!showWarning); - field.setFieldLabel(fieldLabel); }, }, + maxValue: 7, + allowBlank: false, }, { xtype: 'displayfield', - name: 'min_size-warning', + bind: { + hidden: '{!showMinSizeHalfWarning}', + }, + hidden: true, userCls: 'pmx-hint', value: gettext('min_size < size/2 can lead to data loss, incomplete PGs or unfound objects.'), + }, + { + xtype: 'displayfield', + bind: { + hidden: '{!showMinSizeOneWarning}', + }, hidden: true, + userCls: 'pmx-hint', + value: gettext('a min_size of 1 is not recommended and can lead to data loss'), }, { xtype: 'pmxDisplayEditField', @@ -34842,6 +38573,8 @@ Ext.define('PVE.Ceph.PoolEdit', { cbindData: { pool_name: '', isCreate: (cfg) => !cfg.pool_name, + defaultSize: undefined, + defaultMinSize: undefined, }, cbind: { @@ -34864,6 +38597,8 @@ Ext.define('PVE.Ceph.PoolEdit', { pool_name: '{pool_name}', isErasure: '{isErasure}', isCreate: '{isCreate}', + defaultSize: '{defaultSize}', + defaultMinSize: '{defaultMinSize}', }, }], }); @@ -34881,6 +38616,14 @@ Ext.define('PVE.node.Ceph.PoolList', { features: [{ ftype: 'summary' }], columns: [ + { + text: gettext('Pool #'), + minWidth: 70, + flex: 1, + align: 'right', + sortable: true, + dataIndex: 'pool', + }, { text: gettext('Name'), minWidth: 120, @@ -35036,14 +38779,37 @@ Ext.define('PVE.node.Ceph.PoolList', { { text: gettext('Create'), handler: function() { - Ext.create('PVE.Ceph.PoolEdit', { - title: gettext('Create') + ': Ceph Pool', - isCreate: true, - isErasure: false, - nodename: nodename, - autoShow: true, - listeners: { - destroy: () => rstore.load(), + let keys = [ + 'global:osd-pool-default-min-size', + 'global:osd-pool-default-size', + ]; + let params = { + 'config-keys': keys.join(';'), + }; + + Proxmox.Utils.API2Request({ + url: '/nodes/localhost/ceph/cfg/value', + method: 'GET', + params, + waitMsgTarget: me.getView(), + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: function({ result: { data } }) { + let global = data.global; + let defaultSize = global?.['osd-pool-default-size'] ?? 3; + let defaultMinSize = global?.['osd-pool-default-min-size'] ?? 2; + + Ext.create('PVE.Ceph.PoolEdit', { + title: gettext('Create') + ': Ceph Pool', + isCreate: true, + isErasure: false, + defaultSize, + defaultMinSize, + nodename: nodename, + autoShow: true, + listeners: { + destroy: () => rstore.load(), + }, + }); }, }); }, @@ -36023,6 +39789,13 @@ Ext.define('PVE.ceph.ServiceWidget', { }, }, }); +Ext.define('pve-ceph-warnings', { + extend: 'Ext.data.Model', + fields: ['id', 'summary', 'detail', 'severity'], + idProperty: 'id', +}); + + Ext.define('PVE.node.CephStatus', { extend: 'Ext.panel.Panel', alias: 'widget.pveNodeCephStatus', @@ -36095,35 +39868,53 @@ Ext.define('PVE.node.CephStatus', { xtype: 'grid', itemId: 'warnings', flex: 2, + maxHeight: 430, stateful: true, stateId: 'ceph-status-warnings', + viewConfig: { + enableTextSelection: true, + }, // we load the store manually, to show an emptyText specify an empty intermediate store store: { + type: 'diff', trackRemoved: false, data: [], + rstore: { + storeid: 'pve-ceph-warnings', + type: 'update', + model: 'pve-ceph-warnings', + }, }, updateHealth: function(health) { let checks = health.checks || {}; let checkRecords = Object.keys(checks).sort().map(key => { let check = checks[key]; - return { + let data = { id: key, summary: check.summary.message, - detail: check.detail.reduce((acc, v) => `${acc}\n${v.message}`, ''), + detail: check.detail.reduce((acc, v) => `${acc}\n${v.message}`, '').trimStart(), severity: check.severity, }; + data.noDetails = data.detail.length === 0; + data.detailsCls = data.detail.length === 0 ? 'pmx-opacity-75' : ''; + if (data.detail.length === 0) { + data.detail = "no additional data"; + } + return data; }); - this.getStore().loadRawData(checkRecords, false); + let rstore = this.getStore().rstore; + rstore.loadData(checkRecords, false); + rstore.fireEvent('load', rstore, checkRecords, true); }, emptyText: gettext('No Warnings/Errors'), columns: [ { dataIndex: 'severity', - header: gettext('Severity'), + tooltip: gettext('Severity'), align: 'center', - width: 70, + width: 38, renderer: function(value) { let health = PVE.Utils.map_ceph_health[value]; let icon = PVE.Utils.get_health_icon(health); @@ -36143,38 +39934,44 @@ Ext.define('PVE.node.CephStatus', { }, { xtype: 'actioncolumn', - width: 40, + width: 50, align: 'center', - tooltip: gettext('Detail'), + tooltip: gettext('Actions'), items: [ { - iconCls: 'x-fa fa-info-circle', - handler: function(grid, rowindex, colindex, item, e, record) { - var win = Ext.create('Ext.window.Window', { - title: gettext('Detail'), - resizable: true, - modal: true, - width: 650, - height: 400, - layout: { - type: 'fit', - }, - items: [{ - scrollable: true, - padding: 10, - xtype: 'box', - html: [ - '' + Ext.htmlEncode(record.data.summary) + '', - '
' + Ext.htmlEncode(record.data.detail) + '
', - ], - }], - }); - win.show(); + iconCls: 'x-fa fa-clipboard', + tooltip: gettext('Copy to Clipboard'), + handler: function(grid, rowindex, colindex, item, e, { data }) { + let detail = data.noDetails ? '': `\n${data.detail}`; + navigator.clipboard + .writeText(`${data.severity}: ${data.summary}${detail}`) + .catch(err => Ext.Msg.alert(gettext('Error'), err)); }, }, ], }, ], + listeners: { + itemdblclick: function(view, record, row, rowIdx, e) { + // inspired by Ext.grid.plugin.RowExpander, but for double click + let rowNode = view.getNode(rowIdx); + let normalRow = Ext.fly(rowNode); + + let collapsedCls = view.rowBodyFeature.rowCollapsedCls; + + if (normalRow.hasCls(collapsedCls)) { + view.rowBodyFeature.rowExpander.toggleRow(rowIdx, record); + } + }, + }, + plugins: [ + { + ptype: 'rowexpander', + expandOnDblClick: false, + scrollIntoViewOnExpand: false, + rowBodyTpl: '
{detail}
', + }, + ], }, ], }, @@ -36532,6 +40329,7 @@ Ext.define('PVE.ceph.StatusDetail', { colors: [ '#CFCFCF', '#21BF4B', + '#3892d4', '#FFCC00', '#FF6C59', ], @@ -36584,13 +40382,12 @@ Ext.define('PVE.ceph.StatusDetail', { clean: 1, active: 1, - // working + // busy activating: 2, backfill_wait: 2, backfilling: 2, creating: 2, deep: 2, - degraded: 2, forced_backfill: 2, forced_recovery: 2, peered: 2, @@ -36603,17 +40400,20 @@ Ext.define('PVE.ceph.StatusDetail', { snaptrim: 2, snaptrim_wait: 2, - // error - backfill_toofull: 3, - backfill_unfound: 3, - down: 3, - incomplete: 3, - inconsistent: 3, - recovery_toofull: 3, - recovery_unfound: 3, - snaptrim_error: 3, - stale: 3, + // warning + degraded: 3, undersized: 3, + + // critical + backfill_toofull: 4, + backfill_unfound: 4, + down: 4, + incomplete: 4, + inconsistent: 4, + recovery_toofull: 4, + recovery_unfound: 4, + snaptrim_error: 4, + stale: 4, }, statecategories: [ @@ -36628,11 +40428,15 @@ Ext.define('PVE.ceph.StatusDetail', { cls: 'good', }, { - text: gettext('Working'), + text: gettext('Busy'), + cls: 'pve-ceph-status-busy', + }, + { + text: gettext('Warning'), cls: 'warning', }, { - text: gettext('Error'), + text: gettext('Critical'), cls: 'critical', }, ], @@ -36857,15 +40661,19 @@ Ext.define('PVE.node.ACMEAccountCreate', { checkbox.setHidden(true); Proxmox.Utils.API2Request({ - url: '/cluster/acme/tos', + url: '/cluster/acme/meta', method: 'GET', params: { directory: value, }, success: function(response, opt) { - field.setValue(response.result.data); - disp.setValue(response.result.data); - checkbox.setHidden(false); + if (response.result.data.termsOfService) { + field.setValue(response.result.data.termsOfService); + disp.setValue(response.result.data.termsOfService); + checkbox.setHidden(false); + } else { + disp.setValue(undefined); + } }, failure: function(response, opt) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); @@ -37301,6 +41109,10 @@ Ext.define('PVE.node.ACME', { orderFinished: function(success) { if (!success) return; + // reload only if the Web UI is open on the same node that the cert was ordered for + if (this.getView().nodename !== Proxmox.NodeName) { + return; + } var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!'); Ext.getBody().mask(txt, ['pve-static-mask']); // reload after 10 seconds automatically @@ -37972,6 +41784,20 @@ Ext.define('PVE.node.CmdMenu', { }); }, }, + { + text: gettext('Bulk Suspend'), + itemId: 'bulksuspend', + iconCls: 'fa fa-fw fa-download', + handler: function() { + Ext.create('PVE.window.BulkAction', { + nodename: this.up('menu').nodename, + title: gettext('Bulk Suspend'), + btnText: gettext('Suspend'), + action: 'suspendall', + autoShow: true, + }); + }, + }, { text: gettext('Bulk Migrate'), itemId: 'bulkmigrate', @@ -38045,6 +41871,7 @@ Ext.define('PVE.node.CmdMenu', { if (!caps.vms['VM.PowerMgmt']) { me.getComponent('bulkstart').setDisabled(true); me.getComponent('bulkstop').setDisabled(true); + me.getComponent('bulksuspend').setDisabled(true); } if (!caps.nodes['Sys.PowerMgmt']) { me.getComponent('wakeonlan').setDisabled(true); @@ -38055,6 +41882,10 @@ Ext.define('PVE.node.CmdMenu', { if (me.pveSelNode.data.running) { me.getComponent('wakeonlan').setDisabled(true); } + + if (PVE.Utils.isStandaloneNode()) { + me.getComponent('bulkmigrate').setVisible(false); + } }, }); Ext.define('PVE.node.Config', { @@ -38124,10 +41955,25 @@ Ext.define('PVE.node.Config', { }); }, }, + { + text: gettext('Bulk Suspend'), + iconCls: 'fa fa-fw fa-download', + disabled: !caps.vms['VM.PowerMgmt'], + handler: function() { + Ext.create('PVE.window.BulkAction', { + autoShow: true, + nodename: nodename, + title: gettext('Bulk Suspend'), + btnText: gettext('Suspend'), + action: 'suspendall', + }); + }, + }, { text: gettext('Bulk Migrate'), iconCls: 'fa fa-fw fa-send-o', disabled: !caps.vms['VM.Migrate'], + hidden: PVE.Utils.isStandaloneNode(), handler: function() { Ext.create('PVE.window.BulkAction', { autoShow: true, @@ -38432,7 +42278,7 @@ Ext.define('PVE.node.Config', { }, { xtype: 'pveNodeCephPoolList', - title: 'Pools', + title: gettext('Pools'), iconCls: 'fa fa-sitemap', groups: ['ceph'], itemId: 'ceph-pools', @@ -38456,6 +42302,8 @@ Ext.define('PVE.node.Config', { onlineHelp: 'chapter_pve_firewall', url: '/api2/extjs/nodes/' + nodename + '/firewall/log', itemId: 'firewall-fwlog', + log_select_timespan: true, + submitFormat: 'U', }, { xtype: 'cephLogView', @@ -38479,7 +42327,7 @@ Ext.define('PVE.node.Config', { extraFilter: [ { xtype: 'pveGuestIDSelector', - fieldLabel: gettext('VMID'), + fieldLabel: 'VMID', allowBlank: true, name: 'vmid', }, @@ -39312,7 +43160,7 @@ Ext.define('PVE.node.StatusView', { extend: 'Proxmox.panel.StatusView', alias: 'widget.pveNodeStatus', - height: 300, + height: 350, bodyPadding: '15 5 15 5', layout: { @@ -39408,18 +43256,42 @@ Ext.define('PVE.node.StatusView', { value: '', }, { - itemId: 'kversion', colspan: 2, title: gettext('Kernel Version'), printBar: false, - textField: 'kversion', + // TODO: remove with next major and only use newish current-kernel textfield + multiField: true, + //textField: 'current-kernel', + renderer: ({ data }) => { + if (!data['current-kernel']) { + return data.kversion; + } + let kernel = data['current-kernel']; + let buildDate = kernel.version.match(/\((.+)\)\s*$/)[1] ?? 'unknown'; + return `${kernel.sysname} ${kernel.release} (${buildDate})`; + }, + value: '', + }, + { + colspan: 2, + title: gettext('Boot Mode'), + printBar: false, + textField: 'boot-info', + renderer: boot => { + if (boot.mode === 'legacy-bios') { + return 'Legacy BIOS'; + } else if (boot.mode === 'efi') { + return `EFI${boot.secureboot ? ' (Secure Boot)' : ''}`; + } + return Proxmox.Utils.unknownText; + }, value: '', }, { itemId: 'version', colspan: 2, printBar: false, - title: gettext('PVE Manager Version'), + title: gettext('Manager Version'), textField: 'pveversion', value: '', }, @@ -39454,14 +43326,21 @@ Ext.define('PVE.node.StatusView', { }); Ext.define('PVE.node.SubscriptionKeyEdit', { extend: 'Proxmox.window.Edit', + title: gettext('Upload Subscription Key'), - width: 300, + width: 350, + items: { xtype: 'textfield', name: 'key', value: '', fieldLabel: gettext('Subscription Key'), + labelWidth: 120, + getSubmitValue: function() { + return this.processRawValue(this.getRawValue())?.trim(); + }, }, + initComponent: function() { var me = this; @@ -39555,21 +43434,7 @@ Ext.define('PVE.node.Subscription', { throw "no node name specified"; } - var reload = function() { - me.rstore.load(); - }; - - var baseurl = '/nodes/' + me.nodename + '/subscription'; - - var render_status = function(value) { - var message = me.getObjectValue('message'); - if (message) { - return value + ": " + message; - } - return value; - }; - - var rows = { + let rows = { productname: { header: gettext('Type'), }, @@ -39578,7 +43443,10 @@ Ext.define('PVE.node.Subscription', { }, status: { header: gettext('Status'), - renderer: render_status, + renderer: v => { + let message = me.getObjectValue('message'); + return message ? `${v}: ${message}` : v; + }, }, message: { visible: false, @@ -39598,53 +43466,43 @@ Ext.define('PVE.node.Subscription', { }, signature: { header: gettext('Signed/Offline'), - renderer: (value) => { - if (value) { - return gettext('Yes'); - } else { - return gettext('No'); - } - }, + renderer: v => v ? gettext('Yes') : gettext('No'), }, }; Ext.apply(me, { - url: '/api2/json' + baseurl, + url: `/api2/json/nodes/${me.nodename}/subscription`, cwidth1: 170, tbar: [ { text: gettext('Upload Subscription Key'), - handler: function() { - var win = Ext.create('PVE.node.SubscriptionKeyEdit', { - url: '/api2/extjs/' + baseurl, - }); - win.show(); - win.on('destroy', reload); - }, + handler: () => Ext.create('PVE.node.SubscriptionKeyEdit', { + autoShow: true, + url: `/api2/extjs/nodes/${me.nodename}/subscription`, + listeners: { + destroy: () => me.rstore.load(), + }, + }), }, { text: gettext('Check'), - handler: function() { - Proxmox.Utils.API2Request({ - params: { force: 1 }, - url: baseurl, - method: 'POST', - waitMsgTarget: me, - failure: function(response, opts) { - Ext.Msg.alert(gettext('Error'), response.htmlStatus); - }, - callback: reload, - }); - }, + handler: () => Proxmox.Utils.API2Request({ + params: { force: 1 }, + url: `/nodes/${me.nodename}/subscription`, + method: 'POST', + waitMsgTarget: me, + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + callback: () => me.rstore.load(), + }), }, { text: gettext('Remove Subscription'), xtype: 'proxmoxStdRemoveButton', confirmMsg: gettext('Are you sure you want to remove the subscription key?'), - baseurl: baseurl, + baseurl: `/nodes/${me.nodename}/subscription`, dangerous: true, selModel: false, - callback: reload, + callback: () => me.rstore.load(), }, '-', { @@ -39656,7 +43514,7 @@ Ext.define('PVE.node.Subscription', { ], rows: rows, listeners: { - activate: reload, + activate: () => me.rstore.load(), }, }); @@ -39815,7 +43673,7 @@ Ext.define('PVE.node.Summary', { layout: 'column', minWidth: 700, defaults: { - minHeight: 325, + minHeight: 350, padding: 5, columnWidth: 1, }, @@ -40426,7 +44284,7 @@ Ext.define('PVE.pool.StatusView', { }; Ext.apply(me, { - url: "/api2/json/pools/" + pool, + url: "/api2/json/pools/?poolid=" + pool, rows: rows, }); @@ -41075,11 +44933,7 @@ Ext.define('PVE.qemu.CDInputPanel', { values.mediaType = 'none'; } else { values.mediaType = 'iso'; - var match = drive.file.match(/^([^:]+):/); - if (match) { - values.cdstorage = match[1]; - values.cdimage = drive.file; - } + values.cdimage = drive.file; } me.drive = drive; @@ -41090,8 +44944,7 @@ Ext.define('PVE.qemu.CDInputPanel', { setNodename: function(nodename) { var me = this; - me.cdstoragesel.setNodename(nodename); - me.cdfilesel.setStorage(undefined, nodename); + me.isosel.setNodename(nodename); }, initComponent: function() { @@ -41119,8 +44972,7 @@ Ext.define('PVE.qemu.CDInputPanel', { if (!me.rendered) { return; } - me.down('field[name=cdstorage]').setDisabled(!value); - var cdImageField = me.down('field[name=cdimage]'); + var cdImageField = me.down('pveIsoSelector'); cdImageField.setDisabled(!value); if (value) { cdImageField.validate(); @@ -41131,32 +44983,14 @@ Ext.define('PVE.qemu.CDInputPanel', { }, }); - me.cdfilesel = Ext.create('PVE.form.FileSelector', { - name: 'cdimage', - nodename: me.nodename, - storageContent: 'iso', - fieldLabel: gettext('ISO image'), - labelAlign: 'right', - allowBlank: false, - }); - me.cdstoragesel = Ext.create('PVE.form.StorageSelector', { - name: 'cdstorage', + me.isosel = Ext.create('PVE.form.IsoSelector', { nodename: me.nodename, - fieldLabel: gettext('Storage'), - labelAlign: 'right', - storageContent: 'iso', - allowBlank: false, - autoSelect: me.insideWizard, - listeners: { - change: function(f, value) { - me.cdfilesel.setStorage(value); - }, - }, + insideWizard: me.insideWizard, + name: 'cdimage', }); - items.push(me.cdstoragesel); - items.push(me.cdfilesel); + items.push(me.isosel); items.push({ xtype: 'radiofield', @@ -41331,8 +45165,8 @@ Ext.define('PVE.qemu.CloudInit', { enableFn: function(record) { let view = this.up('grid'); var caps = Ext.state.Manager.get('GuiCap'); - if (view.rows[record.data.key].never_delete || - !caps.vms['VM.Config.Network']) { + let caps_ci = caps.vms['VM.Config.Network'] || caps.vms['VM.Config.Cloudinit']; + if (view.rows[record.data.key].never_delete || !caps_ci) { return false; } @@ -41523,7 +45357,7 @@ Ext.define('PVE.qemu.CloudInit', { ], } : undefined, renderer: function(value) { - return value || Proxmox.Utils.defaultText; + return Ext.String.htmlEncode(value || Proxmox.Utils.defaultText); }, }, cipassword: { @@ -41545,20 +45379,20 @@ Ext.define('PVE.qemu.CloudInit', { ], } : undefined, renderer: function(value) { - return value || Proxmox.Utils.noneText; + return Ext.String.htmlEncode(value || Proxmox.Utils.noneText); }, }, searchdomain: { header: gettext('DNS domain'), iconCls: 'fa fa-globe', - editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + editor: caps_ci ? 'PVE.lxc.DNSEdit' : undefined, never_delete: true, defaultValue: gettext('use host settings'), }, nameserver: { header: gettext('DNS servers'), iconCls: 'fa fa-globe', - editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + editor: caps_ci ? 'PVE.lxc.DNSEdit' : undefined, never_delete: true, defaultValue: gettext('use host settings'), }, @@ -41595,6 +45429,24 @@ Ext.define('PVE.qemu.CloudInit', { }, defaultValue: '', }, + ciupgrade: { + header: gettext('Upgrade packages'), + iconCls: 'fa fa-archive', + renderer: Proxmox.Utils.format_boolean, + defaultValue: 1, + editor: { + xtype: 'proxmoxWindowEdit', + subject: gettext('Upgrade packages on boot'), + items: { + xtype: 'proxmoxcheckbox', + name: 'ciupgrade', + uncheckedValue: 0, + value: 1, // serves as default value, using defaultValue is not enough + fieldLabel: gettext('Upgrade packages'), + labelWidth: 140, + }, + }, + }, }; var i; var ipconfig_renderer = function(value, md, record, ri, ci, store, pending) { @@ -41612,7 +45464,7 @@ Ext.define('PVE.qemu.CloudInit', { me.rows['net' + i.toString()] = { multiKey: ['ipconfig' + i.toString(), 'net' + i.toString()], header: gettext('IP Config') + ' (net' + i.toString() +')', - editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.IPConfigEdit' : undefined, + editor: caps_ci ? 'PVE.qemu.IPConfigEdit' : undefined, iconCls: 'fa fa-exchange', renderer: ipconfig_renderer, }; @@ -41664,7 +45516,7 @@ Ext.define('PVE.qemu.CmdMenu', { }; let caps = Ext.state.Manager.get('GuiCap'); - let standalone = PVE.data.ResourceStore.getNodes().length < 2; + let standalone = PVE.Utils.isStandaloneNode(); let running = false, stopped = true, suspended = false; switch (info.status) { @@ -41871,7 +45723,7 @@ Ext.define('PVE.qemu.Config', { var migrateBtn = Ext.create('Ext.Button', { text: gettext('Migrate'), disabled: !caps.vms['VM.Migrate'], - hidden: PVE.data.ResourceStore.getNodes().length < 2, + hidden: PVE.Utils.isStandaloneNode(), handler: function() { var win = Ext.create('PVE.window.Migrate', { vmtype: 'qemu', @@ -42143,7 +45995,7 @@ Ext.define('PVE.qemu.Config', { }); } - if (caps.vms['VM.Console']) { + if (caps.vms['VM.Audit']) { me.items.push( { xtype: 'pveFirewallRules', @@ -42181,7 +46033,12 @@ Ext.define('PVE.qemu.Config', { list_refs_url: base_url + '/firewall/refs', itemId: 'firewall-ipset', }, - { + ); + } + + if (caps.vms['VM.Console']) { + me.items.push( + { title: gettext('Log'), groups: ['firewall'], iconCls: 'fa fa-list', @@ -42189,6 +46046,8 @@ Ext.define('PVE.qemu.Config', { itemId: 'firewall-fwlog', xtype: 'proxmoxLogView', url: '/api2/extjs' + base_url + '/firewall/log', + log_select_timespan: true, + submitFormat: 'U', }, ); } @@ -42308,6 +46167,41 @@ Ext.define('PVE.qemu.CreateWizard', { subject: gettext('Virtual Machine'), + // fot the special case that we have 2 cdrom drives + // + // emulates part of the backend bootorder logic, but includes all + // cdrom drives since we don't know which one the user put in a bootable iso + // and hardcodes the known values (ide0/2, net0) + calculateBootOrder: function(values) { + // user selected windows + second cdrom + if (values.ide0 && values.ide0.match(/media=cdrom/)) { + let disk; + PVE.Utils.forEachBus(['ide', 'scsi', 'virtio', 'sata'], (type, id) => { + let confId = type + id; + if (!values[confId]) { + return undefined; + } + if (values[confId].match(/media=cdrom/)) { + return undefined; + } + disk = confId; + return false; // abort loop + }); + + let order = []; + if (disk) { + order.push(disk); + } + order.push('ide0', 'ide2'); + if (values.net0) { + order.push('net0'); + } + + return `order=${order.join(';')}`; + } + return undefined; + }, + items: [ { xtype: 'inputpanel', @@ -42390,6 +46284,15 @@ Ext.define('PVE.qemu.CreateWizard', { fieldLabel: gettext('Shutdown timeout'), }, ], + + advancedColumnB: [ + { + xtype: 'pveTagFieldSet', + name: 'tags', + maxHeight: 150, + }, + ], + onGetValues: function(values) { ['name', 'pool', 'onboot', 'agent'].forEach(function(field) { if (!values[field]) { @@ -42434,6 +46337,9 @@ Ext.define('PVE.qemu.CreateWizard', { { xtype: 'pveQemuOSTypePanel', insideWizard: true, + bind: { + nodename: '{nodename}', + }, }, ], }, @@ -42498,8 +46404,15 @@ Ext.define('PVE.qemu.CreateWizard', { ], listeners: { show: function(panel) { - var kv = this.up('window').getValues(); + let wizard = this.up('window'); + var kv = wizard.getValues(); var data = []; + + let boot = wizard.calculateBootOrder(kv); + if (boot) { + kv.boot = boot; + } + Ext.Object.each(kv, function(key, value) { if (key === 'delete') { // ignore return; @@ -42524,6 +46437,11 @@ Ext.define('PVE.qemu.CreateWizard', { var nodename = kv.nodename; delete kv.nodename; + let boot = wizard.calculateBootOrder(kv); + if (boot) { + kv.boot = boot; + } + Proxmox.Utils.API2Request({ url: '/nodes/' + nodename + '/qemu', waitMsgTarget: wizard, @@ -43483,6 +47401,10 @@ Ext.define('PVE.window.HDResize', { Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, success: function(response, options) { + Ext.create('Proxmox.window.TaskProgress', { + autoShow: true, + upid: response.result.data, + }); me.close(); }, }); @@ -43516,7 +47438,7 @@ Ext.define('PVE.window.HDResize', { maxValue: 128*1024, decimalPrecision: 3, value: '0', - fieldLabel: gettext('Size Increment') + ' (GiB)', + fieldLabel: `${gettext('Size Increment')} (${gettext('GiB')})`, allowBlank: false, }); @@ -43822,8 +47744,8 @@ Ext.define('PVE.qemu.HardwareView', { group: 25, order: i, iconCls: 'usb', - editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined, - never_delete: !caps.nodes['Sys.Console'], + editor: caps.nodes['Sys.Console'] || caps.mapping['Mapping.Use'] ? 'PVE.qemu.USBEdit' : undefined, + never_delete: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], header: gettext('USB Device') + ' (' + confid + ')', }; } @@ -43833,8 +47755,8 @@ Ext.define('PVE.qemu.HardwareView', { group: 30, order: i, tdCls: 'pve-itype-icon-pci', - never_delete: !caps.nodes['Sys.Console'], - editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined, + never_delete: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], + editor: caps.nodes['Sys.Console'] || caps.mapping['Mapping.Use'] ? 'PVE.qemu.PCIEdit' : undefined, header: gettext('PCI Device') + ' (' + confid + ')', }; } @@ -44140,14 +48062,15 @@ Ext.define('PVE.qemu.HardwareView', { // heuristic only for disabling some stuff, the backend has the final word. const noSysConsolePerm = !caps.nodes['Sys.Console']; + const noHWPerm = !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use']; const noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType']; const noVMConfigNetPerm = !caps.vms['VM.Config.Network']; const noVMConfigDiskPerm = !caps.vms['VM.Config.Disk']; const noVMConfigCDROMPerm = !caps.vms['VM.Config.CDROM']; const noVMConfigCloudinitPerm = !caps.vms['VM.Config.Cloudinit']; - me.down('#addUsb').setDisabled(noSysConsolePerm || isAtUsbLimit()); - me.down('#addPci').setDisabled(noSysConsolePerm || isAtLimit('hostpci')); + me.down('#addUsb').setDisabled(noHWPerm || isAtUsbLimit()); + me.down('#addPci').setDisabled(noHWPerm || isAtLimit('hostpci')); me.down('#addAudio').setDisabled(noVMConfigHWTypePerm || isAtLimit('audio')); me.down('#addSerial').setDisabled(noVMConfigHWTypePerm || isAtLimit('serial')); me.down('#addNet').setDisabled(noVMConfigNetPerm || isAtLimit('net')); @@ -44260,14 +48183,14 @@ Ext.define('PVE.qemu.HardwareView', { text: gettext('USB Device'), itemId: 'addUsb', iconCls: 'fa fa-fw fa-usb black', - disabled: !caps.nodes['Sys.Console'], + disabled: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], handler: editorFactory('USBEdit'), }, { text: gettext('PCI Device'), itemId: 'addPci', iconCls: 'pve-itype-icon-pci', - disabled: !caps.nodes['Sys.Console'], + disabled: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], handler: editorFactory('PCIEdit'), }, { @@ -45123,11 +49046,17 @@ Ext.define('PVE.qemu.MultiHDPanel', { let me = this; let vm = me.getViewModel(); - return { + let res = { ide2: 'media=cdrom', scsihw: vm.get('current.scsihw'), ostype: vm.get('current.ostype'), }; + + if (vm.get('current.ide0') === "some") { + res.ide0 = "media=cdrom"; + } + + return res; }, diskSorter: { @@ -45462,6 +49391,7 @@ Ext.define('PVE.qemu.OSDefaults', { virtio: 1, }, scsihw: 'virtio-scsi-single', + cputype: 'x86-64-v2-AES', }; // virtio-net is in kernel since 2.6.25 @@ -45517,9 +49447,21 @@ Ext.define('PVE.qemu.OSTypeInputPanel', { afterrender: 'onOSTypeChange', change: 'onOSTypeChange', }, + 'checkbox[reference=enableSecondCD]': { + change: 'onSecondCDChange', + }, }, onOSBaseChange: function(field, value) { - this.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]); + let me = this; + me.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]); + if (me.getView().insideWizard) { + let isWindows = value === 'Microsoft Windows'; + let enableSecondCD = me.lookup('enableSecondCD'); + enableSecondCD.setVisible(isWindows); + if (!isWindows) { + enableSecondCD.setValue(false); + } + } }, onOSTypeChange: function(field) { var me = this, ostype = field.getValue(); @@ -45530,6 +49472,7 @@ Ext.define('PVE.qemu.OSTypeInputPanel', { me.setWidget('pveBusSelector', targetValues.busType); me.setWidget('pveNetworkCardSelector', targetValues.networkCard); + me.setWidget('CPUModelSelector', targetValues.cputype); var scsihw = targetValues.scsihw || '__default__'; this.getViewModel().set('current.scsihw', scsihw); this.getViewModel().set('current.ostype', ostype); @@ -45544,6 +49487,48 @@ Ext.define('PVE.qemu.OSTypeInputPanel', { // ignore multiple disks, we only want to set the type if there is a single disk } }, + onSecondCDChange: function(widget, value, lastValue) { + let me = this; + let vm = me.getViewModel(); + let updateVMConfig = function() { + let widgets = Ext.ComponentQuery.query('pveMultiHDPanel'); + if (widgets.length === 1) { + widgets[0].getController().updateVMConfig(); + } + }; + if (value) { + // only for windows + vm.set('current.ide0', "some"); + vm.notify(); + updateVMConfig(); + me.setWidget('pveBusSelector', 'scsi'); + me.setWidget('pveNetworkCardSelector', 'virtio'); + } else { + vm.set('current.ide0', ""); + vm.notify(); + updateVMConfig(); + me.setWidget('pveBusSelector', 'scsi'); + let ostype = me.lookup('ostype').getValue(); + var targetValues = PVE.qemu.OSDefaults.getDefaults(ostype); + me.setWidget('pveBusSelector', targetValues.busType); + } + }, + }, + + setNodename: function(nodename) { + var me = this; + me.lookup('isoSelector').setNodename(nodename); + }, + + onGetValues: function(values) { + if (values.ide0) { + let drive = { + media: 'cdrom', + file: values.ide0, + }; + values.ide0 = PVE.Parser.printQemuDrive(drive); + } + return values; }, initComponent: function() { @@ -45594,6 +49579,34 @@ Ext.define('PVE.qemu.OSTypeInputPanel', { }, ]; + if (me.insideWizard) { + me.items.push( + { + xtype: 'proxmoxcheckbox', + reference: 'enableSecondCD', + isFormField: false, + hidden: true, + checked: false, + boxLabel: gettext('Add additional drive for VirtIO drivers'), + listeners: { + change: function(cb, value) { + me.lookup('isoSelector').setDisabled(!value); + me.lookup('isoSelector').setHidden(!value); + }, + }, + }, + { + xtype: 'pveIsoSelector', + reference: 'isoSelector', + name: 'ide0', + nodename: me.nodename, + insideWizard: true, + hidden: true, + disabled: true, + }, + ); + } + me.callParent(); }, }); @@ -46023,71 +50036,155 @@ Ext.define('PVE.qemu.PCIInputPanel', { onlineHelp: 'qm_pci_passthrough_vm_config', - setVMConfig: function(vmconfig) { - var me = this; - me.vmconfig = vmconfig; + controller: { + xclass: 'Ext.app.ViewController', - var hostpci = me.vmconfig[me.confid] || ''; + setVMConfig: function(vmconfig) { + let me = this; + let view = me.getView(); + me.vmconfig = vmconfig; + + let hostpci = me.vmconfig[view.confid] || ''; - var values = PVE.Parser.parsePropertyString(hostpci, 'host'); - if (values.host) { - if (!values.host.match(/^[0-9a-f]{4}:/i)) { // add optional domain - values.host = "0000:" + values.host; + let values = PVE.Parser.parsePropertyString(hostpci, 'host'); + if (values.host) { + if (!values.host.match(/^[0-9a-f]{4}:/i)) { // add optional domain + values.host = "0000:" + values.host; + } + if (values.host.length < 11) { // 0000:00:00 format not 0000:00:00.0 + values.host += ".0"; + values.multifunction = true; + } + values.type = 'raw'; + } else if (values.mapping) { + values.type = 'mapped'; } - if (values.host.length < 11) { // 0000:00:00 format not 0000:00:00.0 - values.host += ".0"; - values.multifunction = true; + + values['x-vga'] = PVE.Parser.parseBoolean(values['x-vga'], 0); + values.pcie = PVE.Parser.parseBoolean(values.pcie, 0); + values.rombar = PVE.Parser.parseBoolean(values.rombar, 1); + + view.setValues(values); + if (!me.vmconfig.machine || me.vmconfig.machine.indexOf('q35') === -1) { + // machine is not set to some variant of q35, so we disable pcie + let pcie = me.lookup('pcie'); + pcie.setDisabled(true); + pcie.setBoxLabel(gettext('Q35 only')); } - } - values['x-vga'] = PVE.Parser.parseBoolean(values['x-vga'], 0); - values.pcie = PVE.Parser.parseBoolean(values.pcie, 0); - values.rombar = PVE.Parser.parseBoolean(values.rombar, 1); + if (values.romfile) { + me.lookup('romfile').setVisible(true); + } + }, - me.setValues(values); - if (!me.vmconfig.machine || me.vmconfig.machine.indexOf('q35') === -1) { - // machine is not set to some variant of q35, so we disable pcie - var pcie = me.down('field[name=pcie]'); - pcie.setDisabled(true); - pcie.setBoxLabel(gettext('Q35 only')); - } + selectorEnable: function(selector) { + let me = this; + me.pciDevChange(selector, selector.getValue()); + }, - if (values.romfile) { - me.down('field[name=romfile]').setVisible(true); - } - }, + pciDevChange: function(pcisel, value) { + let me = this; + let mdevfield = me.lookup('mdev'); + if (!value) { + if (!pcisel.isDisabled()) { + mdevfield.setDisabled(true); + } + return; + } + let pciDev = pcisel.getStore().getById(value); - onGetValues: function(values) { - let me = this; - if (!me.confid) { - for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) { - if (!me.vmconfig['hostpci' + i.toString()]) { - me.confid = 'hostpci' + i.toString(); - break; + mdevfield.setDisabled(!pciDev || !pciDev.data.mdev); + if (!pciDev) { + return; + } + + let path = value; + if (pciDev.data.map) { + // find local mapping + for (const entry of pciDev.data.map) { + let mapping = PVE.Parser.parsePropertyString(entry); + if (mapping.node === pcisel.up('inputpanel').nodename) { + path = mapping.path.split(';')[0]; + break; + } + } + if (path.indexOf('.') === -1) { + path += '.0'; } } - // FIXME: what if no confid was found?? - } - values.host.replace(/^0000:/, ''); // remove optional '0000' domain - if (values.multifunction) { - values.host = values.host.substring(0, values.host.indexOf('.')); // skip the '.X' - delete values.multifunction; - } + if (pciDev.data.mdev) { + mdevfield.setPciID(path); + } + if (pcisel.reference === 'selector') { + let iommu = pciDev.data.iommugroup; + if (iommu === -1) { + return; + } + // try to find out if there are more devices in that iommu group + let id = path.substring(0, 5); // 00:00 + let count = 0; + pcisel.getStore().each(({ data }) => { + if (data.iommugroup === iommu && data.id.substring(0, 5) !== id) { + count++; + return false; + } + return true; + }); + me.lookup('group_warning').setVisible(count > 0); + } + }, - if (values.rombar) { - delete values.rombar; - } else { - values.rombar = 0; - } + onGetValues: function(values) { + let me = this; + let view = me.getView(); + if (!view.confid) { + for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) { + if (!me.vmconfig['hostpci' + i.toString()]) { + view.confid = 'hostpci' + i.toString(); + break; + } + } + // FIXME: what if no confid was found?? + } - if (!values.romfile) { - delete values.romfile; - } + values.host?.replace(/^0000:/, ''); // remove optional '0000' domain + + if (values.multifunction && values.host) { + values.host = values.host.substring(0, values.host.indexOf('.')); // skip the '.X' + delete values.multifunction; + } + + if (values.rombar) { + delete values.rombar; + } else { + values.rombar = 0; + } + + if (!values.romfile) { + delete values.romfile; + } + + delete values.type; - let ret = {}; - ret[me.confid] = PVE.Parser.printPropertyString(values, 'host'); - return ret; + let ret = {}; + ret[view.confid] = PVE.Parser.printPropertyString(values, 'host'); + return ret; + }, + }, + + viewModel: { + data: { + isMapped: true, + }, + }, + + setVMConfig: function(vmconfig) { + return this.getController().setVMConfig(vmconfig); + }, + + onGetValues: function(values) { + return this.getController().onGetValues(values); }, initComponent: function() { @@ -46098,78 +50195,97 @@ Ext.define('PVE.qemu.PCIInputPanel', { throw "no node name specified"; } + me.columnT = [ + { + xtype: 'displayfield', + reference: 'iommu_warning', + hidden: true, + columnWidth: 1, + padding: '0 0 10 0', + value: 'No IOMMU detected, please activate it.' + + 'See Documentation for further information.', + userCls: 'pmx-hint', + }, + { + xtype: 'displayfield', + reference: 'group_warning', + hidden: true, + columnWidth: 1, + padding: '0 0 10 0', + itemId: 'iommuwarning', + value: 'The selected Device is not in a seperate IOMMU group, make sure this is intended.', + userCls: 'pmx-hint', + }, + ]; + me.column1 = [ + { + xtype: 'radiofield', + name: 'type', + inputValue: 'mapped', + boxLabel: gettext('Mapped Device'), + bind: { + value: '{isMapped}', + }, + }, + { + xtype: 'pvePCIMapSelector', + fieldLabel: gettext('Device'), + reference: 'mapped_selector', + name: 'mapping', + labelAlign: 'right', + nodename: me.nodename, + allowBlank: false, + bind: { + disabled: '{!isMapped}', + }, + listeners: { + change: 'pciDevChange', + enable: 'selectorEnable', + }, + }, + { + xtype: 'radiofield', + name: 'type', + inputValue: 'raw', + checked: true, + boxLabel: gettext('Raw Device'), + }, { xtype: 'pvePCISelector', fieldLabel: gettext('Device'), name: 'host', + reference: 'selector', nodename: me.nodename, + labelAlign: 'right', allowBlank: false, + disabled: true, + bind: { + disabled: '{isMapped}', + }, onLoadCallBack: function(store, records, success) { if (!success || !records.length) { return; } - if (records.every((val) => val.data.iommugroup === -1)) { // no IOMMU groups - let warning = Ext.create('Ext.form.field.Display', { - columnWidth: 1, - padding: '0 0 10 0', - value: 'No IOMMU detected, please activate it.' + - 'See Documentation for further information.', - userCls: 'pmx-hint', - }); - me.items.insert(0, warning); - me.updateLayout(); // insert does not trigger that - } + me.lookup('iommu_warning').setVisible( + records.every((val) => val.data.iommugroup === -1), + ); }, listeners: { - change: function(pcisel, value) { - if (!value) { - return; - } - let pciDev = pcisel.getStore().getById(value); - let mdevfield = me.down('field[name=mdev]'); - mdevfield.setDisabled(!pciDev || !pciDev.data.mdev); - if (!pciDev) { - return; - } - if (pciDev.data.mdev) { - mdevfield.setPciID(value); - } - let iommu = pciDev.data.iommugroup; - if (iommu === -1) { - return; - } - // try to find out if there are more devices in that iommu group - let id = pciDev.data.id.substring(0, 5); // 00:00 - let count = 0; - pcisel.getStore().each(({ data }) => { - if (data.iommugroup === iommu && data.id.substring(0, 5) !== id) { - count++; - return false; - } - return true; - }); - let warning = me.down('#iommuwarning'); - if (count && !warning) { - warning = Ext.create('Ext.form.field.Display', { - columnWidth: 1, - padding: '0 0 10 0', - itemId: 'iommuwarning', - value: 'The selected Device is not in a seperate IOMMU group, make sure this is intended.', - userCls: 'pmx-hint', - }); - me.items.insert(0, warning); - me.updateLayout(); // insert does not trigger that - } else if (!count && warning) { - me.remove(warning); - } - }, + change: 'pciDevChange', + enable: 'selectorEnable', }, }, { xtype: 'proxmoxcheckbox', fieldLabel: gettext('All Functions'), + reference: 'all_functions', + disabled: true, + labelAlign: 'right', name: 'multifunction', + bind: { + disabled: '{isMapped}', + }, }, ]; @@ -46177,6 +50293,7 @@ Ext.define('PVE.qemu.PCIInputPanel', { { xtype: 'pveMDevSelector', name: 'mdev', + reference: 'mdev', disabled: true, fieldLabel: gettext('MDev Type'), nodename: me.nodename, @@ -46208,6 +50325,7 @@ Ext.define('PVE.qemu.PCIInputPanel', { submitValue: true, hidden: true, fieldLabel: 'ROM-File', + reference: 'romfile', name: 'romfile', }, { @@ -46234,6 +50352,7 @@ Ext.define('PVE.qemu.PCIInputPanel', { { xtype: 'proxmoxcheckbox', fieldLabel: 'PCI-Express', + reference: 'pcie', name: 'pcie', }, { @@ -47252,6 +51371,15 @@ Ext.define('PVE.qemu.USBInputPanel', { autoComplete: false, onlineHelp: 'qm_usb_passthrough', + cbindData: function(initialConfig) { + let me = this; + if (!me.pveSelNode) { + throw "no pveSelNode given"; + } + + return { nodename: me.pveSelNode.data.node }; + }, + viewModel: { data: {}, }, @@ -47283,6 +51411,10 @@ Ext.define('PVE.qemu.USBInputPanel', { case 'spice': val = 'spice'; break; + case 'mapped': + val = `mapping=${values[type]}`; + delete values.mapped; + break; case 'hostdevice': case 'port': val = 'host=' + values[type]; @@ -47313,6 +51445,23 @@ Ext.define('PVE.qemu.USBInputPanel', { submitValue: false, checked: true, }, + { + name: 'usb', + inputValue: 'mapped', + boxLabel: gettext('Use mapped Device'), + reference: 'mapped', + submitValue: false, + }, + { + xtype: 'pveUSBMapSelector', + disabled: true, + name: 'mapped', + cbind: { nodename: '{nodename}' }, + bind: { disabled: '{!mapped.checked}' }, + allowBlank: false, + fieldLabel: gettext('Choose Device'), + labelAlign: 'right', + }, { name: 'usb', inputValue: 'hostdevice', @@ -47396,30 +51545,33 @@ Ext.define('PVE.qemu.USBEdit', { return; } - var data = response.result.data[me.confid].split(','); - var port, hostdevice, usb3 = false; - var type = 'spice'; - - for (let i = 0; i < data.length; i++) { - if (/^(host=)?(0x)?[a-zA-Z0-9]{4}:(0x)?[a-zA-Z0-9]{4}$/.test(data[i])) { - hostdevice = data[i]; - hostdevice = hostdevice.replace('host=', '').replace('0x', ''); - type = 'hostdevice'; - } else if (/^(host=)?(\d+)-(\d+(\.\d+)*)$/.test(data[i])) { - port = data[i]; - port = port.replace('host=', ''); - type = 'port'; - } + let data = PVE.Parser.parsePropertyString(response.result.data[me.confid], 'host'); + let port, hostdevice, mapped, usb3 = false; + let usb; - if (/^usb3=(1|on|true)$/.test(data[i])) { - usb3 = true; + if (data.host) { + if (/^(0x)?[a-zA-Z0-9]{4}:(0x)?[a-zA-Z0-9]{4}$/.test(data.host)) { + hostdevice = data.host.replace('0x', ''); + usb = 'hostdevice'; + } else if (/^(\d+)-(\d+(\.\d+)*)$/.test(data.host)) { + port = data.host; + usb = 'port'; + } else if (/^spice$/i.test(data.host)) { + usb = 'spice'; } + } else if (data.mapping) { + mapped = data.mapping; + usb = 'mapped'; } + + usb3 = data.usb3 ?? false; + var values = { - usb: type, - hostdevice: hostdevice, - port: port, - usb3: usb3, + usb, + hostdevice, + port, + usb3, + mapped, }; ipanel.setValues(values); @@ -47454,14 +51606,15 @@ Ext.define('PVE.sdn.Browser', { const caps = Ext.state.Manager.get('GuiCap'); - if (caps.sdn['SDN.Audit']) { - me.items.push({ - xtype: 'pveSDNZoneContentView', - title: gettext('Content'), - iconCls: 'fa fa-th', - itemId: 'content', - }); - } + me.items.push({ + nodename: nodename, + zone: sdnId, + xtype: 'pveSDNZoneContentPanel', + title: gettext('Content'), + iconCls: 'fa fa-th', + itemId: 'content', + }); + if (caps.sdn['Permissions.Modify']) { me.items.push({ xtype: 'pveACLView', @@ -47759,10 +51912,6 @@ Ext.define('PVE.sdn.VnetInputPanel', { values.type = 'vnet'; } - if (!values.vlanaware) { - delete values.vlanaware; - } - return values; }, @@ -47779,10 +51928,14 @@ Ext.define('PVE.sdn.VnetInputPanel', { fieldLabel: gettext('Name'), }, { - xtype: 'textfield', + xtype: 'proxmoxtextfield', name: 'alias', fieldLabel: gettext('Alias'), allowBlank: true, + skipEmptyText: true, + cbind: { + deleteEmpty: "{!isCreate}", + }, }, { xtype: 'pveSDNZoneSelector', @@ -47798,13 +51951,19 @@ Ext.define('PVE.sdn.VnetInputPanel', { maxValue: 16777216, fieldLabel: gettext('Tag'), allowBlank: true, + cbind: { + deleteEmpty: "{!isCreate}", + }, }, { xtype: 'proxmoxcheckbox', name: 'vlanaware', - uncheckedValue: 0, + uncheckedValue: null, checked: false, fieldLabel: gettext('VLAN Aware'), + cbind: { + deleteEmpty: "{!isCreate}", + }, }, ], }); @@ -48006,11 +52165,300 @@ Ext.define('PVE.sdn.VnetView', { me.callParent(); }, }); +Ext.define('PVE.sdn.VnetACLAdd', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveSDNVnetACLAdd'], + + url: '/access/acl', + method: 'PUT', + isAdd: true, + isCreate: true, + + width: 400, + initComponent: function() { + let me = this; + + let items = [ + { + xtype: 'hiddenfield', + name: 'path', + value: me.path, + allowBlank: false, + fieldLabel: gettext('Path'), + }, + ]; + + if (me.aclType === 'group') { + me.subject = gettext("Group Permission"); + items.push({ + xtype: 'pveGroupSelector', + name: 'groups', + fieldLabel: gettext('Group'), + }); + } else if (me.aclType === 'user') { + me.subject = gettext("User Permission"); + items.push({ + xtype: 'pmxUserSelector', + name: 'users', + fieldLabel: gettext('User'), + }); + } else if (me.aclType === 'token') { + me.subject = gettext("API Token Permission"); + items.push({ + xtype: 'pveTokenSelector', + name: 'tokens', + fieldLabel: gettext('API Token'), + }); + } else { + throw "unknown ACL type"; + } + + items.push({ + xtype: 'pmxRoleSelector', + name: 'roles', + value: 'NoAccess', + fieldLabel: gettext('Role'), + }); + + items.push({ + xtype: 'proxmoxintegerfield', + name: 'vlan', + minValue: 1, + maxValue: 4096, + allowBlank: true, + fieldLabel: 'VLAN', + emptyText: gettext('All'), + }); + + let ipanel = Ext.create('Proxmox.panel.InputPanel', { + items: items, + onlineHelp: 'pveum_permission_management', + onGetValues: function(values) { + if (values.vlan) { + values.path = values.path + "/" + values.vlan; + delete values.vlan; + } + return values; + }, + }); + + Ext.apply(me, { + items: [ipanel], + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.sdn.VnetACLView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveSDNVnetACLView'], + + onlineHelp: 'chapter_user_management', + + stateful: true, + stateId: 'grid-acls', + + // use fixed path + path: undefined, + + setPath: function(path) { + let me = this; + + me.path = path; + + if (path === undefined) { + me.down('#groupmenu').setDisabled(true); + me.down('#usermenu').setDisabled(true); + me.down('#tokenmenu').setDisabled(true); + } else { + me.down('#groupmenu').setDisabled(false); + me.down('#usermenu').setDisabled(false); + me.down('#tokenmenu').setDisabled(false); + me.store.load(); + } + }, + initComponent: function() { + let me = this; + + let store = Ext.create('Ext.data.Store', { + model: 'pve-acl', + proxy: { + type: 'proxmox', + url: "/api2/json/access/acl", + }, + sorters: { + property: 'path', + direction: 'ASC', + }, + }); + + store.addFilter(Ext.create('Ext.util.Filter', { + filterFn: item => item.data.path.replace(/(\/sdn\/zones\/(.*)\/(.*))\/[0-9]*$/, '$1') === me.path, + })); + + let render_ugid = function(ugid, metaData, record) { + if (record.data.type === 'group') { + return '@' + ugid; + } + + return Ext.String.htmlEncode(ugid); + }; + + let render_vlan = function(path, metaData, record) { + let vlan = 'any'; + const match = path.match(/(\/sdn\/zones\/)(.*)\/(.*)\/([0-9]*)$/); + if (match) { + vlan = match[4]; + } + + return Ext.String.htmlEncode(vlan); + }; + + let columns = [ + { + header: gettext('User') + '/' + gettext('Group') + '/' + gettext('API Token'), + flex: 1, + sortable: true, + renderer: render_ugid, + dataIndex: 'ugid', + }, + { + header: gettext('Role'), + flex: 1, + sortable: true, + dataIndex: 'roleid', + }, + { + header: gettext('VLAN'), + flex: 1, + sortable: true, + renderer: render_vlan, + dataIndex: 'path', + }, + ]; + + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: gettext('Are you sure you want to remove this entry'), + handler: function(btn, event, rec) { + var params = { + 'delete': 1, + path: rec.data.path, + roles: rec.data.roleid, + }; + if (rec.data.type === 'group') { + params.groups = rec.data.ugid; + } else if (rec.data.type === 'user') { + params.users = rec.data.ugid; + } else if (rec.data.type === 'token') { + params.tokens = rec.data.ugid; + } else { + throw 'unknown data type'; + } + + Proxmox.Utils.API2Request({ + url: '/access/acl', + params: params, + method: 'PUT', + waitMsgTarget: me, + callback: () => store.load(), + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + }, + }); + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + menu: { + xtype: 'menu', + items: [ + { + text: gettext('Group Permission'), + disabled: !me.path, + itemId: 'groupmenu', + iconCls: 'fa fa-fw fa-group', + handler: function() { + var win = Ext.create('PVE.sdn.VnetACLAdd', { + aclType: 'group', + path: me.path, + }); + win.on('destroy', () => store.load()); + win.show(); + }, + }, + { + text: gettext('User Permission'), + disabled: !me.path, + itemId: 'usermenu', + iconCls: 'fa fa-fw fa-user', + handler: function() { + var win = Ext.create('PVE.sdn.VnetACLAdd', { + aclType: 'user', + path: me.path, + }); + win.on('destroy', () => store.load()); + win.show(); + }, + }, + { + text: gettext('API Token Permission'), + disabled: !me.path, + itemId: 'tokenmenu', + iconCls: 'fa fa-fw fa-user-o', + handler: function() { + let win = Ext.create('PVE.sdn.VnetACLAdd', { + aclType: 'token', + path: me.path, + }); + win.on('destroy', () => store.load()); + win.show(); + }, + }, + ], + }, + }, + remove_btn, + ], + viewConfig: { + trackOver: false, + }, + columns: columns, + listeners: { + }, + }); + + me.callParent(); + }, +}, function() { + Ext.define('pve-acl-vnet', { + extend: 'Ext.data.Model', + fields: [ + 'path', 'type', 'ugid', 'roleid', + { + name: 'propagate', + type: 'boolean', + }, + ], + }); +}); Ext.define('PVE.sdn.Vnet', { extend: 'Ext.panel.Panel', alias: 'widget.pveSDNVnet', - title: 'Vnet', + title: 'VNet', onlineHelp: 'pvesdn_config_vnet', @@ -48024,7 +52472,7 @@ Ext.define('PVE.sdn.Vnet', { }); var vnetview_panel = Ext.createWidget('pveSDNVnetView', { - title: 'Vnets', + title: 'VNets', region: 'west', subnetview_panel: subnetview_panel, width: '50%', @@ -48058,13 +52506,6 @@ Ext.define('PVE.sdn.SubnetInputPanel', { delete values.cidr; } - if (!values.gateway) { - delete values.gateway; - } - if (!values.snat) { - delete values.snat; - } - return values; }, @@ -48080,25 +52521,208 @@ Ext.define('PVE.sdn.SubnetInputPanel', { fieldLabel: gettext('Subnet'), }, { - xtype: 'textfield', + xtype: 'proxmoxtextfield', name: 'gateway', vtype: 'IP64Address', fieldLabel: gettext('Gateway'), allowBlank: true, + skipEmptyText: true, + cbind: { + deleteEmpty: "{!isCreate}", + }, }, { xtype: 'proxmoxcheckbox', name: 'snat', - uncheckedValue: 0, + uncheckedValue: null, checked: false, fieldLabel: 'SNAT', + cbind: { + deleteEmpty: "{!isCreate}", + }, }, { xtype: 'proxmoxtextfield', name: 'dnszoneprefix', skipEmptyText: true, - fieldLabel: gettext('DNS zone prefix'), + fieldLabel: gettext('DNS Zone Prefix'), allowBlank: true, + cbind: { + deleteEmpty: "{!isCreate}", + }, + }, + ], +}); + +Ext.define('PVE.sdn.SubnetDhcpRangePanel', { + extend: 'Ext.form.FieldContainer', + mixins: ['Ext.form.field.Field'], + + initComponent: function() { + let me = this; + + me.callParent(); + me.initField(); + }, + + // since value is an array of objects we need to override isEquals here + isEqual: function(value1, value2) { + return JSON.stringify(value1) === JSON.stringify(value2); + }, + + getValue: function() { + let me = this; + let store = me.lookup('grid').getStore(); + + let value = []; + + store.getData() + .each((item) => { + // needs a deep copy otherwise we run in to ExtJS reference + // shenaningans + value.push({ + 'start-address': item.data['start-address'], + 'end-address': item.data['end-address'], + }); + }); + + return value; + }, + + getSubmitData: function() { + let me = this; + + let data = {}; + + let value = me.getValue() + .map((item) => `start-address=${item['start-address']},end-address=${item['end-address']}`); + + if (value.length) { + data[me.getName()] = value; + } else if (!me.isCreate) { + data.delete = me.getName(); + } + + return data; + }, + + setValue: function(dhcpRanges) { + let me = this; + let store = me.lookup('grid').getStore(); + + let data = []; + + dhcpRanges.forEach((item) => { + // needs a deep copy otherwise we run in to ExtJS reference + // shenaningans + data.push({ + 'start-address': item['start-address'], + 'end-address': item['end-address'], + }); + }); + + store.setData(data); + }, + + getErrors: function() { + let me = this; + let errors = []; + + return errors; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + addRange: function() { + let me = this; + me.lookup('grid').getStore().add({}); + + me.getView().checkChange(); + }, + + removeRange: function(field) { + let me = this; + let record = field.getWidgetRecord(); + + me.lookup('grid').getStore().remove(record); + + me.getView().checkChange(); + }, + + onValueChange: function(field, value) { + let me = this; + let record = field.getWidgetRecord(); + let column = field.getWidgetColumn(); + + record.set(column.dataIndex, value); + record.commit(); + + me.getView().checkChange(); + }, + + control: { + 'grid button': { + click: 'removeRange', + }, + 'field': { + change: 'onValueChange', + }, + }, + }, + + items: [ + { + xtype: 'grid', + reference: 'grid', + scrollable: true, + store: { + fields: ['start-address', 'end-address'], + }, + columns: [ + { + text: gettext('Start Address'), + xtype: 'widgetcolumn', + dataIndex: 'start-address', + flex: 1, + widget: { + xtype: 'textfield', + vtype: 'IP64Address', + }, + }, + { + text: gettext('End Address'), + xtype: 'widgetcolumn', + dataIndex: 'end-address', + flex: 1, + widget: { + xtype: 'textfield', + vtype: 'IP64Address', + }, + }, + { + xtype: 'widgetcolumn', + width: 40, + widget: { + xtype: 'button', + iconCls: 'fa fa-trash-o', + }, + }, + ], + }, + { + xtype: 'container', + layout: { + type: 'hbox', + }, + items: [ + { + xtype: 'button', + text: gettext('Add'), + iconCls: 'fa fa-plus-circle', + handler: 'addRange', + }, + ], }, ], }); @@ -48114,6 +52738,8 @@ Ext.define('PVE.sdn.SubnetEdit', { base_url: undefined, + bodyPadding: 0, + initComponent: function() { var me = this; @@ -48129,11 +52755,22 @@ Ext.define('PVE.sdn.SubnetEdit', { let ipanel = Ext.create('PVE.sdn.SubnetInputPanel', { isCreate: me.isCreate, + title: gettext('General'), + }); + + let dhcpPanel = Ext.create('PVE.sdn.SubnetDhcpRangePanel', { + isCreate: me.isCreate, + title: gettext('DHCP Ranges'), + name: 'dhcp-range', }); Ext.apply(me, { items: [ - ipanel, + { + xtype: 'tabpanel', + bodyPadding: 10, + items: [ipanel, dhcpPanel], + }, ], }); @@ -48142,8 +52779,7 @@ Ext.define('PVE.sdn.SubnetEdit', { if (!me.isCreate) { me.load({ success: function(response, options) { - let values = response.result.data; - ipanel.setValues(values); + me.setValues(response.result.data); }, }); } @@ -48252,7 +52888,7 @@ Ext.define('PVE.sdn.SubnetView', { ], columns: [ { - header: 'ID', + header: gettext('Subnet'), flex: 2, dataIndex: 'cidr', renderer: function(value, metaData, rec) { @@ -48276,7 +52912,7 @@ Ext.define('PVE.sdn.SubnetView', { }, }, { - header: gettext('Dns prefix'), + header: gettext('DNS Prefix'), flex: 1, dataIndex: 'dnszoneprefix', renderer: function(value, metaData, rec) { @@ -48336,17 +52972,18 @@ Ext.define('PVE.sdn.ZoneContentView', { initComponent: function() { var me = this; - var nodename = me.pveSelNode.data.node; - if (!nodename) { + if (!me.nodename) { throw "no node name specified"; } - var zone = me.pveSelNode.data.sdn; - if (!zone) { + if (!me.zone) { throw "no zone ID specified"; } - var baseurl = "/nodes/" + nodename + "/sdn/zones/" + zone + "/content"; + var baseurl = "/nodes/" + me.nodename + "/sdn/zones/" + me.zone + "/content"; + if (me.zone === 'localnetwork') { + baseurl = "/nodes/" + me.nodename + "/network?type=any_local_bridge"; + } var store = Ext.create('Ext.data.Store', { model: 'pve-sdnzone-content', groupField: 'content', @@ -48367,7 +53004,6 @@ Ext.define('PVE.sdn.ZoneContentView', { }; Proxmox.Utils.monStoreErrors(me, store); - Ext.apply(me, { store: store, selModel: sm, @@ -48398,18 +53034,48 @@ Ext.define('PVE.sdn.ZoneContentView', { dataIndex: 'statusmsg', }, ], - listeners: { - activate: reload, - }, + listeners: { + activate: reload, + show: reload, + select: function(_sm, rec) { + let path = `/sdn/zones/${me.zone}/${rec.data.vnet}`; + me.permissions_panel.setPath(path); + }, + deselect: function() { + me.permissions_panel.setPath(undefined); + }, + }, }); - + store.load(); me.callParent(); }, }, function() { Ext.define('pve-sdnzone-content', { extend: 'Ext.data.Model', fields: [ - 'vnet', 'status', 'statusmsg', + { + name: 'iface', + convert: function(value, record) { + //map local vmbr to vnet + if (record.data.iface) { + record.data.vnet = record.data.iface; + } + return value; + }, + }, + { + name: 'comments', + convert: function(value, record) { + //map local vmbr comments to vnet alias + if (record.data.comments) { + record.data.alias = record.data.comments; + } + return value; + }, + }, + 'vnet', + 'status', + 'statusmsg', { name: 'text', convert: function(value, record) { @@ -48425,6 +53091,47 @@ Ext.define('PVE.sdn.ZoneContentView', { idProperty: 'vnet', }); }); +Ext.define('PVE.sdn.ZoneContentPanel', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveSDNZoneContentPanel', + + title: 'VNet', + + onlineHelp: 'pvesdn_config_vnet', + + initComponent: function() { + var me = this; + + var permissions_panel = Ext.createWidget('pveSDNVnetACLView', { + title: gettext('VNet Permissions'), + region: 'center', + border: false, + }); + + var vnetview_panel = Ext.createWidget('pveSDNZoneContentView', { + title: 'VNets', + region: 'west', + permissions_panel: permissions_panel, + nodename: me.nodename, + zone: me.zone, + width: '50%', + border: false, + split: true, + }); + + Ext.apply(me, { + layout: 'border', + items: [vnetview_panel, permissions_panel], + listeners: { + show: function() { + permissions_panel.fireEvent('show', permissions_panel); + }, + }, + }); + + me.callParent(); + }, +}); Ext.define('PVE.sdn.ZoneView', { extend: 'Ext.grid.GridPanel', alias: ['widget.pveSDNZoneView'], @@ -48564,7 +53271,7 @@ Ext.define('PVE.sdn.ZoneView', { }, }, { - header: 'Ipam', + header: 'IPAM', flex: 3, dataIndex: 'ipam', renderer: function(value, metaData, rec) { @@ -48580,7 +53287,7 @@ Ext.define('PVE.sdn.ZoneView', { }, }, { - header: gettext('Dns'), + header: gettext('DNS'), flex: 3, dataIndex: 'dns', renderer: function(value, metaData, rec) { @@ -48588,7 +53295,7 @@ Ext.define('PVE.sdn.ZoneView', { }, }, { - header: gettext('Reverse dns'), + header: gettext('Reverse DNS'), flex: 3, dataIndex: 'reversedns', renderer: function(value, metaData, rec) { @@ -48622,6 +53329,86 @@ Ext.define('PVE.sdn.ZoneView', { me.callParent(); }, }); +Ext.define('PVE.sdn.IpamEditInputPanel', { + extend: 'Proxmox.panel.InputPanel', + mixins: ['Proxmox.Mixin.CBind'], + + isCreate: false, + + onGetValues: function(values) { + let me = this; + + if (!values.vmid) { + delete values.vmid; + } + + return values; + }, + + items: [ + { + xtype: 'pmxDisplayEditField', + name: 'vmid', + fieldLabel: 'VMID', + allowBlank: false, + editable: false, + cbind: { + hidden: '{isCreate}', + }, + }, + { + xtype: 'pmxDisplayEditField', + name: 'mac', + fieldLabel: 'MAC', + allowBlank: false, + cbind: { + editable: '{isCreate}', + }, + }, + { + xtype: 'proxmoxtextfield', + name: 'ip', + fieldLabel: gettext('IP Address'), + allowBlank: false, + }, + ], +}); + +Ext.define('PVE.sdn.IpamEdit', { + extend: 'Proxmox.window.Edit', + + subject: gettext('DHCP Mapping'), + width: 350, + + isCreate: false, + mapping: {}, + + url: '/cluster/sdn/vnets', + + submitUrl: function(url, values) { + return `${url}/${values.vnet}/ips`; + }, + + initComponent: function() { + var me = this; + + me.method = me.isCreate ? 'POST' : 'PUT'; + + let ipanel = Ext.create('PVE.sdn.IpamEditInputPanel', { + isCreate: me.isCreate, + }); + + Ext.apply(me, { + items: [ + ipanel, + ], + }); + + me.callParent(); + + ipanel.setValues(me.mapping); + }, +}); Ext.define('PVE.sdn.Options', { extend: 'Ext.panel.Panel', alias: 'widget.pveSDNOptions', @@ -48645,7 +53432,7 @@ Ext.define('PVE.sdn.Options', { }, { xtype: 'pveSDNIpamView', - title: 'IPAMs', + title: 'IPAM', flex: 1, padding: '0 0 20 0', border: 0, @@ -48843,6 +53630,67 @@ Ext.define('PVE.sdn.controllers.BgpInputPanel', { me.callParent(); }, }); +Ext.define('PVE.sdn.controllers.IsisInputPanel', { + extend: 'PVE.panel.SDNControllerBase', + + onlineHelp: 'pvesdn_controller_plugin_evpn', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + values.controller = 'isis' + values.node; + } else { + delete values.controller; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: 'pveNodeSelector', + name: 'node', + fieldLabel: gettext('Node'), + multiSelect: false, + autoSelect: false, + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'isis-domain', + fieldLabel: 'Domain', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'isis-net', + fieldLabel: 'Network entity title', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'isis-ifaces', + fieldLabel: gettext('Interfaces'), + allowBlank: false, + }, + ]; + + me.advancedItems = [ + { + xtype: 'textfield', + name: 'loopback', + fieldLabel: gettext('Loopback Interface'), + }, + ]; + + me.callParent(); + }, +}); Ext.define('PVE.sdn.IpamView', { extend: 'Ext.grid.GridPanel', alias: ['widget.pveSDNIpamView'], @@ -49072,7 +53920,7 @@ Ext.define('PVE.sdn.ipams.NetboxInputPanel', { { xtype: 'textfield', name: 'url', - fieldLabel: gettext('Url'), + fieldLabel: gettext('URL'), allowBlank: false, }, { @@ -49152,7 +54000,7 @@ Ext.define('PVE.sdn.ipams.PhpIpamInputPanel', { { xtype: 'textfield', name: 'url', - fieldLabel: gettext('Url'), + fieldLabel: gettext('URL'), allowBlank: false, }, { @@ -49403,19 +54251,19 @@ Ext.define('PVE.sdn.dns.PowerdnsInputPanel', { { xtype: 'textfield', name: 'url', - fieldLabel: 'url', + fieldLabel: 'URL', allowBlank: false, }, { xtype: 'textfield', name: 'key', - fieldLabel: gettext('api key'), + fieldLabel: gettext('API Key'), allowBlank: false, }, { xtype: 'proxmoxintegerfield', name: 'ttl', - fieldLabel: 'ttl', + fieldLabel: 'TTL', allowBlank: true, }, ]; @@ -49443,24 +54291,56 @@ Ext.define('PVE.panel.SDNZoneBase', { initComponent: function() { var me = this; - me.advancedItems = [ + me.items.unshift({ + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'zone', + maxLength: 8, + value: me.zone || '', + fieldLabel: 'ID', + allowBlank: false, + }); + + me.items.push( + { + xtype: 'proxmoxintegerfield', + name: 'mtu', + minValue: 100, + maxValue: 65000, + fieldLabel: 'MTU', + allowBlank: true, + emptyText: 'auto', + deleteEmpty: !me.isCreate, + }, + { + xtype: 'pveNodeSelector', + name: 'nodes', + fieldLabel: gettext('Nodes'), + emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', + multiSelect: true, + autoSelect: false, + }, { xtype: 'pveSDNIpamSelector', - fieldLabel: gettext('Ipam'), + fieldLabel: gettext('IPAM'), name: 'ipam', - value: 'pve', + value: me.ipam || 'pve', allowBlank: false, }, + ); + + me.advancedItems = me.advancedItems ?? []; + + me.advancedItems.unshift( { xtype: 'pveSDNDnsSelector', - fieldLabel: gettext('Dns server'), + fieldLabel: gettext('DNS Server'), name: 'dns', value: '', allowBlank: true, }, { xtype: 'pveSDNDnsSelector', - fieldLabel: gettext('Reverse Dns server'), + fieldLabel: gettext('Reverse DNS Server'), name: 'reversedns', value: '', allowBlank: true, @@ -49469,10 +54349,11 @@ Ext.define('PVE.panel.SDNZoneBase', { xtype: 'proxmoxtextfield', name: 'dnszone', skipEmptyText: true, - fieldLabel: gettext('DNS zone'), + fieldLabel: gettext('DNS Zone'), allowBlank: true, + deleteEmpty: !me.isCreate, }, - ]; + ); me.callParent(); }, @@ -49544,30 +54425,8 @@ Ext.define('PVE.sdn.zones.EvpnInputPanel', { if (me.isCreate) { values.type = me.type; - } else { - delete values.zone; } - if (!values.mac) { - delete values.mac; - } - - if (values['advertise-subnets'] === 0) { - delete values['advertise-subnets']; - } - - if (values['exitnodes-local-routing'] === 0) { - delete values['exitnodes-local-routing']; - } - - if (values['disable-arp-nd-suppression'] === 0) { - delete values['disable-arp-nd-suppression']; - } - - if (values['exitnodes-primary'] === '') { - delete values['exitnodes-primary']; - } - return values; }, @@ -49575,14 +54434,6 @@ Ext.define('PVE.sdn.zones.EvpnInputPanel', { var me = this; me.items = [ - { - xtype: me.isCreate ? 'textfield' : 'displayfield', - name: 'zone', - maxLength: 8, - value: me.zone || '', - fieldLabel: 'ID', - allowBlank: false, - }, { xtype: 'pveSDNControllerSelector', fieldLabel: gettext('Controller'), @@ -49599,12 +54450,13 @@ Ext.define('PVE.sdn.zones.EvpnInputPanel', { allowBlank: false, }, { - xtype: 'textfield', + xtype: 'proxmoxtextfield', name: 'mac', - fieldLabel: gettext('Vnet MAC address'), + fieldLabel: gettext('VNet MAC Address'), vtype: 'MacAddress', allowBlank: true, emptyText: 'auto', + deleteEmpty: !me.isCreate, }, { xtype: 'pveNodeSelector', @@ -49623,49 +54475,34 @@ Ext.define('PVE.sdn.zones.EvpnInputPanel', { { xtype: 'proxmoxcheckbox', name: 'exitnodes-local-routing', - uncheckedValue: 0, + uncheckedValue: null, checked: false, - fieldLabel: gettext('Exit Nodes local routing'), + fieldLabel: gettext('Exit Nodes Local Routing'), + deleteEmpty: !me.isCreate, }, { xtype: 'proxmoxcheckbox', name: 'advertise-subnets', - uncheckedValue: 0, + uncheckedValue: null, checked: false, - fieldLabel: gettext('Advertise subnets'), + fieldLabel: gettext('Advertise Subnets'), + deleteEmpty: !me.isCreate, }, { xtype: 'proxmoxcheckbox', name: 'disable-arp-nd-suppression', - uncheckedValue: 0, + uncheckedValue: null, checked: false, - fieldLabel: gettext('Disable arp-nd suppression'), + fieldLabel: gettext('Disable ARP-nd Suppression'), + deleteEmpty: !me.isCreate, }, { - xtype: 'textfield', + xtype: 'proxmoxtextfield', name: 'rt-import', - fieldLabel: gettext('Route-target import'), + fieldLabel: gettext('Route Target Import'), allowBlank: true, + deleteEmpty: !me.isCreate, }, - { - xtype: 'proxmoxintegerfield', - name: 'mtu', - minValue: 100, - maxValue: 65000, - fieldLabel: 'MTU', - skipEmptyText: true, - allowBlank: true, - emptyText: 'auto', - }, - { - xtype: 'pveNodeSelector', - name: 'nodes', - fieldLabel: gettext('Nodes'), - emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', - multiSelect: true, - autoSelect: false, - }, - ]; me.callParent(); @@ -49692,14 +54529,6 @@ Ext.define('PVE.sdn.zones.QinQInputPanel', { let me = this; me.items = [ - { - xtype: me.isCreate ? 'textfield' : 'displayfield', - name: 'zone', - maxLength: 8, - value: me.zone || '', - fieldLabel: 'ID', - allowBlank: false, - }, { xtype: 'textfield', name: 'bridge', @@ -49717,7 +54546,7 @@ Ext.define('PVE.sdn.zones.QinQInputPanel', { { xtype: 'proxmoxKVComboBox', name: 'vlan-protocol', - fieldLabel: gettext('Service-VLAN Protocol'), + fieldLabel: gettext('Service VLAN Protocol'), allowBlank: true, value: '802.1q', comboItems: [ @@ -49725,24 +54554,6 @@ Ext.define('PVE.sdn.zones.QinQInputPanel', { ['802.1ad', '802.1ad'], ], }, - { - xtype: 'proxmoxintegerfield', - name: 'mtu', - minValue: 100, - maxValue: 65000, - fieldLabel: 'MTU', - skipEmptyText: true, - allowBlank: true, - emptyText: 'auto', - }, - { - xtype: 'pveNodeSelector', - name: 'nodes', - fieldLabel: gettext('Nodes'), - emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', - multiSelect: true, - autoSelect: false, - }, ]; me.callParent(); @@ -49768,34 +54579,17 @@ Ext.define('PVE.sdn.zones.SimpleInputPanel', { initComponent: function() { var me = this; - me.items = [ - { - xtype: me.isCreate ? 'textfield' : 'displayfield', - name: 'zone', - maxLength: 10, - value: me.zone || '', - fieldLabel: 'ID', - allowBlank: false, - }, - { - xtype: 'proxmoxintegerfield', - name: 'mtu', - minValue: 100, - maxValue: 65000, - fieldLabel: 'MTU', - skipEmptyText: true, - allowBlank: true, - emptyText: 'auto', - }, - { - xtype: 'pveNodeSelector', - name: 'nodes', - fieldLabel: gettext('Nodes'), - emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', - multiSelect: true, - autoSelect: false, - }, - + me.items = []; + me.advancedItems = [ + { + xtype: 'proxmoxcheckbox', + name: 'dhcp', + inputValue: 'dnsmasq', + uncheckedValue: null, + checked: false, + fieldLabel: gettext('automatic DHCP'), + deleteEmpty: !me.isCreate, + }, ]; me.callParent(); @@ -49822,39 +54616,12 @@ Ext.define('PVE.sdn.zones.VlanInputPanel', { var me = this; me.items = [ - { - xtype: me.isCreate ? 'textfield' : 'displayfield', - name: 'zone', - maxLength: 10, - value: me.zone || '', - fieldLabel: 'ID', - allowBlank: false, - }, { xtype: 'textfield', name: 'bridge', fieldLabel: 'Bridge', allowBlank: false, }, - { - xtype: 'proxmoxintegerfield', - name: 'mtu', - minValue: 100, - maxValue: 65000, - fieldLabel: 'MTU', - skipEmptyText: true, - allowBlank: true, - emptyText: 'auto', - }, - { - xtype: 'pveNodeSelector', - name: 'nodes', - fieldLabel: gettext('Nodes'), - emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', - multiSelect: true, - autoSelect: false, - }, - ]; me.callParent(); @@ -49883,38 +54650,12 @@ Ext.define('PVE.sdn.zones.VxlanInputPanel', { var me = this; me.items = [ - { - xtype: me.isCreate ? 'textfield' : 'displayfield', - maxLength: 8, - name: 'zone', - value: me.zone || '', - fieldLabel: 'ID', - allowBlank: false, - }, { xtype: 'textfield', name: 'peers', fieldLabel: gettext('Peer Address List'), allowBlank: false, }, - { - xtype: 'proxmoxintegerfield', - name: 'mtu', - minValue: 100, - maxValue: 65000, - fieldLabel: 'MTU', - skipEmptyText: true, - allowBlank: true, - emptyText: 'auto', - }, - { - xtype: 'pveNodeSelector', - name: 'nodes', - fieldLabel: gettext('Nodes'), - emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', - multiSelect: true, - autoSelect: false, - }, ]; me.callParent(); @@ -50377,41 +55118,44 @@ Ext.define('PVE.storage.BackupView', { pruneButton, ); + me.extraColumns = {}; + if (isPBS) { - me.extraColumns = { - encrypted: { - header: gettext('Encrypted'), - dataIndex: 'encrypted', - renderer: PVE.Utils.render_backup_encryption, - sorter: { - property: 'encrypted', - transform: encrypted => encrypted ? 1 : 0, - }, + me.extraColumns.encrypted = { + header: gettext('Encrypted'), + dataIndex: 'encrypted', + renderer: PVE.Utils.render_backup_encryption, + sorter: { + property: 'encrypted', + transform: encrypted => encrypted ? 1 : 0, }, - verification: { - header: gettext('Verify State'), - dataIndex: 'verification', - renderer: PVE.Utils.render_backup_verification, - sorter: { - property: 'verification', - transform: value => { - let state = value?.state ?? 'none'; - let order = PVE.Utils.verificationStateOrder; - return order[state] ?? order.__default__; - }, + }; + me.extraColumns.verification = { + header: gettext('Verify State'), + dataIndex: 'verification', + renderer: PVE.Utils.render_backup_verification, + sorter: { + property: 'verification', + transform: value => { + let state = value?.state ?? 'none'; + let order = PVE.Utils.verificationStateOrder; + return order[state] ?? order.__default__; }, }, }; } + me.extraColumns.vmid = { + header: 'VMID', + dataIndex: 'vmid', + hidden: true, + sorter: (a, b) => (a.data.vmid ?? 0) - (b.data.vmid ?? 0), + }; + me.callParent(); me.store.getSorters().clear(); me.store.setSorters([ - { - property: 'vmid', - direction: 'ASC', - }, { property: 'vdate', direction: 'DESC', @@ -50862,6 +55606,9 @@ Ext.define('PVE.storage.CIFSInputPanel', { if (values.username?.length === 0) { delete values.username; } + if (values.subdir?.length === 0) { + delete values.subdir; + } return me.callParent([values]); }, @@ -50949,6 +55696,14 @@ Ext.define('PVE.storage.CIFSInputPanel', { }, }, }, + { + xtype: 'pmxDisplayEditField', + editable: me.isCreate, + name: 'subdir', + fieldLabel: gettext('Subdirectory'), + allowBlank: true, + emptyText: gettext('/some/path'), + }, ]; me.callParent(); @@ -51578,7 +56333,7 @@ Ext.define('PVE.storage.LunSelector', { initComponent: function() { let me = this; - if (PVE.data.ResourceStore.getNodes().length > 1) { + if (!PVE.Utils.isStandaloneNode()) { me.errorHeight = 140; Ext.apply(me.listConfig ?? {}, { tbar: { @@ -53263,16 +58018,24 @@ Ext.define('PVE.storage.TemplateView', { me.callParent(); }, }); + Ext.define('PVE.storage.ZFSInputPanel', { extend: 'PVE.panel.StorageBase', viewModel: { parent: null, data: { +isComstar: true, + isFreeNAS: false, isLIO: false, - isComstar: true, + isToken: false, hasWriteCacheOption: true, }, +formulas: { + hideUsername: function(get) { + return (!get('isFreeNAS') || !(get('isFreeNAS') && !get('isToken'))); + }, + }, }, controller: { @@ -53280,13 +58043,42 @@ Ext.define('PVE.storage.ZFSInputPanel', { control: { 'field[name=iscsiprovider]': { change: 'changeISCSIProvider', +}, + 'field[name=truenas_token_auth]': { + change: 'changeUsername', }, }, changeISCSIProvider: function(f, newVal, oldVal) { +var me = this; var vm = this.getViewModel(); vm.set('isLIO', newVal === 'LIO'); vm.set('isComstar', newVal === 'comstar'); - vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt'); + vm.set('isFreeNAS', newVal === 'freenas'); + vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt'); + if (newVal !== 'freenas') { + me.lookupReference('freenas_use_ssl_field').setValue(false); + me.lookupReference('truenas_token_auth_field').setValue(false); + me.lookupReference('freenas_apiv4_host_field').setValue(''); + me.lookupReference('freenas_user_field').setValue(''); + me.lookupReference('freenas_user_field').allowBlank = true; + me.lookupReference('truenas_secret_field').setValue(''); + me.lookupReference('truenas_secret_field').allowBlank = true; + me.lookupReference('truenas_confirm_secret_field').setValue(''); + me.lookupReference('truenas_confirm_secret_field').allowBlank = true; + } else { + me.lookupReference('freenas_user_field').allowBlank = false; + me.lookupReference('truenas_secret_field').allowBlank = false; + me.lookupReference('truenas_confirm_secret_field').allowBlank = false; + } + }, + changeUsername: function(f, newVal, oldVal) { + var me = this; + var vm = me.getViewModel(); + vm.set('isToken', newVal); + me.lookupReference('freenas_user_field').allowBlank = newVal; + if (newVal) { + me.lookupReference('freenas_user_field').setValue(''); + } }, }, @@ -53299,28 +58091,78 @@ Ext.define('PVE.storage.ZFSInputPanel', { values.nowritecache = values.writecache ? 0 : 1; delete values.writecache; + console.warn(values.freenas_password); + if (values.freenas_password) { + values.truenas_secret = values.freenas_password; + } + console.warn(values.truenas_secret); return me.callParent([values]); }, setValues: function(values) { - values.writecache = values.nowritecache ? 0 : 1; - this.callParent([values]); - }, + if (values.freenas_password) { + values.truenas_secret = values.freenas_password; + } + values.truenas_confirm_secret = values.truenas_secret; + values.writecache = values.nowritecache ? 0 : 1; + this.callParent([values]); + }, + + initComponent: function() { + var me = this; + + var tnsecret = Ext.create('Ext.form.TextField', { + xtype: 'proxmoxtextfield', + name: 'truenas_secret', + reference: 'truenas_secret_field', + inputType: me.isCreate ? '' : 'password', + value: '', + editable: true, + emptyText: Proxmox.Utils.noneText, + bind: { + hidden: '{!isFreeNAS}' + }, + fieldLabel: gettext('API Password'), + change: function(f, value) { + if (f.rendered) { + f.up().down('field[name=truenas_confirm_secret]').validate(); + } + }, + }); - initComponent: function() { - var me = this; + var tnconfirmsecret = Ext.create('Ext.form.TextField', { + xtype: 'proxmoxtextfield', + name: 'truenas_confirm_secret', + reference: 'truenas_confirm_secret_field', + inputType: me.isCreate ? '' : 'password', + value: '', + editable: true, + submitValue: false, + emptyText: Proxmox.Utils.noneText, + bind: { + hidden: '{!isFreeNAS}' + }, + fieldLabel: gettext('Confirm API Password'), + validator: function(value) { + var pw = me.up().down('field[name=truenas_secret]').getValue(); + if (pw !== value) { + return "Secrets do not match!"; + } + return true; + }, + }); - me.column1 = [ - { - xtype: me.isCreate ? 'textfield' : 'displayfield', - name: 'portal', + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'portal', value: '', fieldLabel: gettext('Portal'), allowBlank: false, }, { - xtype: me.isCreate ? 'textfield' : 'displayfield', + xtype: 'textfield', name: 'pool', value: '', fieldLabel: gettext('Pool'), @@ -53330,11 +58172,11 @@ Ext.define('PVE.storage.ZFSInputPanel', { xtype: me.isCreate ? 'textfield' : 'displayfield', name: 'blocksize', value: '4k', - fieldLabel: gettext('Block Size'), + fieldLabel: gettext('ZFS Block Size'), allowBlank: false, }, { - xtype: me.isCreate ? 'textfield' : 'displayfield', + xtype: 'textfield', name: 'target', value: '', fieldLabel: gettext('Target'), @@ -53345,8 +58187,59 @@ Ext.define('PVE.storage.ZFSInputPanel', { name: 'comstar_tg', value: '', fieldLabel: gettext('Target group'), - bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, + bind: { + hidden: '{!isComstar}' + }, allowBlank: true, +}, + { + xtype: 'proxmoxcheckbox', + name: 'freenas_use_ssl', + reference: 'freenas_use_ssl_field', + inputId: 'freenas_use_ssl_field', + checked: false, + bind: { + hidden: '{!isFreeNAS}' + }, + uncheckedValue: 0, + fieldLabel: gettext('API use SSL'), + }, + { + xtype: 'proxmoxcheckbox', + name: 'truenas_token_auth', + reference: 'truenas_token_auth_field', + inputId: 'truenas_use_token_auth_field', + checked: false, + listeners: { + change: function(field, newValue) { + if (newValue === true) { + tnsecret.labelEl.update('API Token'); + tnconfirmsecret.labelEl.update('Confirm API Token'); + me.lookupReference('freenas_user_field').setValue(''); + me.lookupReference('freenas_user_field').allowBlank = true; + } else { + tnsecret.labelEl.update('API Password'); + tnconfirmsecret.labelEl.update('Confirm API Password'); + me.lookupReference('freenas_user_field').allowBlank = false; + } + }, + }, + bind: { + hidden: '{!isFreeNAS}' + }, + uncheckedValue: 0, + fieldLabel: gettext('API Token Auth'), + }, + { + xtype: 'textfield', + name: 'freenas_user', + reference: 'freenas_user_field', + inputId: 'freenas_user_field', + value: '', + fieldLabel: gettext('API Username'), + bind: { + hidden: '{hideUsername}' + }, }, ]; @@ -53377,7 +58270,9 @@ Ext.define('PVE.storage.ZFSInputPanel', { xtype: me.isCreate ? 'textfield' : 'displayfield', name: 'comstar_hg', value: '', - bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, + bind: { + hidden: '{!isComstar}' + }, fieldLabel: gettext('Host group'), allowBlank: true, }, @@ -53385,15 +58280,32 @@ Ext.define('PVE.storage.ZFSInputPanel', { xtype: me.isCreate ? 'textfield' : 'displayfield', name: 'lio_tpg', value: '', - bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' }, - allowBlank: false, - fieldLabel: gettext('Target portal group'), + bind: { + hidden: '{!isLIO}' + }, + fieldLabel: gettext('Target portal group'), + allowBlank: true + }, + { + xtype: 'proxmoxtextfield', + name: 'freenas_apiv4_host', + reference: 'freenas_apiv4_host_field', + value: '', + editable: true, + emptyText: Proxmox.Utils.noneText, + bind: { + hidden: '{!isFreeNAS}' + }, + fieldLabel: gettext('API IPv4 Host'), }, + tnsecret, + tnconfirmsecret, ]; me.callParent(); }, }); + Ext.define('PVE.storage.ZFSPoolSelector', { extend: 'PVE.form.ComboBoxSetStoreNode', alias: 'widget.pveZFSPoolSelector', @@ -53992,7 +58904,7 @@ Ext.define('PVE.StdWorkspace', { listeners: { resize: function(panel, width, height) { var viewWidth = me.getSize().width; - if (width > viewWidth - 100) { + if (width > viewWidth - 100 && viewWidth > 150) { panel.setWidth(viewWidth - 100); } }, @@ -54013,7 +58925,7 @@ Ext.define('PVE.StdWorkspace', { listeners: { resize: function(panel, width, height) { var viewHeight = me.getSize().height; - if (height > viewHeight - 150) { + if (height > viewHeight - 150 && viewHeight > 200) { panel.setHeight(viewHeight - 150); } }, @@ -54033,6 +58945,35 @@ Ext.define('PVE.StdWorkspace', { modalWindows.forEach(win => win.alignTo(me, 'c-c')); } }); + + let tagSelectors = []; + ['circle', 'dense'].forEach((style) => { + ['dark', 'light'].forEach((variant) => { + tagSelectors.push(`.proxmox-tags-${style} .proxmox-tag-${variant}`); + }); + }); + + Ext.create('Ext.tip.ToolTip', { + target: me.el, + delegate: tagSelectors.join(', '), + trackMouse: true, + renderTo: Ext.getBody(), + border: 0, + minWidth: 0, + padding: 0, + bodyBorder: 0, + bodyPadding: 0, + dismissDelay: 0, + userCls: 'pmx-tag-tooltip', + shadow: false, + listeners: { + beforeshow: function(tip) { + let tag = Ext.htmlEncode(tip.triggerElement.innerHTML); + let tagEl = Proxmox.Utils.getTagElement(tag, PVE.UIOptions.tagOverrides); + tip.update(`${tagEl}`); + }, + }, + }); }, }); diff --git a/stable-7/pve-manager/js/pvemanagerlib.js.patch b/stable-7/pve-manager/js/pvemanagerlib.js.patch deleted file mode 100644 index 52019ab..0000000 --- a/stable-7/pve-manager/js/pvemanagerlib.js.patch +++ /dev/null @@ -1,189 +0,0 @@ ---- pvemanagerlib.js.orig 2022-03-17 09:08:40.000000000 -0400 -+++ pvemanagerlib.js 2022-04-03 08:54:10.229689187 -0400 -@@ -8068,6 +8068,7 @@ - alias: ['widget.pveiScsiProviderSelector'], - comboItems: [ - ['comstar', 'Comstar'], -+ ['freenas', 'FreeNAS-API'], - ['istgt', 'istgt'], - ['iet', 'IET'], - ['LIO', 'LIO'], -@@ -49636,6 +49637,7 @@ - data: { - isLIO: false, - isComstar: true, -+ isFreeNAS: false, - hasWriteCacheOption: true, - }, - }, -@@ -49648,10 +49650,26 @@ - }, - }, - changeISCSIProvider: function(f, newVal, oldVal) { -+ var me = this; - var vm = this.getViewModel(); - vm.set('isLIO', newVal === 'LIO'); - vm.set('isComstar', newVal === 'comstar'); -- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt'); -+ vm.set('isFreeNAS', newVal === 'freenas'); -+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt'); -+ if (newVal !== 'freenas') { -+ me.lookupReference('freenas_use_ssl_field').setValue(false); -+ me.lookupReference('freenas_apiv4_host_field').setValue(''); -+ me.lookupReference('freenas_user_field').setValue(''); -+ me.lookupReference('freenas_user_field').allowBlank = true; -+ me.lookupReference('freenas_password_field').setValue(''); -+ me.lookupReference('freenas_password_field').allowBlank = true; -+ me.lookupReference('freenas_confirmpw_field').setValue(''); -+ me.lookupReference('freenas_confirmpw_field').allowBlank = true; -+ } else { -+ me.lookupReference('freenas_user_field').allowBlank = false; -+ me.lookupReference('freenas_password_field').allowBlank = false; -+ me.lookupReference('freenas_confirmpw_field').allowBlank = false; -+ } - }, - }, - -@@ -49669,6 +49687,7 @@ - }, - - setValues: function(values) { -+ values.freenas_confirmpw = values.freenas_password; - values.writecache = values.nowritecache ? 0 : 1; - this.callParent([values]); - }, -@@ -49685,7 +49704,7 @@ - allowBlank: false, - }, - { -- xtype: me.isCreate ? 'textfield' : 'displayfield', -+ xtype: 'textfield', - name: 'pool', - value: '', - fieldLabel: gettext('Pool'), -@@ -49695,11 +49714,11 @@ - xtype: me.isCreate ? 'textfield' : 'displayfield', - name: 'blocksize', - value: '4k', -- fieldLabel: gettext('Block Size'), -+ fieldLabel: gettext('ZFS Block Size'), - allowBlank: false, - }, - { -- xtype: me.isCreate ? 'textfield' : 'displayfield', -+ xtype: 'textfield', - name: 'target', - value: '', - fieldLabel: gettext('Target'), -@@ -49710,9 +49729,34 @@ - name: 'comstar_tg', - value: '', - fieldLabel: gettext('Target group'), -- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, -+ bind: { -+ hidden: '{!isComstar}' -+ }, - allowBlank: true, - }, -+ { -+ xtype: 'proxmoxcheckbox', -+ name: 'freenas_use_ssl', -+ reference: 'freenas_use_ssl_field', -+ inputId: 'freenas_use_ssl_field', -+ checked: false, -+ bind: { -+ hidden: '{!isFreeNAS}' -+ }, -+ uncheckedValue: 0, -+ fieldLabel: gettext('API use SSL'), -+ }, -+ { -+ xtype: 'textfield', -+ name: 'freenas_user', -+ reference: 'freenas_user_field', -+ inputId: 'freenas_user_field', -+ value: '', -+ fieldLabel: gettext('API Username'), -+ bind: { -+ hidden: '{!isFreeNAS}' -+ }, -+ }, - ]; - - me.column2 = [ -@@ -49742,7 +49786,9 @@ - xtype: me.isCreate ? 'textfield' : 'displayfield', - name: 'comstar_hg', - value: '', -- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, -+ bind: { -+ hidden: '{!isComstar}' -+ }, - fieldLabel: gettext('Host group'), - allowBlank: true, - }, -@@ -49750,9 +49796,62 @@ - xtype: me.isCreate ? 'textfield' : 'displayfield', - name: 'lio_tpg', - value: '', -- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' }, -- allowBlank: false, -+ bind: { -+ hidden: '{!isLIO}' -+ }, - fieldLabel: gettext('Target portal group'), -+ allowBlank: true -+ }, -+ { -+ xtype: 'proxmoxtextfield', -+ name: 'freenas_apiv4_host', -+ reference: 'freenas_apiv4_host_field', -+ value: '', -+ editable: true, -+ emptyText: Proxmox.Utils.noneText, -+ bind: { -+ hidden: '{!isFreeNAS}' -+ }, -+ fieldLabel: gettext('API IPv4 Host'), -+ }, -+ { -+ xtype: 'proxmoxtextfield', -+ name: 'freenas_password', -+ reference: 'freenas_password_field', -+ inputType: me.isCreate ? '' : 'password', -+ value: '', -+ editable: true, -+ emptyText: Proxmox.Utils.noneText, -+ bind: { -+ hidden: '{!isFreeNAS}' -+ }, -+ fieldLabel: gettext('API Password'), -+ change: function(f, value) { -+ if (f.rendered) { -+ f.up().down('field[name=freenas_confirmpw]').validate(); -+ } -+ }, -+ }, -+ { -+ xtype: 'proxmoxtextfield', -+ name: 'freenas_confirmpw', -+ reference: 'freenas_confirmpw_field', -+ inputType: me.isCreate ? '' : 'password', -+ value: '', -+ editable: true, -+ submitValue: false, -+ emptyText: Proxmox.Utils.noneText, -+ bind: { -+ hidden: '{!isFreeNAS}' -+ }, -+ fieldLabel: gettext('Confirm Password'), -+ validator: function(value) { -+ var pw = this.up().down('field[name=freenas_password]').getValue(); -+ if (pw !== value) { -+ return "Passwords do not match!"; -+ } -+ return true; -+ }, - }, - ]; - diff --git a/stable-8/perl5/PVE/Storage/ZFSPlugin-8.0.5_1.pm.patch b/stable-8/perl5/PVE/Storage/ZFSPlugin-8.0.5_1.pm.patch new file mode 100644 index 0000000..613c882 --- /dev/null +++ b/stable-8/perl5/PVE/Storage/ZFSPlugin-8.0.5_1.pm.patch @@ -0,0 +1,157 @@ +--- ZFSPlugin.pm.orig 2023-12-31 09:56:18.895228853 -0500 ++++ ZFSPlugin.pm 2023-12-31 09:57:08.830488875 -0500 +@@ -10,6 +10,7 @@ + + use base qw(PVE::Storage::ZFSPoolPlugin); + use PVE::Storage::LunCmd::Comstar; ++use PVE::Storage::LunCmd::FreeNAS; + use PVE::Storage::LunCmd::Istgt; + use PVE::Storage::LunCmd::Iet; + use PVE::Storage::LunCmd::LIO; +@@ -26,13 +27,14 @@ + modify_lu => 1, + add_view => 1, + list_view => 1, ++ list_extent => 1, + list_lu => 1, + }; + + my $zfs_unknown_scsi_provider = sub { + my ($provider) = @_; + +- die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]"; ++ die "$provider: unknown iscsi provider. Available [comstar, freenas, istgt, iet, LIO]"; + }; + + my $zfs_get_base = sub { +@@ -40,6 +42,8 @@ + + if ($scfg->{iscsiprovider} eq 'comstar') { + return PVE::Storage::LunCmd::Comstar::get_base; ++ } elsif ($scfg->{iscsiprovider} eq 'freenas') { ++ return PVE::Storage::LunCmd::FreeNAS::get_base; + } elsif ($scfg->{iscsiprovider} eq 'istgt') { + return PVE::Storage::LunCmd::Istgt::get_base; + } elsif ($scfg->{iscsiprovider} eq 'iet') { +@@ -62,6 +66,8 @@ + if ($lun_cmds->{$method}) { + if ($scfg->{iscsiprovider} eq 'comstar') { + $msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params); ++ } elsif ($scfg->{iscsiprovider} eq 'freenas') { ++ $msg = PVE::Storage::LunCmd::FreeNAS::run_lun_command($scfg, $timeout, $method, @params); + } elsif ($scfg->{iscsiprovider} eq 'istgt') { + $msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params); + } elsif ($scfg->{iscsiprovider} eq 'iet') { +@@ -166,6 +172,15 @@ + die "lun_number for guid $guid is not a number"; + } + ++# Part of the multipath enhancement ++sub zfs_get_wwid_number { ++ my ($class, $scfg, $guid) = @_; ++ ++ die "could not find lun_number for guid $guid" if !$guid; ++ ++ return $class->zfs_request($scfg, undef, 'list_extent', $guid); ++} ++ + # Configuration + + sub type { +@@ -184,6 +199,32 @@ + description => "iscsi provider", + type => 'string', + }, ++ # This is for FreeNAS iscsi and API intergration ++ # And some enhancements asked by the community ++ freenas_user => { ++ description => "FreeNAS API Username", ++ type => 'string', ++ }, ++ freenas_password => { ++ description => "FreeNAS API Password (Deprecated)", ++ type => 'string', ++ }, ++ truenas_secret => { ++ description => "TrueNAS API Secret", ++ type => 'string', ++ }, ++ truenas_token_auth => { ++ description => "TrueNAS API Authentication with Token", ++ type => 'boolean', ++ }, ++ freenas_use_ssl => { ++ description => "FreeNAS API access via SSL", ++ type => 'boolean', ++ }, ++ freenas_apiv4_host => { ++ description => "FreeNAS API Host", ++ type => 'string', ++ }, + # this will disable write caching on comstar and istgt. + # it is not implemented for iet. iet blockio always operates with + # writethrough caching when not in readonly mode +@@ -211,14 +252,20 @@ + nodes => { optional => 1 }, + disable => { optional => 1 }, + portal => { fixed => 1 }, +- target => { fixed => 1 }, +- pool => { fixed => 1 }, ++ target => { fixed => 0 }, ++ pool => { fixed => 0 }, + blocksize => { fixed => 1 }, + iscsiprovider => { fixed => 1 }, + nowritecache => { optional => 1 }, + sparse => { optional => 1 }, + comstar_hg => { optional => 1 }, + comstar_tg => { optional => 1 }, ++ freenas_user => { optional => 1 }, ++ freenas_password => { optional => 1 }, ++ truenas_secret => { optional => 1 }, ++ truenas_token_auth => { optional => 1 }, ++ freenas_use_ssl => { optional => 1 }, ++ freenas_apiv4_host => { optional => 1 }, + lio_tpg => { optional => 1 }, + content => { optional => 1 }, + bwlimit => { optional => 1 }, +@@ -243,6 +290,40 @@ + + my $path = "iscsi://$portal/$target/$lun"; + ++ # Multipath enhancement ++ eval { ++ my $wwid = $class->zfs_get_wwid_number($scfg, $guid); ++# syslog(info,"JD: path get_lun_number guid $guid"); ++ ++ if ($wwid =~ /^([-\@\w.]+)$/) { ++ $wwid = $1; # $data now untainted ++ } else { ++ die "Bad data in '$wwid'"; # log this somewhere ++ } ++ my $wwid_end = substr $wwid, 16; ++ ++ my $mapper = ''; ++ sleep 3; ++ run_command("iscsiadm -m session --rescan"); ++ sleep 3; ++ my $line = `/usr/sbin/multipath -ll | grep \"$wwid_end\"`; ++ my ($mapper_device) = split(' ', $line); ++ $mapper_device = "" unless $mapper_device; ++ $mapper .= $mapper_device; ++ ++ if ($mapper =~ /^([-\@\w.]+)$/) { ++ $mapper = $1; # $data now untainted ++ } else { ++ $mapper = ''; ++ } ++ ++# syslog(info,"Multipath mapper found: $mapper\n"); ++ if ($mapper ne "") { ++ $path = "/dev/mapper/$mapper"; ++ sleep 5; ++ } ++ }; ++ + return ($path, $vmid, $vtype); + } + diff --git a/stable-8/perl5/PVE/Storage/ZFSPlugin.pm.orig b/stable-8/perl5/PVE/Storage/ZFSPlugin.pm.orig new file mode 100644 index 0000000..d4dc2a4 --- /dev/null +++ b/stable-8/perl5/PVE/Storage/ZFSPlugin.pm.orig @@ -0,0 +1,422 @@ +package PVE::Storage::ZFSPlugin; + +use strict; +use warnings; +use IO::File; +use POSIX; +use PVE::Tools qw(run_command); +use PVE::Storage::ZFSPoolPlugin; +use PVE::RPCEnvironment; + +use base qw(PVE::Storage::ZFSPoolPlugin); +use PVE::Storage::LunCmd::Comstar; +use PVE::Storage::LunCmd::Istgt; +use PVE::Storage::LunCmd::Iet; +use PVE::Storage::LunCmd::LIO; + + +my @ssh_opts = ('-o', 'BatchMode=yes'); +my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts); +my $id_rsa_path = '/etc/pve/priv/zfs'; + +my $lun_cmds = { + create_lu => 1, + delete_lu => 1, + import_lu => 1, + modify_lu => 1, + add_view => 1, + list_view => 1, + list_lu => 1, +}; + +my $zfs_unknown_scsi_provider = sub { + my ($provider) = @_; + + die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]"; +}; + +my $zfs_get_base = sub { + my ($scfg) = @_; + + if ($scfg->{iscsiprovider} eq 'comstar') { + return PVE::Storage::LunCmd::Comstar::get_base; + } elsif ($scfg->{iscsiprovider} eq 'istgt') { + return PVE::Storage::LunCmd::Istgt::get_base; + } elsif ($scfg->{iscsiprovider} eq 'iet') { + return PVE::Storage::LunCmd::Iet::get_base; + } elsif ($scfg->{iscsiprovider} eq 'LIO') { + return PVE::Storage::LunCmd::LIO::get_base; + } else { + $zfs_unknown_scsi_provider->($scfg->{iscsiprovider}); + } +}; + +sub zfs_request { + my ($class, $scfg, $timeout, $method, @params) = @_; + + $timeout = PVE::RPCEnvironment->is_worker() ? 60*60 : 10 + if !$timeout; + + my $msg = ''; + + if ($lun_cmds->{$method}) { + if ($scfg->{iscsiprovider} eq 'comstar') { + $msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params); + } elsif ($scfg->{iscsiprovider} eq 'istgt') { + $msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params); + } elsif ($scfg->{iscsiprovider} eq 'iet') { + $msg = PVE::Storage::LunCmd::Iet::run_lun_command($scfg, $timeout, $method, @params); + } elsif ($scfg->{iscsiprovider} eq 'LIO') { + $msg = PVE::Storage::LunCmd::LIO::run_lun_command($scfg, $timeout, $method, @params); + } else { + $zfs_unknown_scsi_provider->($scfg->{iscsiprovider}); + } + } else { + + my $target = 'root@' . $scfg->{portal}; + + my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target]; + + if ($method eq 'zpool_list') { + push @$cmd, 'zpool', 'list'; + } else { + push @$cmd, 'zfs', $method; + } + + push @$cmd, @params; + + my $output = sub { + my $line = shift; + $msg .= "$line\n"; + }; + + run_command($cmd, outfunc => $output, timeout => $timeout); + } + + return $msg; +} + +sub zfs_get_lu_name { + my ($class, $scfg, $zvol) = @_; + + my $base = $zfs_get_base->($scfg); + + $zvol = ($class->parse_volname($zvol))[1]; + + my $object = ($zvol =~ /^.+\/.+/) ? "$base/$zvol" : "$base/$scfg->{pool}/$zvol"; + + my $lu_name = $class->zfs_request($scfg, undef, 'list_lu', $object); + + return $lu_name if $lu_name; + + die "Could not find lu_name for zvol $zvol"; +} + +sub zfs_add_lun_mapping_entry { + my ($class, $scfg, $zvol, $guid) = @_; + + if (!defined($guid)) { + $guid = $class->zfs_get_lu_name($scfg, $zvol); + } + + $class->zfs_request($scfg, undef, 'add_view', $guid); +} + +sub zfs_delete_lu { + my ($class, $scfg, $zvol) = @_; + + my $guid = $class->zfs_get_lu_name($scfg, $zvol); + + $class->zfs_request($scfg, undef, 'delete_lu', $guid); +} + +sub zfs_create_lu { + my ($class, $scfg, $zvol) = @_; + + my $base = $zfs_get_base->($scfg); + my $guid = $class->zfs_request($scfg, undef, 'create_lu', "$base/$scfg->{pool}/$zvol"); + + return $guid; +} + +sub zfs_import_lu { + my ($class, $scfg, $zvol) = @_; + + my $base = $zfs_get_base->($scfg); + $class->zfs_request($scfg, undef, 'import_lu', "$base/$scfg->{pool}/$zvol"); +} + +sub zfs_resize_lu { + my ($class, $scfg, $zvol, $size) = @_; + + my $guid = $class->zfs_get_lu_name($scfg, $zvol); + + $class->zfs_request($scfg, undef, 'modify_lu', "${size}K", $guid); +} + +sub zfs_get_lun_number { + my ($class, $scfg, $guid) = @_; + + die "could not find lun_number for guid $guid" if !$guid; + + if ($class->zfs_request($scfg, undef, 'list_view', $guid) =~ /^(\d+)$/) { + return $1; + } + + die "lun_number for guid $guid is not a number"; +} + +# Configuration + +sub type { + return 'zfs'; +} + +sub plugindata { + return { + content => [ {images => 1}, { images => 1 }], + }; +} + +sub properties { + return { + iscsiprovider => { + description => "iscsi provider", + type => 'string', + }, + # this will disable write caching on comstar and istgt. + # it is not implemented for iet. iet blockio always operates with + # writethrough caching when not in readonly mode + nowritecache => { + description => "disable write caching on the target", + type => 'boolean', + }, + comstar_tg => { + description => "target group for comstar views", + type => 'string', + }, + comstar_hg => { + description => "host group for comstar views", + type => 'string', + }, + lio_tpg => { + description => "target portal group for Linux LIO targets", + type => 'string', + }, + }; +} + +sub options { + return { + nodes => { optional => 1 }, + disable => { optional => 1 }, + portal => { fixed => 1 }, + target => { fixed => 1 }, + pool => { fixed => 1 }, + blocksize => { fixed => 1 }, + iscsiprovider => { fixed => 1 }, + nowritecache => { optional => 1 }, + sparse => { optional => 1 }, + comstar_hg => { optional => 1 }, + comstar_tg => { optional => 1 }, + lio_tpg => { optional => 1 }, + content => { optional => 1 }, + bwlimit => { optional => 1 }, + }; +} + +# Storage implementation + +sub path { + my ($class, $scfg, $volname, $storeid, $snapname) = @_; + + die "direct access to snapshots not implemented" + if defined($snapname); + + my ($vtype, $name, $vmid) = $class->parse_volname($volname); + + my $target = $scfg->{target}; + my $portal = $scfg->{portal}; + + my $guid = $class->zfs_get_lu_name($scfg, $name); + my $lun = $class->zfs_get_lun_number($scfg, $guid); + + my $path = "iscsi://$portal/$target/$lun"; + + return ($path, $vmid, $vtype); +} + +sub create_base { + my ($class, $storeid, $scfg, $volname) = @_; + + my $snap = '__base__'; + + my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) = + $class->parse_volname($volname); + + die "create_base not possible with base image\n" if $isBase; + + my $newname = $name; + $newname =~ s/^vm-/base-/; + + my $newvolname = $basename ? "$basename/$newname" : "$newname"; + + $class->zfs_delete_lu($scfg, $name); + $class->zfs_request($scfg, undef, 'rename', "$scfg->{pool}/$name", "$scfg->{pool}/$newname"); + + my $guid = $class->zfs_create_lu($scfg, $newname); + $class->zfs_add_lun_mapping_entry($scfg, $newname, $guid); + + my $running = undef; #fixme : is create_base always offline ? + + $class->volume_snapshot($scfg, $storeid, $newname, $snap, $running); + + return $newvolname; +} + +sub clone_image { + my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_; + + my $name = $class->SUPER::clone_image($scfg, $storeid, $volname, $vmid, $snap); + + # get ZFS dataset name from PVE volname + my (undef, $clonedname) = $class->parse_volname($name); + + my $guid = $class->zfs_create_lu($scfg, $clonedname); + $class->zfs_add_lun_mapping_entry($scfg, $clonedname, $guid); + + return $name; +} + +sub alloc_image { + my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_; + + die "unsupported format '$fmt'" if $fmt ne 'raw'; + + die "illegal name '$name' - should be 'vm-$vmid-*'\n" + if $name && $name !~ m/^vm-$vmid-/; + + my $volname = $name; + + $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt) if !$volname; + + $class->zfs_create_zvol($scfg, $volname, $size); + + my $guid = $class->zfs_create_lu($scfg, $volname); + $class->zfs_add_lun_mapping_entry($scfg, $volname, $guid); + + return $volname; +} + +sub free_image { + my ($class, $storeid, $scfg, $volname, $isBase) = @_; + + my ($vtype, $name, $vmid) = $class->parse_volname($volname); + + $class->zfs_delete_lu($scfg, $name); + + eval { $class->zfs_delete_zvol($scfg, $name); }; + if (my $err = $@) { + my $guid = $class->zfs_create_lu($scfg, $name); + $class->zfs_add_lun_mapping_entry($scfg, $name, $guid); + die $err; + } + + return undef; +} + +sub volume_resize { + my ($class, $scfg, $storeid, $volname, $size, $running) = @_; + + $volname = ($class->parse_volname($volname))[1]; + + my $new_size = $class->SUPER::volume_resize($scfg, $storeid, $volname, $size, $running); + + $class->zfs_resize_lu($scfg, $volname, $new_size); + + return $new_size; +} + +sub volume_snapshot_delete { + my ($class, $scfg, $storeid, $volname, $snap, $running) = @_; + + $volname = ($class->parse_volname($volname))[1]; + + $class->zfs_request($scfg, undef, 'destroy', "$scfg->{pool}/$volname\@$snap"); +} + +sub volume_snapshot_rollback { + my ($class, $scfg, $storeid, $volname, $snap) = @_; + + $volname = ($class->parse_volname($volname))[1]; + + $class->zfs_delete_lu($scfg, $volname); + + $class->zfs_request($scfg, undef, 'rollback', "$scfg->{pool}/$volname\@$snap"); + + $class->zfs_import_lu($scfg, $volname); + + $class->zfs_add_lun_mapping_entry($scfg, $volname); +} + +sub storage_can_replicate { + my ($class, $scfg, $storeid, $format) = @_; + + return 0; +} + +sub volume_has_feature { + my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_; + + my $features = { + snapshot => { current => 1, snap => 1}, + clone => { base => 1}, + template => { current => 1}, + copy => { base => 1, current => 1}, + }; + + my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) = + $class->parse_volname($volname); + + my $key = undef; + + if ($snapname) { + $key = 'snap'; + } else { + $key = $isBase ? 'base' : 'current'; + } + + return 1 if $features->{$feature}->{$key}; + + return undef; +} + +sub activate_storage { + my ($class, $storeid, $scfg, $cache) = @_; + + return 1; +} + +sub deactivate_storage { + my ($class, $storeid, $scfg, $cache) = @_; + + return 1; +} + +sub activate_volume { + my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_; + + die "unable to activate snapshot from remote zfs storage" if $snapname; + + return 1; +} + +sub deactivate_volume { + my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_; + + die "unable to deactivate snapshot from remote zfs storage" if $snapname; + + return 1; +} + +1; diff --git a/stable-7/perl5/PVE/Storage/ZFSPlugin.pm.patch b/stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch similarity index 89% rename from stable-7/perl5/PVE/Storage/ZFSPlugin.pm.patch rename to stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch index d791749..613c882 100644 --- a/stable-7/perl5/PVE/Storage/ZFSPlugin.pm.patch +++ b/stable-8/perl5/PVE/Storage/ZFSPlugin.pm.patch @@ -1,5 +1,5 @@ ---- ZFSPlugin.pm.orig 2022-02-04 12:08:01.000000000 -0500 -+++ ZFSPlugin.pm 2022-03-26 13:51:40.660068908 -0400 +--- ZFSPlugin.pm.orig 2023-12-31 09:56:18.895228853 -0500 ++++ ZFSPlugin.pm 2023-12-31 09:57:08.830488875 -0500 @@ -10,6 +10,7 @@ use base qw(PVE::Storage::ZFSPoolPlugin); @@ -58,7 +58,7 @@ # Configuration sub type { -@@ -184,6 +199,24 @@ +@@ -184,6 +199,32 @@ description => "iscsi provider", type => 'string', }, @@ -69,9 +69,17 @@ + type => 'string', + }, + freenas_password => { -+ description => "FreeNAS API Password", ++ description => "FreeNAS API Password (Deprecated)", + type => 'string', + }, ++ truenas_secret => { ++ description => "TrueNAS API Secret", ++ type => 'string', ++ }, ++ truenas_token_auth => { ++ description => "TrueNAS API Authentication with Token", ++ type => 'boolean', ++ }, + freenas_use_ssl => { + description => "FreeNAS API access via SSL", + type => 'boolean', @@ -83,7 +91,7 @@ # this will disable write caching on comstar and istgt. # it is not implemented for iet. iet blockio always operates with # writethrough caching when not in readonly mode -@@ -211,14 +244,18 @@ +@@ -211,14 +252,20 @@ nodes => { optional => 1 }, disable => { optional => 1 }, portal => { fixed => 1 }, @@ -99,12 +107,14 @@ comstar_tg => { optional => 1 }, + freenas_user => { optional => 1 }, + freenas_password => { optional => 1 }, ++ truenas_secret => { optional => 1 }, ++ truenas_token_auth => { optional => 1 }, + freenas_use_ssl => { optional => 1 }, + freenas_apiv4_host => { optional => 1 }, lio_tpg => { optional => 1 }, content => { optional => 1 }, bwlimit => { optional => 1 }, -@@ -243,6 +280,40 @@ +@@ -243,6 +290,40 @@ my $path = "iscsi://$portal/$target/$lun"; diff --git a/stable-8/pve-docs/api-viewer/apidoc-8.0.5_1.js.patch b/stable-8/pve-docs/api-viewer/apidoc-8.0.5_1.js.patch new file mode 100644 index 0000000..281fcc3 --- /dev/null +++ b/stable-8/pve-docs/api-viewer/apidoc-8.0.5_1.js.patch @@ -0,0 +1,91 @@ +--- apidoc.js.orig 2024-01-06 13:02:06.730512378 -0500 ++++ apidoc.js 2024-01-06 13:02:55.349787105 -0500 +@@ -50336,6 +50336,37 @@ + "type" : "string", + "typetext" : "" + }, ++ "freenas_user" : { ++ "description" : "FreeNAS user for API access", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "freenas_password" : { ++ "description" : "FreeNAS password for API access (Deprecated)", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "truenas_secret" : { ++ "description" : "TrueNAS Secret for API access", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "freenas_use_ssl" : { ++ "description" : "FreeNAS API access via SSL", ++ "optional" : 1, ++ "type" : "boolean", ++ "typetext" : "" ++ }, ++ "freenas_apiv4_host" : { ++ "description" : "FreeNAS API Host via IPv4", ++ "format" : "address", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, + "fuse" : { + "description" : "Mount CephFS through FUSE.", + "optional" : 1, +@@ -50555,6 +50586,12 @@ + "type" : "boolean", + "typetext" : "" + }, ++ "target" : { ++ "description" : "iSCSI target.", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, + "transport" : { + "description" : "Gluster transport: tcp or rdma", + "enum" : [ +@@ -50854,6 +50891,37 @@ + "optional" : 1, + "type" : "string", + "typetext" : "" ++ }, ++ "freenas_user" : { ++ "description" : "FreeNAS user for API access", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "freenas_password" : { ++ "description" : "FreeNAS password for API access (Deprecated)", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "truenas_secret" : { ++ "description" : "TrueNAS secret for API access", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "freenas_use_ssl" : { ++ "description" : "FreeNAS API access via SSL", ++ "optional" : 1, ++ "type" : "boolean", ++ "typetext" : "" ++ }, ++ "freenas_apiv4_host" : { ++ "description" : "FreeNAS API Host via IPv4", ++ "format" : "address", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" + }, + "fuse" : { + "description" : "Mount CephFS through FUSE.", diff --git a/stable-8/pve-docs/api-viewer/apidoc.js.orig b/stable-8/pve-docs/api-viewer/apidoc.js.orig new file mode 100644 index 0000000..bcd7554 --- /dev/null +++ b/stable-8/pve-docs/api-viewer/apidoc.js.orig @@ -0,0 +1,55548 @@ +const apiSchema = [ + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Mark replication job for removal.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "default" : 0, + "description" : "Will remove the jobconfig entry, but will not cleanup.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "id" : { + "description" : "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '-'.", + "format" : "pve-replication-job-id", + "pattern" : "[1-9][0-9]{2,8}-\\d{1,9}", + "type" : "string" + }, + "keep" : { + "default" : 0, + "description" : "Keep replicated data at target (do not remove).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read replication job configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '-'.", + "format" : "pve-replication-job-id", + "pattern" : "[1-9][0-9]{2,8}-\\d{1,9}", + "type" : "string" + } + } + }, + "permissions" : { + "description" : "Requires the VM.Audit permission on /vms/.", + "user" : "all" + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update replication job configuration.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Description.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "description" : "Flag to disable/deactivate the entry.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "id" : { + "description" : "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '-'.", + "format" : "pve-replication-job-id", + "pattern" : "[1-9][0-9]{2,8}-\\d{1,9}", + "type" : "string" + }, + "rate" : { + "description" : "Rate limit in mbps (megabytes per second) as floating point number.", + "minimum" : 1, + "optional" : 1, + "type" : "number", + "typetext" : " (1 - N)" + }, + "remove_job" : { + "description" : "Mark the replication job for removal. The job will remove all local replication snapshots. When set to 'full', it also tries to remove replicated volumes on the target. The job then removes itself from the configuration file.", + "enum" : [ + "local", + "full" + ], + "optional" : 1, + "type" : "string" + }, + "schedule" : { + "default" : "*/15", + "description" : "Storage replication schedule. The format is a subset of `systemd` calendar events.", + "format" : "pve-calendar-event", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "For internal use, to detect if the guest was stolen.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/replication/{id}", + "text" : "{id}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List replication jobs.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "description" : "Requires the VM.Audit permission on /vms/.", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new replication job", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Description.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "description" : "Flag to disable/deactivate the entry.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "id" : { + "description" : "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '-'.", + "format" : "pve-replication-job-id", + "pattern" : "[1-9][0-9]{2,8}-\\d{1,9}", + "type" : "string" + }, + "rate" : { + "description" : "Rate limit in mbps (megabytes per second) as floating point number.", + "minimum" : 1, + "optional" : 1, + "type" : "number", + "typetext" : " (1 - N)" + }, + "remove_job" : { + "description" : "Mark the replication job for removal. The job will remove all local replication snapshots. When set to 'full', it also tries to remove replicated volumes on the target. The job then removes itself from the configuration file.", + "enum" : [ + "local", + "full" + ], + "optional" : 1, + "type" : "string" + }, + "schedule" : { + "default" : "*/15", + "description" : "Storage replication schedule. The format is a subset of `systemd` calendar events.", + "format" : "pve-calendar-event", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "For internal use, to detect if the guest was stolen.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "target" : { + "description" : "Target node.", + "format" : "pve-node", + "optional" : 0, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Section type.", + "enum" : [ + "local" + ], + "type" : "string" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/replication", + "text" : "replication" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove Metric server.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read metric server configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new external metric server config", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "api-path-prefix" : { + "description" : "An API path prefix inserted between ':/' and '/api2/'. Can be useful if the InfluxDB service runs behind a reverse proxy.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bucket" : { + "description" : "The InfluxDB bucket/db. Only necessary when using the http v2 api.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "description" : "Flag to disable the plugin.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "id" : { + "description" : "The ID of the entry.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "influxdbproto" : { + "default" : "udp", + "enum" : [ + "udp", + "http", + "https" + ], + "optional" : 1, + "type" : "string" + }, + "max-body-size" : { + "default" : 25000000, + "description" : "InfluxDB max-body-size in bytes. Requests are batched up to this size.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "mtu" : { + "default" : 1500, + "description" : "MTU for metrics transmission over UDP", + "maximum" : 65536, + "minimum" : 512, + "optional" : 1, + "type" : "integer", + "typetext" : " (512 - 65536)" + }, + "organization" : { + "description" : "The InfluxDB organization. Only necessary when using the http v2 api. Has no meaning when using v2 compatibility api.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "path" : { + "description" : "root graphite path (ex: proxmox.mycluster.mykey)", + "format" : "graphite-path", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "port" : { + "description" : "server network port", + "maximum" : 65536, + "minimum" : 1, + "type" : "integer", + "typetext" : " (1 - 65536)" + }, + "proto" : { + "description" : "Protocol to send graphite data. TCP or UDP (default)", + "enum" : [ + "udp", + "tcp" + ], + "optional" : 1, + "type" : "string" + }, + "server" : { + "description" : "server dns name or IP address", + "format" : "address", + "type" : "string", + "typetext" : "" + }, + "timeout" : { + "default" : 1, + "description" : "graphite TCP socket timeout (default=1)", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "token" : { + "description" : "The InfluxDB access token. Only necessary when using the http v2 api. If the v2 compatibility api is used, use 'user:password' instead.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Plugin type.", + "enum" : [ + "graphite", + "influxdb" + ], + "format" : "pve-configid", + "type" : "string" + }, + "verify-certificate" : { + "default" : 1, + "description" : "Set to 0 to disable certificate verification for https endpoints.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update metric server configuration.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "api-path-prefix" : { + "description" : "An API path prefix inserted between ':/' and '/api2/'. Can be useful if the InfluxDB service runs behind a reverse proxy.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bucket" : { + "description" : "The InfluxDB bucket/db. Only necessary when using the http v2 api.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "description" : "Flag to disable the plugin.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "id" : { + "description" : "The ID of the entry.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "influxdbproto" : { + "default" : "udp", + "enum" : [ + "udp", + "http", + "https" + ], + "optional" : 1, + "type" : "string" + }, + "max-body-size" : { + "default" : 25000000, + "description" : "InfluxDB max-body-size in bytes. Requests are batched up to this size.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "mtu" : { + "default" : 1500, + "description" : "MTU for metrics transmission over UDP", + "maximum" : 65536, + "minimum" : 512, + "optional" : 1, + "type" : "integer", + "typetext" : " (512 - 65536)" + }, + "organization" : { + "description" : "The InfluxDB organization. Only necessary when using the http v2 api. Has no meaning when using v2 compatibility api.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "path" : { + "description" : "root graphite path (ex: proxmox.mycluster.mykey)", + "format" : "graphite-path", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "port" : { + "description" : "server network port", + "maximum" : 65536, + "minimum" : 1, + "type" : "integer", + "typetext" : " (1 - 65536)" + }, + "proto" : { + "description" : "Protocol to send graphite data. TCP or UDP (default)", + "enum" : [ + "udp", + "tcp" + ], + "optional" : 1, + "type" : "string" + }, + "server" : { + "description" : "server dns name or IP address", + "format" : "address", + "type" : "string", + "typetext" : "" + }, + "timeout" : { + "default" : 1, + "description" : "graphite TCP socket timeout (default=1)", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "token" : { + "description" : "The InfluxDB access token. Only necessary when using the http v2 api. If the v2 compatibility api is used, use 'user:password' instead.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "verify-certificate" : { + "default" : 1, + "description" : "Set to 0 to disable certificate verification for https endpoints.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/metrics/server/{id}", + "text" : "{id}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List configured metric servers.", + "method" : "GET", + "name" : "server_index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "disable" : { + "description" : "Flag to disable the plugin.", + "type" : "boolean" + }, + "id" : { + "description" : "The ID of the entry.", + "type" : "string" + }, + "port" : { + "description" : "Server network port", + "type" : "integer" + }, + "server" : { + "description" : "Server dns name or IP address", + "type" : "string" + }, + "type" : { + "description" : "Plugin type.", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/metrics/server", + "text" : "server" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Metrics index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/metrics", + "text" : "metrics" + }, + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove sendmail endpoint", + "method" : "DELETE", + "name" : "delete_sendmail_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Return a specific sendmail endpoint", + "method" : "GET", + "name" : "get_sendmail_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Audit" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "properties" : { + "author" : { + "description" : "Author of the mail", + "optional" : 1, + "type" : "string" + }, + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean" + }, + "from-address" : { + "description" : "`From` address for the mail", + "optional" : 1, + "type" : "string" + }, + "mailto" : { + "description" : "List of email recipients", + "items" : { + "format" : "email-or-username", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "mailto-user" : { + "description" : "List of users", + "items" : { + "format" : "pve-userid", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update existing sendmail endpoint", + "method" : "PUT", + "name" : "update_sendmail_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "author" : { + "description" : "Author of the mail", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "items" : { + "format" : "pve-configid", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "from-address" : { + "description" : "`From` address for the mail", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "mailto" : { + "description" : "List of email recipients", + "items" : { + "format" : "email-or-username", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "mailto-user" : { + "description" : "List of users", + "items" : { + "format" : "pve-userid", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/notifications/endpoints/sendmail/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Returns a list of all sendmail endpoints", + "method" : "GET", + "name" : "get_sendmail_endpoints", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Audit" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : { + "author" : { + "description" : "Author of the mail", + "optional" : 1, + "type" : "string" + }, + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean" + }, + "from-address" : { + "description" : "`From` address for the mail", + "optional" : 1, + "type" : "string" + }, + "mailto" : { + "description" : "List of email recipients", + "items" : { + "format" : "email-or-username", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "mailto-user" : { + "description" : "List of users", + "items" : { + "format" : "pve-userid", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string" + }, + "origin" : { + "description" : "Show if this entry was created by a user or was built-in", + "enum" : [ + "user-created", + "builtin", + "modified-builtin" + ], + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new sendmail endpoint", + "method" : "POST", + "name" : "create_sendmail_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "author" : { + "description" : "Author of the mail", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "from-address" : { + "description" : "`From` address for the mail", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "mailto" : { + "description" : "List of email recipients", + "items" : { + "format" : "email-or-username", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "mailto-user" : { + "description" : "List of users", + "items" : { + "format" : "pve-userid", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/notifications/endpoints/sendmail", + "text" : "sendmail" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove gotify endpoint", + "method" : "DELETE", + "name" : "delete_gotify_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Return a specific gotify endpoint", + "method" : "GET", + "name" : "get_gotify_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "Name of the endpoint.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Audit" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "properties" : { + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string" + }, + "server" : { + "description" : "Server URL", + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update existing gotify endpoint", + "method" : "PUT", + "name" : "update_gotify_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "items" : { + "format" : "pve-configid", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "server" : { + "description" : "Server URL", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "token" : { + "description" : "Secret token", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/notifications/endpoints/gotify/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Returns a list of all gotify endpoints", + "method" : "GET", + "name" : "get_gotify_endpoints", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/notifications", + [ + "Mapping.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string" + }, + "origin" : { + "description" : "Show if this entry was created by a user or was built-in", + "enum" : [ + "user-created", + "builtin", + "modified-builtin" + ], + "type" : "string" + }, + "server" : { + "description" : "Server URL", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new gotify endpoint", + "method" : "POST", + "name" : "create_gotify_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "server" : { + "description" : "Server URL", + "type" : "string", + "typetext" : "" + }, + "token" : { + "description" : "Secret token", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/notifications/endpoints/gotify", + "text" : "gotify" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove smtp endpoint", + "method" : "DELETE", + "name" : "delete_smtp_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Return a specific smtp endpoint", + "method" : "GET", + "name" : "get_smtp_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Audit" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "properties" : { + "author" : { + "description" : "Author of the mail. Defaults to 'Proxmox VE'.", + "optional" : 1, + "type" : "string" + }, + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean" + }, + "from-address" : { + "description" : "`From` address for the mail", + "type" : "string" + }, + "mailto" : { + "description" : "List of email recipients", + "items" : { + "format" : "email-or-username", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "mailto-user" : { + "description" : "List of users", + "items" : { + "format" : "pve-userid", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "mode" : { + "default" : "tls", + "description" : "Determine which encryption method shall be used for the connection.", + "enum" : [ + "insecure", + "starttls", + "tls" + ], + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string" + }, + "port" : { + "description" : "The port to be used. Defaults to 465 for TLS based connections, 587 for STARTTLS based connections and port 25 for insecure plain-text connections.", + "optional" : 1, + "type" : "integer" + }, + "server" : { + "description" : "The address of the SMTP server.", + "type" : "string" + }, + "username" : { + "description" : "Username for SMTP authentication", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update existing smtp endpoint", + "method" : "PUT", + "name" : "update_smtp_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "author" : { + "description" : "Author of the mail. Defaults to 'Proxmox VE'.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "items" : { + "format" : "pve-configid", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "from-address" : { + "description" : "`From` address for the mail", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "mailto" : { + "description" : "List of email recipients", + "items" : { + "format" : "email-or-username", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "mailto-user" : { + "description" : "List of users", + "items" : { + "format" : "pve-userid", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "mode" : { + "default" : "tls", + "description" : "Determine which encryption method shall be used for the connection.", + "enum" : [ + "insecure", + "starttls", + "tls" + ], + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "Password for SMTP authentication", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "port" : { + "description" : "The port to be used. Defaults to 465 for TLS based connections, 587 for STARTTLS based connections and port 25 for insecure plain-text connections.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "server" : { + "description" : "The address of the SMTP server.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "username" : { + "description" : "Username for SMTP authentication", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/notifications/endpoints/smtp/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Returns a list of all smtp endpoints", + "method" : "GET", + "name" : "get_smtp_endpoints", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Audit" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : { + "author" : { + "description" : "Author of the mail. Defaults to 'Proxmox VE'.", + "optional" : 1, + "type" : "string" + }, + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean" + }, + "from-address" : { + "description" : "`From` address for the mail", + "type" : "string" + }, + "mailto" : { + "description" : "List of email recipients", + "items" : { + "format" : "email-or-username", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "mailto-user" : { + "description" : "List of users", + "items" : { + "format" : "pve-userid", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "mode" : { + "default" : "tls", + "description" : "Determine which encryption method shall be used for the connection.", + "enum" : [ + "insecure", + "starttls", + "tls" + ], + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string" + }, + "origin" : { + "description" : "Show if this entry was created by a user or was built-in", + "enum" : [ + "user-created", + "builtin", + "modified-builtin" + ], + "type" : "string" + }, + "port" : { + "description" : "The port to be used. Defaults to 465 for TLS based connections, 587 for STARTTLS based connections and port 25 for insecure plain-text connections.", + "optional" : 1, + "type" : "integer" + }, + "server" : { + "description" : "The address of the SMTP server.", + "type" : "string" + }, + "username" : { + "description" : "Username for SMTP authentication", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new smtp endpoint", + "method" : "POST", + "name" : "create_smtp_endpoint", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "author" : { + "description" : "Author of the mail. Defaults to 'Proxmox VE'.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "default" : 0, + "description" : "Disable this target", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "from-address" : { + "description" : "`From` address for the mail", + "type" : "string", + "typetext" : "" + }, + "mailto" : { + "description" : "List of email recipients", + "items" : { + "format" : "email-or-username", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "mailto-user" : { + "description" : "List of users", + "items" : { + "format" : "pve-userid", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "mode" : { + "default" : "tls", + "description" : "Determine which encryption method shall be used for the connection.", + "enum" : [ + "insecure", + "starttls", + "tls" + ], + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "The name of the endpoint.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "Password for SMTP authentication", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "port" : { + "description" : "The port to be used. Defaults to 465 for TLS based connections, 587 for STARTTLS based connections and port 25 for insecure plain-text connections.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "server" : { + "description" : "The address of the SMTP server.", + "type" : "string", + "typetext" : "" + }, + "username" : { + "description" : "Username for SMTP authentication", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/notifications/endpoints/smtp", + "text" : "smtp" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Index for all available endpoint types.", + "method" : "GET", + "name" : "endpoints_index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/notifications/endpoints", + "text" : "endpoints" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Send a test notification to a provided target.", + "method" : "POST", + "name" : "test_target", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "Name of the target.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Audit" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Use" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/notifications/targets/{name}/test", + "text" : "test" + } + ], + "leaf" : 0, + "path" : "/cluster/notifications/targets/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Returns a list of all entities that can be used as notification targets.", + "method" : "GET", + "name" : "get_all_targets", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Audit" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Use" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string" + }, + "disable" : { + "default" : 0, + "description" : "Show if this target is disabled", + "optional" : 1, + "type" : "boolean" + }, + "name" : { + "description" : "Name of the target.", + "format" : "pve-configid", + "type" : "string" + }, + "origin" : { + "description" : "Show if this entry was created by a user or was built-in", + "enum" : [ + "user-created", + "builtin", + "modified-builtin" + ], + "type" : "string" + }, + "type" : { + "description" : "Type of the target.", + "enum" : [ + "sendmail", + "gotify" + ], + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/notifications/targets", + "text" : "targets" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove matcher", + "method" : "DELETE", + "name" : "delete_matcher", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Return a specific matcher", + "method" : "GET", + "name" : "get_matcher", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Audit" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "properties" : { + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string" + }, + "disable" : { + "default" : 0, + "description" : "Disable this matcher", + "optional" : 1, + "type" : "boolean" + }, + "invert-match" : { + "description" : "Invert match of the whole matcher", + "optional" : 1, + "type" : "boolean" + }, + "match-calendar" : { + "description" : "Match notification timestamp", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "match-field" : { + "description" : "Metadata fields to match (regex or exact match). Must be in the form (regex|exact):=", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "match-severity" : { + "description" : "Notification severities to match", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "mode" : { + "default" : "all", + "description" : "Choose between 'all' and 'any' for when multiple properties are specified", + "enum" : [ + "all", + "any" + ], + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "Name of the matcher.", + "format" : "pve-configid", + "type" : "string" + }, + "target" : { + "description" : "Targets to notify on match", + "items" : { + "format" : "pve-configid", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update existing matcher", + "method" : "PUT", + "name" : "update_matcher", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "items" : { + "format" : "pve-configid", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "default" : 0, + "description" : "Disable this matcher", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "invert-match" : { + "description" : "Invert match of the whole matcher", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "match-calendar" : { + "description" : "Match notification timestamp", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "match-field" : { + "description" : "Metadata fields to match (regex or exact match). Must be in the form (regex|exact):=", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "match-severity" : { + "description" : "Notification severities to match", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "mode" : { + "default" : "all", + "description" : "Choose between 'all' and 'any' for when multiple properties are specified", + "enum" : [ + "all", + "any" + ], + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "Name of the matcher.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "target" : { + "description" : "Targets to notify on match", + "items" : { + "format" : "pve-configid", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/notifications/matchers/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Returns a list of all matchers", + "method" : "GET", + "name" : "get_matchers", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Audit" + ] + ], + [ + "perm", + "/mapping/notifications", + [ + "Mapping.Use" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string" + }, + "disable" : { + "default" : 0, + "description" : "Disable this matcher", + "optional" : 1, + "type" : "boolean" + }, + "invert-match" : { + "description" : "Invert match of the whole matcher", + "optional" : 1, + "type" : "boolean" + }, + "match-calendar" : { + "description" : "Match notification timestamp", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "match-field" : { + "description" : "Metadata fields to match (regex or exact match). Must be in the form (regex|exact):=", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "match-severity" : { + "description" : "Notification severities to match", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "mode" : { + "default" : "all", + "description" : "Choose between 'all' and 'any' for when multiple properties are specified", + "enum" : [ + "all", + "any" + ], + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "Name of the matcher.", + "format" : "pve-configid", + "type" : "string" + }, + "origin" : { + "description" : "Show if this entry was created by a user or was built-in", + "enum" : [ + "user-created", + "builtin", + "modified-builtin" + ], + "type" : "string" + }, + "target" : { + "description" : "Targets to notify on match", + "items" : { + "format" : "pve-configid", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new matcher", + "method" : "POST", + "name" : "create_matcher", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Comment", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "default" : 0, + "description" : "Disable this matcher", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "invert-match" : { + "description" : "Invert match of the whole matcher", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "match-calendar" : { + "description" : "Match notification timestamp", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "match-field" : { + "description" : "Metadata fields to match (regex or exact match). Must be in the form (regex|exact):=", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "match-severity" : { + "description" : "Notification severities to match", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "mode" : { + "default" : "all", + "description" : "Choose between 'all' and 'any' for when multiple properties are specified", + "enum" : [ + "all", + "any" + ], + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "Name of the matcher.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "target" : { + "description" : "Targets to notify on match", + "items" : { + "format" : "pve-configid", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/notifications", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/notifications/matchers", + "text" : "matchers" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Index for notification-related API endpoints.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/notifications", + "text" : "notifications" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Return the version of the cluster join API available on this node.", + "method" : "GET", + "name" : "join_api_version", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "description" : "Cluster Join API version, currently 1", + "minimum" : 0, + "type" : "integer" + } + } + }, + "leaf" : 1, + "path" : "/cluster/config/apiversion", + "text" : "apiversion" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Removes a node from the cluster configuration.", + "method" : "DELETE", + "name" : "delnode", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Adds a node to the cluster configuration. This call is for internal use.", + "method" : "POST", + "name" : "addnode", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "apiversion" : { + "description" : "The JOIN_API_VERSION of the new node.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "force" : { + "description" : "Do not throw error if node already exists.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "link[n]" : { + "description" : "Address and priority information of a single corosync link. (up to 8 links supported; link0..link7)", + "format" : { + "address" : { + "default_key" : 1, + "description" : "Hostname (or IP) of this corosync link address.", + "format" : "address", + "format_description" : "IP", + "type" : "string" + }, + "priority" : { + "default" : 0, + "description" : "The priority for the link when knet is used in 'passive' mode (default). Lower value means higher priority. Only valid for cluster create, ignored on node add.", + "maximum" : 255, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[address=] [,priority=]" + }, + "new_node_ip" : { + "description" : "IP Address of node to add. Used as fallback if no links are given.", + "format" : "ip", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "nodeid" : { + "description" : "Node id for this node.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "votes" : { + "description" : "Number of votes for this node", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "protected" : 1, + "returns" : { + "properties" : { + "corosync_authkey" : { + "type" : "string" + }, + "corosync_conf" : { + "type" : "string" + }, + "warnings" : { + "items" : { + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/cluster/config/nodes/{node}", + "text" : "{node}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Corosync node list.", + "method" : "GET", + "name" : "nodes", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "node" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{node}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/config/nodes", + "text" : "nodes" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get information needed to join this cluster over the connected node.", + "method" : "GET", + "name" : "join_info", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "default" : "current connected node", + "description" : "The node for which the joinee gets the nodeinfo. ", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "config_digest" : { + "type" : "string" + }, + "nodelist" : { + "items" : { + "additionalProperties" : 1, + "properties" : { + "name" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string" + }, + "nodeid" : { + "description" : "Node id for this node.", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "pve_addr" : { + "format" : "ip", + "type" : "string" + }, + "pve_fp" : { + "description" : "Certificate SHA 256 fingerprint.", + "pattern" : "([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}", + "type" : "string" + }, + "quorum_votes" : { + "minimum" : 0, + "type" : "integer" + }, + "ring0_addr" : { + "description" : "Address and priority information of a single corosync link. (up to 8 links supported; link0..link7)", + "format" : { + "address" : { + "default_key" : 1, + "description" : "Hostname (or IP) of this corosync link address.", + "format" : "address", + "format_description" : "IP", + "type" : "string" + }, + "priority" : { + "default" : 0, + "description" : "The priority for the link when knet is used in 'passive' mode (default). Lower value means higher priority. Only valid for cluster create, ignored on node add.", + "maximum" : 255, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + }, + "preferred_node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string" + }, + "totem" : { + "type" : "object" + } + }, + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Joins this node into an existing cluster. If no links are given, default to IP resolved by node's hostname on single link (fallback fails for clusters with multiple links).", + "method" : "POST", + "name" : "join", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "fingerprint" : { + "description" : "Certificate SHA 256 fingerprint.", + "pattern" : "([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}", + "type" : "string" + }, + "force" : { + "description" : "Do not throw error if node already exists.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "hostname" : { + "description" : "Hostname (or IP) of an existing cluster member.", + "type" : "string", + "typetext" : "" + }, + "link[n]" : { + "description" : "Address and priority information of a single corosync link. (up to 8 links supported; link0..link7)", + "format" : { + "address" : { + "default_key" : 1, + "description" : "Hostname (or IP) of this corosync link address.", + "format" : "address", + "format_description" : "IP", + "type" : "string" + }, + "priority" : { + "default" : 0, + "description" : "The priority for the link when knet is used in 'passive' mode (default). Lower value means higher priority. Only valid for cluster create, ignored on node add.", + "maximum" : 255, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[address=] [,priority=]" + }, + "nodeid" : { + "description" : "Node id for this node.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "password" : { + "description" : "Superuser (root) password of peer node.", + "maxLength" : 128, + "type" : "string", + "typetext" : "" + }, + "votes" : { + "description" : "Number of votes for this node", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "protected" : 1, + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/cluster/config/join", + "text" : "join" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get corosync totem protocol settings.", + "method" : "GET", + "name" : "totem", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/cluster/config/totem", + "text" : "totem" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get QDevice status", + "method" : "GET", + "name" : "status", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/cluster/config/qdevice", + "text" : "qdevice" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Generate new cluster configuration. If no links given, default to local IP address as link0.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "clustername" : { + "description" : "The name of the cluster.", + "format" : "pve-node", + "maxLength" : 15, + "type" : "string", + "typetext" : "" + }, + "link[n]" : { + "description" : "Address and priority information of a single corosync link. (up to 8 links supported; link0..link7)", + "format" : { + "address" : { + "default_key" : 1, + "description" : "Hostname (or IP) of this corosync link address.", + "format" : "address", + "format_description" : "IP", + "type" : "string" + }, + "priority" : { + "default" : 0, + "description" : "The priority for the link when knet is used in 'passive' mode (default). Lower value means higher priority. Only valid for cluster create, ignored on node add.", + "maximum" : 255, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[address=] [,priority=]" + }, + "nodeid" : { + "description" : "Node id for this node.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "votes" : { + "description" : "Number of votes for this node.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + } + } + }, + "protected" : 1, + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/cluster/config", + "text" : "config" + }, + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete rule.", + "method" : "DELETE", + "name" : "delete_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group" : { + "description" : "Security Group name.", + "maxLength" : 18, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get single rule data.", + "method" : "GET", + "name" : "get_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "group" : { + "description" : "Security Group name.", + "maxLength" : 18, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : null, + "returns" : { + "properties" : { + "action" : { + "type" : "string" + }, + "comment" : { + "optional" : 1, + "type" : "string" + }, + "dest" : { + "optional" : 1, + "type" : "string" + }, + "dport" : { + "optional" : 1, + "type" : "string" + }, + "enable" : { + "optional" : 1, + "type" : "integer" + }, + "icmp-type" : { + "optional" : 1, + "type" : "string" + }, + "iface" : { + "optional" : 1, + "type" : "string" + }, + "ipversion" : { + "optional" : 1, + "type" : "integer" + }, + "log" : { + "description" : "Log level for firewall rule", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "optional" : 1, + "type" : "string" + }, + "pos" : { + "type" : "integer" + }, + "proto" : { + "optional" : 1, + "type" : "string" + }, + "source" : { + "optional" : 1, + "type" : "string" + }, + "sport" : { + "optional" : 1, + "type" : "string" + }, + "type" : { + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Modify rule data.", + "method" : "PUT", + "name" : "update_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "action" : { + "description" : "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "comment" : { + "description" : "Descriptive comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dest" : { + "description" : "Restrict packet destination address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dport" : { + "description" : "Restrict TCP/UDP destination port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-dport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Flag to enable/disable a rule.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "group" : { + "description" : "Security Group name.", + "maxLength" : 18, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "icmp-type" : { + "description" : "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'.", + "format" : "pve-fw-icmp-type-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name. You have to use network configuration key names for VMs and containers ('net\\d+'). Host related rules can use arbitrary strings.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "log" : { + "description" : "Log level for firewall rule.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "description" : "Use predefined standard macro.", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "moveto" : { + "description" : "Move rule to new position . Other arguments are ignored.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "proto" : { + "description" : "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.", + "format" : "pve-fw-protocol-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "Restrict packet source address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sport" : { + "description" : "Restrict TCP/UDP source port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-sport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Rule type.", + "enum" : [ + "in", + "out", + "group" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/firewall/groups/{group}/{pos}", + "text" : "{pos}" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete security group.", + "method" : "DELETE", + "name" : "delete_security_group", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "group" : { + "description" : "Security Group name.", + "maxLength" : 18, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "List rules.", + "method" : "GET", + "name" : "get_rules", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "group" : { + "description" : "Security Group name.", + "maxLength" : 18, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : null, + "returns" : { + "items" : { + "properties" : { + "pos" : { + "type" : "integer" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{pos}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new rule.", + "method" : "POST", + "name" : "create_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "action" : { + "description" : "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.", + "maxLength" : 20, + "minLength" : 2, + "optional" : 0, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "comment" : { + "description" : "Descriptive comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dest" : { + "description" : "Restrict packet destination address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dport" : { + "description" : "Restrict TCP/UDP destination port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-dport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Flag to enable/disable a rule.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "group" : { + "description" : "Security Group name.", + "maxLength" : 18, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "icmp-type" : { + "description" : "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'.", + "format" : "pve-fw-icmp-type-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name. You have to use network configuration key names for VMs and containers ('net\\d+'). Host related rules can use arbitrary strings.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "log" : { + "description" : "Log level for firewall rule.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "description" : "Use predefined standard macro.", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "proto" : { + "description" : "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.", + "format" : "pve-fw-protocol-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "Restrict packet source address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sport" : { + "description" : "Restrict TCP/UDP source port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-sport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Rule type.", + "enum" : [ + "in", + "out", + "group" + ], + "optional" : 0, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/firewall/groups/{group}", + "text" : "{group}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List security groups.", + "method" : "GET", + "name" : "list_security_groups", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 0, + "type" : "string" + }, + "group" : { + "description" : "Security Group name.", + "maxLength" : 18, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{group}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new security group.", + "method" : "POST", + "name" : "create_security_group", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group" : { + "description" : "Security Group name.", + "maxLength" : 18, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "rename" : { + "description" : "Rename/update an existing security group. You can set 'rename' to the same value as 'name' to update the 'comment' of an existing group.", + "maxLength" : 18, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/firewall/groups", + "text" : "groups" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete rule.", + "method" : "DELETE", + "name" : "delete_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get single rule data.", + "method" : "GET", + "name" : "get_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : null, + "returns" : { + "properties" : { + "action" : { + "type" : "string" + }, + "comment" : { + "optional" : 1, + "type" : "string" + }, + "dest" : { + "optional" : 1, + "type" : "string" + }, + "dport" : { + "optional" : 1, + "type" : "string" + }, + "enable" : { + "optional" : 1, + "type" : "integer" + }, + "icmp-type" : { + "optional" : 1, + "type" : "string" + }, + "iface" : { + "optional" : 1, + "type" : "string" + }, + "ipversion" : { + "optional" : 1, + "type" : "integer" + }, + "log" : { + "description" : "Log level for firewall rule", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "optional" : 1, + "type" : "string" + }, + "pos" : { + "type" : "integer" + }, + "proto" : { + "optional" : 1, + "type" : "string" + }, + "source" : { + "optional" : 1, + "type" : "string" + }, + "sport" : { + "optional" : 1, + "type" : "string" + }, + "type" : { + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Modify rule data.", + "method" : "PUT", + "name" : "update_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "action" : { + "description" : "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "comment" : { + "description" : "Descriptive comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dest" : { + "description" : "Restrict packet destination address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dport" : { + "description" : "Restrict TCP/UDP destination port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-dport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Flag to enable/disable a rule.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "icmp-type" : { + "description" : "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'.", + "format" : "pve-fw-icmp-type-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name. You have to use network configuration key names for VMs and containers ('net\\d+'). Host related rules can use arbitrary strings.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "log" : { + "description" : "Log level for firewall rule.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "description" : "Use predefined standard macro.", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "moveto" : { + "description" : "Move rule to new position . Other arguments are ignored.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "proto" : { + "description" : "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.", + "format" : "pve-fw-protocol-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "Restrict packet source address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sport" : { + "description" : "Restrict TCP/UDP source port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-sport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Rule type.", + "enum" : [ + "in", + "out", + "group" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/firewall/rules/{pos}", + "text" : "{pos}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List rules.", + "method" : "GET", + "name" : "get_rules", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : null, + "returns" : { + "items" : { + "properties" : { + "pos" : { + "type" : "integer" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{pos}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new rule.", + "method" : "POST", + "name" : "create_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "action" : { + "description" : "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.", + "maxLength" : 20, + "minLength" : 2, + "optional" : 0, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "comment" : { + "description" : "Descriptive comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dest" : { + "description" : "Restrict packet destination address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dport" : { + "description" : "Restrict TCP/UDP destination port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-dport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Flag to enable/disable a rule.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "icmp-type" : { + "description" : "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'.", + "format" : "pve-fw-icmp-type-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name. You have to use network configuration key names for VMs and containers ('net\\d+'). Host related rules can use arbitrary strings.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "log" : { + "description" : "Log level for firewall rule.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "description" : "Use predefined standard macro.", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "proto" : { + "description" : "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.", + "format" : "pve-fw-protocol-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "Restrict packet source address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sport" : { + "description" : "Restrict TCP/UDP source port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-sport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Rule type.", + "enum" : [ + "in", + "out", + "group" + ], + "optional" : 0, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/firewall/rules", + "text" : "rules" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove IP or Network from IPSet.", + "method" : "DELETE", + "name" : "remove_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read IP or Network settings from IPSet.", + "method" : "GET", + "name" : "read_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update IP or Network settings", + "method" : "PUT", + "name" : "update_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "nomatch" : { + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/firewall/ipset/{name}/{cidr}", + "text" : "{cidr}" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete IPSet", + "method" : "DELETE", + "name" : "delete_ipset", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "description" : "Delete all members of the IPSet, if there are any.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "List IPSet content", + "method" : "GET", + "name" : "get_ipset", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "cidr" : { + "type" : "string" + }, + "comment" : { + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 0, + "type" : "string" + }, + "nomatch" : { + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{cidr}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Add IP or Network to IPSet.", + "method" : "POST", + "name" : "create_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "nomatch" : { + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/firewall/ipset/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List IPSets", + "method" : "GET", + "name" : "ipset_index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 0, + "type" : "string" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new IPSet", + "method" : "POST", + "name" : "create_ipset", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "rename" : { + "description" : "Rename an existing IPSet. You can set 'rename' to the same value as 'name' to update the 'comment' of an existing IPSet.", + "maxLength" : 64, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/firewall/ipset", + "text" : "ipset" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove IP or Network alias.", + "method" : "DELETE", + "name" : "remove_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read alias.", + "method" : "GET", + "name" : "read_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update IP or Network alias.", + "method" : "PUT", + "name" : "update_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDR", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "rename" : { + "description" : "Rename an existing alias.", + "maxLength" : 64, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/firewall/aliases/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List aliases", + "method" : "GET", + "name" : "get_aliases", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "cidr" : { + "type" : "string" + }, + "comment" : { + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 0, + "type" : "string" + }, + "name" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create IP or Network Alias.", + "method" : "POST", + "name" : "create_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDR", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/firewall/aliases", + "text" : "aliases" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get Firewall options.", + "method" : "GET", + "name" : "get_options", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "properties" : { + "ebtables" : { + "default" : 1, + "description" : "Enable ebtables rules cluster wide.", + "optional" : 1, + "type" : "boolean" + }, + "enable" : { + "description" : "Enable or disable the firewall cluster wide.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "log_ratelimit" : { + "description" : "Log ratelimiting settings", + "format" : { + "burst" : { + "default" : 5, + "description" : "Initial burst of packages which will always get logged before the rate is applied", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "enable" : { + "default" : "1", + "default_key" : 1, + "description" : "Enable or disable log rate limiting", + "type" : "boolean" + }, + "rate" : { + "default" : "1/second", + "description" : "Frequency with which the burst bucket gets refilled", + "format_description" : "rate", + "optional" : 1, + "pattern" : "[1-9][0-9]*\\/(second|minute|hour|day)", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "policy_in" : { + "description" : "Input policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + }, + "policy_out" : { + "description" : "Output policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set Firewall options.", + "method" : "PUT", + "name" : "set_options", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ebtables" : { + "default" : 1, + "description" : "Enable ebtables rules cluster wide.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "enable" : { + "description" : "Enable or disable the firewall cluster wide.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "log_ratelimit" : { + "description" : "Log ratelimiting settings", + "format" : { + "burst" : { + "default" : 5, + "description" : "Initial burst of packages which will always get logged before the rate is applied", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "enable" : { + "default" : "1", + "default_key" : 1, + "description" : "Enable or disable log rate limiting", + "type" : "boolean" + }, + "rate" : { + "default" : "1/second", + "description" : "Frequency with which the burst bucket gets refilled", + "format_description" : "rate", + "optional" : 1, + "pattern" : "[1-9][0-9]*\\/(second|minute|hour|day)", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[enable=]<1|0> [,burst=] [,rate=]" + }, + "policy_in" : { + "description" : "Input policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + }, + "policy_out" : { + "description" : "Output policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/firewall/options", + "text" : "options" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List available macros", + "method" : "GET", + "name" : "get_macros", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "descr" : { + "description" : "More verbose description (if available).", + "type" : "string" + }, + "macro" : { + "description" : "Macro name.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/firewall/macros", + "text" : "macros" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Lists possible IPSet/Alias reference which are allowed in source/dest properties.", + "method" : "GET", + "name" : "refs", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "type" : { + "description" : "Only list references of specified type.", + "enum" : [ + "alias", + "ipset" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "name" : { + "type" : "string" + }, + "ref" : { + "type" : "string" + }, + "scope" : { + "type" : "string" + }, + "type" : { + "enum" : [ + "alias", + "ipset" + ], + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/firewall/refs", + "text" : "refs" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/firewall", + "text" : "firewall" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Returns included guests and the backup status of their disks. Optimized to be used in ExtJS tree views.", + "method" : "GET", + "name" : "get_volume_backup_included", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "The job ID.", + "maxLength" : 50, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "description" : "Root node of the tree object. Children represent guests, grandchildren represent volumes of that guest.", + "properties" : { + "children" : { + "items" : { + "properties" : { + "children" : { + "description" : "The volumes of the guest with the information if they will be included in backups.", + "items" : { + "properties" : { + "id" : { + "description" : "Configuration key of the volume.", + "type" : "string" + }, + "included" : { + "description" : "Whether the volume is included in the backup or not.", + "type" : "boolean" + }, + "name" : { + "description" : "Name of the volume.", + "type" : "string" + }, + "reason" : { + "description" : "The reason why the volume is included (or excluded).", + "type" : "string" + } + }, + "type" : "object" + }, + "optional" : 1, + "type" : "array" + }, + "id" : { + "description" : "VMID of the guest.", + "type" : "integer" + }, + "name" : { + "description" : "Name of the guest", + "optional" : 1, + "type" : "string" + }, + "type" : { + "description" : "Type of the guest, VM, CT or unknown for removed but not purged guests.", + "enum" : [ + "qemu", + "lxc", + "unknown" + ], + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/cluster/backup/{id}/included_volumes", + "text" : "included_volumes" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete vzdump backup job definition.", + "method" : "DELETE", + "name" : "delete_job", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "The job ID.", + "maxLength" : 50, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read vzdump backup job definition.", + "method" : "GET", + "name" : "read_job", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "The job ID.", + "maxLength" : 50, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update vzdump backup job definition.", + "method" : "PUT", + "name" : "update_job", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "all" : { + "default" : 0, + "description" : "Backup all known guest systems on this host.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "bwlimit" : { + "default" : 0, + "description" : "Limit I/O bandwidth (in KiB/s).", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "comment" : { + "description" : "Description for the Job.", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "compress" : { + "default" : "0", + "description" : "Compress dump file.", + "enum" : [ + "0", + "1", + "gzip", + "lzo", + "zstd" + ], + "optional" : 1, + "type" : "string" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dow" : { + "description" : "Day of week selection.", + "format" : "pve-day-of-week-list", + "optional" : 1, + "requires" : "starttime", + "type" : "string", + "typetext" : "" + }, + "dumpdir" : { + "description" : "Store resulting files to specified directory.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enabled" : { + "default" : "1", + "description" : "Enable or disable the job.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "exclude" : { + "description" : "Exclude specified guest systems (assumes --all)", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "exclude-path" : { + "description" : "Exclude certain files/directories (shell globs). Paths starting with '/' are anchored to the container's root, other paths match relative to each subdirectory.", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "id" : { + "description" : "The job ID.", + "maxLength" : 50, + "type" : "string", + "typetext" : "" + }, + "ionice" : { + "default" : 7, + "description" : "Set IO priority when using the BFQ scheduler. For snapshot and suspend mode backups of VMs, this only affects the compressor. A value of 8 means the idle priority is used, otherwise the best-effort priority is used with the specified value.", + "maximum" : 8, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 8)" + }, + "lockwait" : { + "default" : 180, + "description" : "Maximal time to wait for the global lock (minutes).", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "mailnotification" : { + "default" : "always", + "description" : "Deprecated: use 'notification-policy' instead.", + "enum" : [ + "always", + "failure" + ], + "optional" : 1, + "type" : "string" + }, + "mailto" : { + "description" : "Comma-separated list of email addresses or users that should receive email notifications. Has no effect if the 'notification-target' option is set at the same time.", + "format" : "email-or-username-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "maxfiles" : { + "description" : "Deprecated: use 'prune-backups' instead. Maximal number of backup files per guest system.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "mode" : { + "default" : "snapshot", + "description" : "Backup mode.", + "enum" : [ + "snapshot", + "suspend", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "Only run if executed on this node.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "notes-template" : { + "description" : "Template string for generating notes for the backup(s). It can contain variables which will be replaced by their values. Currently supported are {{cluster}}, {{guestname}}, {{node}}, and {{vmid}}, but more might be added in the future. Needs to be a single line, newline and backslash need to be escaped as '\\n' and '\\\\' respectively.", + "maxLength" : 1024, + "optional" : 1, + "requires" : "storage", + "type" : "string", + "typetext" : "" + }, + "notification-policy" : { + "default" : "always", + "description" : "Specify when to send a notification", + "enum" : [ + "always", + "failure", + "never" + ], + "optional" : 1, + "type" : "string" + }, + "notification-target" : { + "description" : "Determine the target to which notifications should be sent. Can either be a notification endpoint or a notification group. This option takes precedence over 'mailto', meaning that if both are set, the 'mailto' option will be ignored.", + "format" : "pve-configid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "performance" : { + "description" : "Other performance-related settings.", + "format" : "backup-performance", + "optional" : 1, + "type" : "string", + "typetext" : "[max-workers=] [,pbs-entries-max=]" + }, + "pigz" : { + "default" : 0, + "description" : "Use pigz instead of gzip when N>0. N=1 uses half of cores, N>1 uses N as thread count.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "pool" : { + "description" : "Backup all known guest systems included in the specified pool.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "protected" : { + "description" : "If true, mark backup(s) as protected.", + "optional" : 1, + "requires" : "storage", + "type" : "boolean", + "typetext" : "" + }, + "prune-backups" : { + "default" : "keep-all=1", + "description" : "Use these retention options instead of those from the storage configuration.", + "format" : "prune-backups", + "optional" : 1, + "type" : "string", + "typetext" : "[keep-all=<1|0>] [,keep-daily=] [,keep-hourly=] [,keep-last=] [,keep-monthly=] [,keep-weekly=] [,keep-yearly=]" + }, + "quiet" : { + "default" : 0, + "description" : "Be quiet.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "remove" : { + "default" : 1, + "description" : "Prune older backups according to 'prune-backups'.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "repeat-missed" : { + "default" : 0, + "description" : "If true, the job will be run as soon as possible if it was missed while the scheduler was not running.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "schedule" : { + "description" : "Backup schedule. The format is a subset of `systemd` calendar events.", + "format" : "pve-calendar-event", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "script" : { + "description" : "Use specified hook script.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "starttime" : { + "description" : "Job Start time.", + "optional" : 1, + "pattern" : "\\d{1,2}:\\d{1,2}", + "type" : "string", + "typetext" : "HH:MM" + }, + "stdexcludes" : { + "default" : 1, + "description" : "Exclude temporary files and logs.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "stop" : { + "default" : 0, + "description" : "Stop running backup jobs on this host.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "stopwait" : { + "default" : 10, + "description" : "Maximal time to wait until a guest system is stopped (minutes).", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "storage" : { + "description" : "Store resulting file to this storage.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tmpdir" : { + "description" : "Store temporary files to specified directory.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The ID of the guest system you want to backup.", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "zstd" : { + "default" : 1, + "description" : "Zstd threads. N=0 uses half of the available cores, N>0 uses N as thread count.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ], + "description" : "The 'tmpdir', 'dumpdir' and 'script' parameters are additionally restricted to the 'root@pam' user." + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/backup/{id}", + "text" : "{id}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List vzdump backup schedule.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "id" : { + "description" : "The job ID.", + "maxLength" : 50, + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new vzdump backup job.", + "method" : "POST", + "name" : "create_job", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "all" : { + "default" : 0, + "description" : "Backup all known guest systems on this host.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "bwlimit" : { + "default" : 0, + "description" : "Limit I/O bandwidth (in KiB/s).", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "comment" : { + "description" : "Description for the Job.", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "compress" : { + "default" : "0", + "description" : "Compress dump file.", + "enum" : [ + "0", + "1", + "gzip", + "lzo", + "zstd" + ], + "optional" : 1, + "type" : "string" + }, + "dow" : { + "default" : "mon,tue,wed,thu,fri,sat,sun", + "description" : "Day of week selection.", + "format" : "pve-day-of-week-list", + "optional" : 1, + "requires" : "starttime", + "type" : "string", + "typetext" : "" + }, + "dumpdir" : { + "description" : "Store resulting files to specified directory.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enabled" : { + "default" : "1", + "description" : "Enable or disable the job.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "exclude" : { + "description" : "Exclude specified guest systems (assumes --all)", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "exclude-path" : { + "description" : "Exclude certain files/directories (shell globs). Paths starting with '/' are anchored to the container's root, other paths match relative to each subdirectory.", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "id" : { + "description" : "Job ID (will be autogenerated).", + "format" : "pve-configid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ionice" : { + "default" : 7, + "description" : "Set IO priority when using the BFQ scheduler. For snapshot and suspend mode backups of VMs, this only affects the compressor. A value of 8 means the idle priority is used, otherwise the best-effort priority is used with the specified value.", + "maximum" : 8, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 8)" + }, + "lockwait" : { + "default" : 180, + "description" : "Maximal time to wait for the global lock (minutes).", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "mailnotification" : { + "default" : "always", + "description" : "Deprecated: use 'notification-policy' instead.", + "enum" : [ + "always", + "failure" + ], + "optional" : 1, + "type" : "string" + }, + "mailto" : { + "description" : "Comma-separated list of email addresses or users that should receive email notifications. Has no effect if the 'notification-target' option is set at the same time.", + "format" : "email-or-username-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "maxfiles" : { + "description" : "Deprecated: use 'prune-backups' instead. Maximal number of backup files per guest system.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "mode" : { + "default" : "snapshot", + "description" : "Backup mode.", + "enum" : [ + "snapshot", + "suspend", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "Only run if executed on this node.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "notes-template" : { + "description" : "Template string for generating notes for the backup(s). It can contain variables which will be replaced by their values. Currently supported are {{cluster}}, {{guestname}}, {{node}}, and {{vmid}}, but more might be added in the future. Needs to be a single line, newline and backslash need to be escaped as '\\n' and '\\\\' respectively.", + "maxLength" : 1024, + "optional" : 1, + "requires" : "storage", + "type" : "string", + "typetext" : "" + }, + "notification-policy" : { + "default" : "always", + "description" : "Specify when to send a notification", + "enum" : [ + "always", + "failure", + "never" + ], + "optional" : 1, + "type" : "string" + }, + "notification-target" : { + "description" : "Determine the target to which notifications should be sent. Can either be a notification endpoint or a notification group. This option takes precedence over 'mailto', meaning that if both are set, the 'mailto' option will be ignored.", + "format" : "pve-configid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "performance" : { + "description" : "Other performance-related settings.", + "format" : "backup-performance", + "optional" : 1, + "type" : "string", + "typetext" : "[max-workers=] [,pbs-entries-max=]" + }, + "pigz" : { + "default" : 0, + "description" : "Use pigz instead of gzip when N>0. N=1 uses half of cores, N>1 uses N as thread count.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "pool" : { + "description" : "Backup all known guest systems included in the specified pool.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "protected" : { + "description" : "If true, mark backup(s) as protected.", + "optional" : 1, + "requires" : "storage", + "type" : "boolean", + "typetext" : "" + }, + "prune-backups" : { + "default" : "keep-all=1", + "description" : "Use these retention options instead of those from the storage configuration.", + "format" : "prune-backups", + "optional" : 1, + "type" : "string", + "typetext" : "[keep-all=<1|0>] [,keep-daily=] [,keep-hourly=] [,keep-last=] [,keep-monthly=] [,keep-weekly=] [,keep-yearly=]" + }, + "quiet" : { + "default" : 0, + "description" : "Be quiet.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "remove" : { + "default" : 1, + "description" : "Prune older backups according to 'prune-backups'.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "repeat-missed" : { + "default" : 0, + "description" : "If true, the job will be run as soon as possible if it was missed while the scheduler was not running.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "schedule" : { + "description" : "Backup schedule. The format is a subset of `systemd` calendar events.", + "format" : "pve-calendar-event", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "script" : { + "description" : "Use specified hook script.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "starttime" : { + "description" : "Job Start time.", + "optional" : 1, + "pattern" : "\\d{1,2}:\\d{1,2}", + "type" : "string", + "typetext" : "HH:MM" + }, + "stdexcludes" : { + "default" : 1, + "description" : "Exclude temporary files and logs.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "stop" : { + "default" : 0, + "description" : "Stop running backup jobs on this host.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "stopwait" : { + "default" : 10, + "description" : "Maximal time to wait until a guest system is stopped (minutes).", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "storage" : { + "description" : "Store resulting file to this storage.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tmpdir" : { + "description" : "Store temporary files to specified directory.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The ID of the guest system you want to backup.", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "zstd" : { + "default" : 1, + "description" : "Zstd threads. N=0 uses half of the available cores, N>0 uses N as thread count.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ], + "description" : "The 'tmpdir', 'dumpdir' and 'script' parameters are additionally restricted to the 'root@pam' user." + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/backup", + "text" : "backup" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Shows all guests which are not covered by any backup job.", + "method" : "GET", + "name" : "get_guests_not_in_backup", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "description" : "Contains the guest objects.", + "items" : { + "properties" : { + "name" : { + "description" : "Name of the guest", + "optional" : 1, + "type" : "string" + }, + "type" : { + "description" : "Type of the guest.", + "enum" : [ + "qemu", + "lxc" + ], + "type" : "string" + }, + "vmid" : { + "description" : "VMID of the guest.", + "type" : "integer" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/backup-info/not-backed-up", + "text" : "not-backed-up" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Index for backup info related endpoints", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "returns" : { + "description" : "Directory index.", + "items" : { + "properties" : { + "subdir" : { + "description" : "API sub-directory endpoint", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{subdir}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/backup-info", + "text" : "backup-info" + }, + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Request resource migration (online) to another node.", + "method" : "POST", + "name" : "migrate", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "Target node.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "sid" : { + "description" : "HA resource ID. This consists of a resource type followed by a resource specific name, separated with colon (example: vm:100 / ct:100). For virtual machines and containers, you can simply use the VM or CT id as a shortcut (example: 100).", + "format" : "pve-ha-resource-or-vm-id", + "type" : "string", + "typetext" : ":" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/ha/resources/{sid}/migrate", + "text" : "migrate" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Request resource relocatzion to another node. This stops the service on the old node, and restarts it on the target node.", + "method" : "POST", + "name" : "relocate", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "Target node.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "sid" : { + "description" : "HA resource ID. This consists of a resource type followed by a resource specific name, separated with colon (example: vm:100 / ct:100). For virtual machines and containers, you can simply use the VM or CT id as a shortcut (example: 100).", + "format" : "pve-ha-resource-or-vm-id", + "type" : "string", + "typetext" : ":" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/ha/resources/{sid}/relocate", + "text" : "relocate" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete resource configuration.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "sid" : { + "description" : "HA resource ID. This consists of a resource type followed by a resource specific name, separated with colon (example: vm:100 / ct:100). For virtual machines and containers, you can simply use the VM or CT id as a shortcut (example: 100).", + "format" : "pve-ha-resource-or-vm-id", + "type" : "string", + "typetext" : ":" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read resource configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "sid" : { + "description" : "HA resource ID. This consists of a resource type followed by a resource specific name, separated with colon (example: vm:100 / ct:100). For virtual machines and containers, you can simply use the VM or CT id as a shortcut (example: 100).", + "format" : "pve-ha-resource-or-vm-id", + "type" : "string", + "typetext" : ":" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "properties" : { + "comment" : { + "description" : "Description.", + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Can be used to prevent concurrent modifications.", + "type" : "string" + }, + "group" : { + "description" : "The HA group identifier.", + "format" : "pve-configid", + "optional" : 1, + "type" : "string" + }, + "max_relocate" : { + "description" : "Maximal number of service relocate tries when a service failes to start.", + "optional" : 1, + "type" : "integer" + }, + "max_restart" : { + "description" : "Maximal number of tries to restart the service on a node after its start failed.", + "optional" : 1, + "type" : "integer" + }, + "sid" : { + "description" : "HA resource ID. This consists of a resource type followed by a resource specific name, separated with colon (example: vm:100 / ct:100). For virtual machines and containers, you can simply use the VM or CT id as a shortcut (example: 100).", + "format" : "pve-ha-resource-or-vm-id", + "type" : "string", + "typetext" : ":" + }, + "state" : { + "description" : "Requested resource state.", + "enum" : [ + "started", + "stopped", + "enabled", + "disabled", + "ignored" + ], + "optional" : 1, + "type" : "string" + }, + "type" : { + "description" : "The type of the resources.", + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update resource configuration.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Description.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group" : { + "description" : "The HA group identifier.", + "format" : "pve-configid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "max_relocate" : { + "default" : 1, + "description" : "Maximal number of service relocate tries when a service failes to start.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "max_restart" : { + "default" : 1, + "description" : "Maximal number of tries to restart the service on a node after its start failed.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "sid" : { + "description" : "HA resource ID. This consists of a resource type followed by a resource specific name, separated with colon (example: vm:100 / ct:100). For virtual machines and containers, you can simply use the VM or CT id as a shortcut (example: 100).", + "format" : "pve-ha-resource-or-vm-id", + "type" : "string", + "typetext" : ":" + }, + "state" : { + "default" : "started", + "description" : "Requested resource state.", + "enum" : [ + "started", + "stopped", + "enabled", + "disabled", + "ignored" + ], + "optional" : 1, + "type" : "string", + "verbose_description" : "Requested resource state. The CRM reads this state and acts accordingly.\nPlease note that `enabled` is just an alias for `started`.\n\n`started`;;\n\nThe CRM tries to start the resource. Service state is\nset to `started` after successful start. On node failures, or when start\nfails, it tries to recover the resource. If everything fails, service\nstate it set to `error`.\n\n`stopped`;;\n\nThe CRM tries to keep the resource in `stopped` state, but it\nstill tries to relocate the resources on node failures.\n\n`disabled`;;\n\nThe CRM tries to put the resource in `stopped` state, but does not try\nto relocate the resources on node failures. The main purpose of this\nstate is error recovery, because it is the only way to move a resource out\nof the `error` state.\n\n`ignored`;;\n\nThe resource gets removed from the manager status and so the CRM and the LRM do\nnot touch the resource anymore. All {pve} API calls affecting this resource\nwill be executed, directly bypassing the HA stack. CRM commands will be thrown\naway while there source is in this state. The resource will not get relocated\non node failures.\n\n" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/ha/resources/{sid}", + "text" : "{sid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List HA resources.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "type" : { + "description" : "Only list resources of specific type", + "enum" : [ + "ct", + "vm" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "sid" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{sid}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new HA resource.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Description.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group" : { + "description" : "The HA group identifier.", + "format" : "pve-configid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "max_relocate" : { + "default" : 1, + "description" : "Maximal number of service relocate tries when a service failes to start.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "max_restart" : { + "default" : 1, + "description" : "Maximal number of tries to restart the service on a node after its start failed.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "sid" : { + "description" : "HA resource ID. This consists of a resource type followed by a resource specific name, separated with colon (example: vm:100 / ct:100). For virtual machines and containers, you can simply use the VM or CT id as a shortcut (example: 100).", + "format" : "pve-ha-resource-or-vm-id", + "type" : "string", + "typetext" : ":" + }, + "state" : { + "default" : "started", + "description" : "Requested resource state.", + "enum" : [ + "started", + "stopped", + "enabled", + "disabled", + "ignored" + ], + "optional" : 1, + "type" : "string", + "verbose_description" : "Requested resource state. The CRM reads this state and acts accordingly.\nPlease note that `enabled` is just an alias for `started`.\n\n`started`;;\n\nThe CRM tries to start the resource. Service state is\nset to `started` after successful start. On node failures, or when start\nfails, it tries to recover the resource. If everything fails, service\nstate it set to `error`.\n\n`stopped`;;\n\nThe CRM tries to keep the resource in `stopped` state, but it\nstill tries to relocate the resources on node failures.\n\n`disabled`;;\n\nThe CRM tries to put the resource in `stopped` state, but does not try\nto relocate the resources on node failures. The main purpose of this\nstate is error recovery, because it is the only way to move a resource out\nof the `error` state.\n\n`ignored`;;\n\nThe resource gets removed from the manager status and so the CRM and the LRM do\nnot touch the resource anymore. All {pve} API calls affecting this resource\nwill be executed, directly bypassing the HA stack. CRM commands will be thrown\naway while there source is in this state. The resource will not get relocated\non node failures.\n\n" + }, + "type" : { + "description" : "Resource type.", + "enum" : [ + "ct", + "vm" + ], + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/ha/resources", + "text" : "resources" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete ha group configuration.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "group" : { + "description" : "The HA group identifier.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read ha group configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "group" : { + "description" : "The HA group identifier.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : {} + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update ha group configuration.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Description.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group" : { + "description" : "The HA group identifier.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "nodes" : { + "description" : "List of cluster node names with optional priority.", + "format" : "pve-ha-group-node-list", + "optional" : 1, + "type" : "string", + "typetext" : "[:]{,[:]}*", + "verbose_description" : "List of cluster node members, where a priority can be given to each node. A resource bound to a group will run on the available nodes with the highest priority. If there are more nodes in the highest priority class, the services will get distributed to those nodes. The priorities have a relative meaning only." + }, + "nofailback" : { + "default" : 0, + "description" : "The CRM tries to run services on the node with the highest priority. If a node with higher priority comes online, the CRM migrates the service to that node. Enabling nofailback prevents that behavior.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "restricted" : { + "default" : 0, + "description" : "Resources bound to restricted groups may only run on nodes defined by the group.", + "optional" : 1, + "type" : "boolean", + "typetext" : "", + "verbose_description" : "Resources bound to restricted groups may only run on nodes defined by the group. The resource will be placed in the stopped state if no group node member is online. Resources on unrestricted groups may run on any cluster node if all group members are offline, but they will migrate back as soon as a group member comes online. One can implement a 'preferred node' behavior using an unrestricted group with only one member." + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/ha/groups/{group}", + "text" : "{group}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get HA groups.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "group" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{group}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new HA group.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Description.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group" : { + "description" : "The HA group identifier.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "nodes" : { + "description" : "List of cluster node names with optional priority.", + "format" : "pve-ha-group-node-list", + "optional" : 0, + "type" : "string", + "typetext" : "[:]{,[:]}*", + "verbose_description" : "List of cluster node members, where a priority can be given to each node. A resource bound to a group will run on the available nodes with the highest priority. If there are more nodes in the highest priority class, the services will get distributed to those nodes. The priorities have a relative meaning only." + }, + "nofailback" : { + "default" : 0, + "description" : "The CRM tries to run services on the node with the highest priority. If a node with higher priority comes online, the CRM migrates the service to that node. Enabling nofailback prevents that behavior.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "restricted" : { + "default" : 0, + "description" : "Resources bound to restricted groups may only run on nodes defined by the group.", + "optional" : 1, + "type" : "boolean", + "typetext" : "", + "verbose_description" : "Resources bound to restricted groups may only run on nodes defined by the group. The resource will be placed in the stopped state if no group node member is online. Resources on unrestricted groups may run on any cluster node if all group members are offline, but they will migrate back as soon as a group member comes online. One can implement a 'preferred node' behavior using an unrestricted group with only one member." + }, + "type" : { + "description" : "Group type.", + "enum" : [ + "group" + ], + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/ha/groups", + "text" : "groups" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get HA manger status.", + "method" : "GET", + "name" : "status", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "crm_state" : { + "description" : "For type 'service'. Service state as seen by the CRM.", + "optional" : 1, + "type" : "string" + }, + "id" : { + "description" : "Status entry ID (quorum, master, lrm:, service:).", + "type" : "string" + }, + "max_relocate" : { + "description" : "For type 'service'.", + "optional" : 1, + "type" : "integer" + }, + "max_restart" : { + "description" : "For type 'service'.", + "optional" : 1, + "type" : "integer" + }, + "node" : { + "description" : "Node associated to status entry.", + "type" : "string" + }, + "quorate" : { + "description" : "For type 'quorum'. Whether the cluster is quorate or not.", + "optional" : 1, + "type" : "boolean" + }, + "request_state" : { + "description" : "For type 'service'. Requested service state.", + "optional" : 1, + "type" : "string" + }, + "sid" : { + "description" : "For type 'service'. Service ID.", + "optional" : 1, + "type" : "string" + }, + "state" : { + "description" : "For type 'service'. Verbose service state.", + "optional" : 1, + "type" : "string" + }, + "status" : { + "description" : "Status of the entry (value depends on type).", + "type" : "string" + }, + "timestamp" : { + "description" : "For type 'lrm','master'. Timestamp of the status information.", + "optional" : 1, + "type" : "integer" + }, + "type" : { + "description" : "Type of status entry.", + "enum" : [ + "quorum", + "master", + "lrm", + "service" + ] + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/ha/status/current", + "text" : "current" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get full HA manger status, including LRM status.", + "method" : "GET", + "name" : "manager_status", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/cluster/ha/status/manager_status", + "text" : "manager_status" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/ha/status", + "text" : "status" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "id" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/ha", + "text" : "ha" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete ACME plugin configuration.", + "method" : "DELETE", + "name" : "delete_plugin", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "Unique identifier for ACME plugin instance.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get ACME plugin configuration.", + "method" : "GET", + "name" : "get_plugin_config", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "Unique identifier for ACME plugin instance.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update ACME plugin configuration.", + "method" : "PUT", + "name" : "update_plugin", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "api" : { + "description" : "API plugin name", + "enum" : [ + "1984hosting", + "acmedns", + "acmeproxy", + "active24", + "ad", + "ali", + "anx", + "artfiles", + "arvan", + "aurora", + "autodns", + "aws", + "azion", + "azure", + "bookmyname", + "bunny", + "cf", + "clouddns", + "cloudns", + "cn", + "conoha", + "constellix", + "cpanel", + "curanet", + "cyon", + "da", + "ddnss", + "desec", + "df", + "dgon", + "dnsexit", + "dnshome", + "dnsimple", + "dnsservices", + "do", + "doapi", + "domeneshop", + "dp", + "dpi", + "dreamhost", + "duckdns", + "durabledns", + "dyn", + "dynu", + "dynv6", + "easydns", + "edgedns", + "euserv", + "exoscale", + "fornex", + "freedns", + "gandi_livedns", + "gcloud", + "gcore", + "gd", + "geoscaling", + "googledomains", + "he", + "hetzner", + "hexonet", + "hostingde", + "huaweicloud", + "infoblox", + "infomaniak", + "internetbs", + "inwx", + "ionos", + "ipv64", + "ispconfig", + "jd", + "joker", + "kappernet", + "kas", + "kinghost", + "knot", + "la", + "leaseweb", + "lexicon", + "linode", + "linode_v4", + "loopia", + "lua", + "maradns", + "me", + "miab", + "misaka", + "myapi", + "mydevil", + "mydnsjp", + "mythic_beasts", + "namecheap", + "namecom", + "namesilo", + "nanelo", + "nederhost", + "neodigit", + "netcup", + "netlify", + "nic", + "njalla", + "nm", + "nsd", + "nsone", + "nsupdate", + "nw", + "oci", + "one", + "online", + "openprovider", + "openstack", + "opnsense", + "ovh", + "pdns", + "pleskxml", + "pointhq", + "porkbun", + "rackcorp", + "rackspace", + "rage4", + "rcode0", + "regru", + "scaleway", + "schlundtech", + "selectel", + "selfhost", + "servercow", + "simply", + "tele3", + "tencent", + "transip", + "udr", + "ultra", + "unoeuro", + "variomedia", + "veesp", + "vercel", + "vscale", + "vultr", + "websupport", + "world4you", + "yandex", + "yc", + "zilore", + "zone", + "zonomi" + ], + "optional" : 1, + "type" : "string" + }, + "data" : { + "description" : "DNS plugin data. (base64 encoded)", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "description" : "Flag to disable the config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "id" : { + "description" : "ACME Plugin ID name", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "nodes" : { + "description" : "List of cluster node names.", + "format" : "pve-node-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "validation-delay" : { + "default" : 30, + "description" : "Extra delay in seconds to wait before requesting validation. Allows to cope with a long TTL of DNS records.", + "maximum" : 172800, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 172800)" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/acme/plugins/{id}", + "text" : "{id}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "ACME plugin index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "type" : { + "description" : "Only list ACME plugins of a specific type", + "enum" : [ + "dns", + "standalone" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : { + "plugin" : { + "description" : "Unique identifier for ACME plugin instance.", + "format" : "pve-configid", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{plugin}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Add ACME plugin configuration.", + "method" : "POST", + "name" : "add_plugin", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "api" : { + "description" : "API plugin name", + "enum" : [ + "1984hosting", + "acmedns", + "acmeproxy", + "active24", + "ad", + "ali", + "anx", + "artfiles", + "arvan", + "aurora", + "autodns", + "aws", + "azion", + "azure", + "bookmyname", + "bunny", + "cf", + "clouddns", + "cloudns", + "cn", + "conoha", + "constellix", + "cpanel", + "curanet", + "cyon", + "da", + "ddnss", + "desec", + "df", + "dgon", + "dnsexit", + "dnshome", + "dnsimple", + "dnsservices", + "do", + "doapi", + "domeneshop", + "dp", + "dpi", + "dreamhost", + "duckdns", + "durabledns", + "dyn", + "dynu", + "dynv6", + "easydns", + "edgedns", + "euserv", + "exoscale", + "fornex", + "freedns", + "gandi_livedns", + "gcloud", + "gcore", + "gd", + "geoscaling", + "googledomains", + "he", + "hetzner", + "hexonet", + "hostingde", + "huaweicloud", + "infoblox", + "infomaniak", + "internetbs", + "inwx", + "ionos", + "ipv64", + "ispconfig", + "jd", + "joker", + "kappernet", + "kas", + "kinghost", + "knot", + "la", + "leaseweb", + "lexicon", + "linode", + "linode_v4", + "loopia", + "lua", + "maradns", + "me", + "miab", + "misaka", + "myapi", + "mydevil", + "mydnsjp", + "mythic_beasts", + "namecheap", + "namecom", + "namesilo", + "nanelo", + "nederhost", + "neodigit", + "netcup", + "netlify", + "nic", + "njalla", + "nm", + "nsd", + "nsone", + "nsupdate", + "nw", + "oci", + "one", + "online", + "openprovider", + "openstack", + "opnsense", + "ovh", + "pdns", + "pleskxml", + "pointhq", + "porkbun", + "rackcorp", + "rackspace", + "rage4", + "rcode0", + "regru", + "scaleway", + "schlundtech", + "selectel", + "selfhost", + "servercow", + "simply", + "tele3", + "tencent", + "transip", + "udr", + "ultra", + "unoeuro", + "variomedia", + "veesp", + "vercel", + "vscale", + "vultr", + "websupport", + "world4you", + "yandex", + "yc", + "zilore", + "zone", + "zonomi" + ], + "optional" : 1, + "type" : "string" + }, + "data" : { + "description" : "DNS plugin data. (base64 encoded)", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "description" : "Flag to disable the config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "id" : { + "description" : "ACME Plugin ID name", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "nodes" : { + "description" : "List of cluster node names.", + "format" : "pve-node-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "ACME challenge type.", + "enum" : [ + "dns", + "standalone" + ], + "type" : "string" + }, + "validation-delay" : { + "default" : 30, + "description" : "Extra delay in seconds to wait before requesting validation. Allows to cope with a long TTL of DNS records.", + "maximum" : 172800, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 172800)" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/acme/plugins", + "text" : "plugins" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Deactivate existing ACME account at CA.", + "method" : "DELETE", + "name" : "deactivate_account", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "default" : "default", + "description" : "ACME account config file name.", + "format" : "pve-configid", + "format_description" : "name", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "protected" : 1, + "returns" : { + "type" : "string" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Return existing ACME account information.", + "method" : "GET", + "name" : "get_account", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "default" : "default", + "description" : "ACME account config file name.", + "format" : "pve-configid", + "format_description" : "name", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "protected" : 1, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "account" : { + "optional" : 1, + "renderer" : "yaml", + "type" : "object" + }, + "directory" : { + "description" : "URL of ACME CA directory endpoint.", + "optional" : 1, + "pattern" : "^https?://.*", + "type" : "string" + }, + "location" : { + "optional" : 1, + "type" : "string" + }, + "tos" : { + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.", + "method" : "PUT", + "name" : "update_account", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "contact" : { + "description" : "Contact email addresses.", + "format" : "email-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "default" : "default", + "description" : "ACME account config file name.", + "format" : "pve-configid", + "format_description" : "name", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "protected" : 1, + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/cluster/acme/account/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "ACMEAccount index.", + "method" : "GET", + "name" : "account_index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Register a new ACME account with CA.", + "method" : "POST", + "name" : "register_account", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "contact" : { + "description" : "Contact email addresses.", + "format" : "email-list", + "type" : "string", + "typetext" : "" + }, + "directory" : { + "default" : "https://acme-v02.api.letsencrypt.org/directory", + "description" : "URL of ACME CA directory endpoint.", + "optional" : 1, + "pattern" : "^https?://.*", + "type" : "string" + }, + "eab-hmac-key" : { + "description" : "HMAC key for External Account Binding.", + "optional" : 1, + "requires" : "eab-kid", + "type" : "string", + "typetext" : "" + }, + "eab-kid" : { + "description" : "Key Identifier for External Account Binding.", + "optional" : 1, + "requires" : "eab-hmac-key", + "type" : "string", + "typetext" : "" + }, + "name" : { + "default" : "default", + "description" : "ACME account config file name.", + "format" : "pve-configid", + "format_description" : "name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tos_url" : { + "description" : "URL of CA TermsOfService - setting this indicates agreement.", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "protected" : 1, + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/cluster/acme/account", + "text" : "account" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Retrieve ACME TermsOfService URL from CA. Deprecated, please use /cluster/acme/meta.", + "method" : "GET", + "name" : "get_tos", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "directory" : { + "default" : "https://acme-v02.api.letsencrypt.org/directory", + "description" : "URL of ACME CA directory endpoint.", + "optional" : 1, + "pattern" : "^https?://.*", + "type" : "string" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "description" : "ACME TermsOfService URL.", + "optional" : 1, + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/cluster/acme/tos", + "text" : "tos" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Retrieve ACME Directory Meta Information", + "method" : "GET", + "name" : "get_meta", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "directory" : { + "default" : "https://acme-v02.api.letsencrypt.org/directory", + "description" : "URL of ACME CA directory endpoint.", + "optional" : 1, + "pattern" : "^https?://.*", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "additionalProperties" : 1, + "properties" : { + "caaIdentities" : { + "description" : "Hostnames referring to the ACME servers.", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "externalAccountRequired" : { + "description" : "EAB Required", + "optional" : 1, + "type" : "boolean" + }, + "termsOfService" : { + "description" : "ACME TermsOfService URL.", + "optional" : 1, + "type" : "string" + }, + "website" : { + "description" : "URL to more information about the ACME server.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/cluster/acme/meta", + "text" : "meta" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get named known ACME directory endpoints.", + "method" : "GET", + "name" : "get_directories", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "type" : "string" + }, + "url" : { + "description" : "URL of ACME CA directory endpoint.", + "pattern" : "^https?://.*", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/acme/directories", + "text" : "directories" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get schema of ACME challenge types.", + "method" : "GET", + "name" : "challengeschema", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "type" : "string" + }, + "name" : { + "description" : "Human readable name, falls back to id", + "type" : "string" + }, + "schema" : { + "type" : "object" + }, + "type" : { + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/acme/challenge-schema", + "text" : "challenge-schema" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "ACMEAccount index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/acme", + "text" : "acme" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get ceph metadata.", + "method" : "GET", + "name" : "metadata", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "scope" : { + "default" : "all", + "enum" : [ + "all", + "versions" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "returns" : { + "description" : "Items for each type of service containing objects for each instance.", + "properties" : { + "mds" : { + "description" : "Metadata servers configured in the cluster and their properties.", + "properties" : { + "{id}" : { + "description" : "Useful properties are listed, but not the full list.", + "properties" : { + "addr" : { + "description" : "Bind addresses and ports.", + "type" : "string" + }, + "ceph_release" : { + "description" : "Ceph release codename currently used.", + "type" : "string" + }, + "ceph_version" : { + "description" : "Version info currently used by the service.", + "type" : "string" + }, + "ceph_version_short" : { + "description" : "Short version (numerical) info currently used by the service.", + "type" : "string" + }, + "hostname" : { + "description" : "Hostname on which the service is running.", + "type" : "string" + }, + "mem_swap_kb" : { + "description" : "Memory of the service currently in swap.", + "type" : "integer" + }, + "mem_total_kb" : { + "description" : "Memory consumption of the service.", + "type" : "integer" + }, + "name" : { + "description" : "Name of the service instance.", + "type" : "string" + } + }, + "type" : "object" + } + }, + "type" : "object" + }, + "mgr" : { + "description" : "Managers configured in the cluster and their properties.", + "properties" : { + "{id}" : { + "description" : "Useful properties are listed, but not the full list.", + "properties" : { + "addr" : { + "description" : "Bind address", + "type" : "string" + }, + "ceph_release" : { + "description" : "Ceph release codename currently used.", + "type" : "string" + }, + "ceph_version" : { + "description" : "Version info currently used by the service.", + "type" : "string" + }, + "ceph_version_short" : { + "description" : "Short version (numerical) info currently used by the service.", + "type" : "string" + }, + "hostname" : { + "description" : "Hostname on which the service is running.", + "type" : "string" + }, + "mem_swap_kb" : { + "description" : "Memory of the service currently in swap.", + "type" : "integer" + }, + "mem_total_kb" : { + "description" : "Memory consumption of the service.", + "type" : "integer" + }, + "name" : { + "description" : "Name of the service instance.", + "type" : "string" + } + }, + "type" : "object" + } + }, + "type" : "object" + }, + "mon" : { + "description" : "Monitors configured in the cluster and their properties.", + "properties" : { + "{id}" : { + "description" : "Useful properties are listed, but not the full list.", + "properties" : { + "addrs" : { + "description" : "Bind addresses and ports.", + "type" : "string" + }, + "ceph_release" : { + "description" : "Ceph release codename currently used.", + "type" : "string" + }, + "ceph_version" : { + "description" : "Version info currently used by the service.", + "type" : "string" + }, + "ceph_version_short" : { + "description" : "Short version (numerical) info currently used by the service.", + "type" : "string" + }, + "hostname" : { + "description" : "Hostname on which the service is running.", + "type" : "string" + }, + "mem_swap_kb" : { + "description" : "Memory of the service currently in swap.", + "type" : "integer" + }, + "mem_total_kb" : { + "description" : "Memory consumption of the service.", + "type" : "integer" + }, + "name" : { + "description" : "Name of the service instance.", + "type" : "string" + } + }, + "type" : "object" + } + }, + "type" : "object" + }, + "node" : { + "description" : "Ceph version installed on the nodes.", + "properties" : { + "{node}" : { + "properties" : { + "buildcommit" : { + "description" : "GIT commit used for the build.", + "type" : "string" + }, + "version" : { + "description" : "Version info.", + "properties" : { + "parts" : { + "description" : "major, minor & patch", + "type" : "array" + }, + "str" : { + "description" : "Version as single string.", + "type" : "string" + } + }, + "type" : "object" + } + }, + "type" : "object" + } + }, + "type" : "object" + }, + "osd" : { + "description" : "OSDs configured in the cluster and their properties.", + "properties" : { + "{id}" : { + "description" : "Useful properties are listed, but not the full list.", + "properties" : { + "back_addr" : { + "description" : "Bind addresses and ports for backend inter OSD traffic.", + "type" : "string" + }, + "ceph_release" : { + "description" : "Ceph release codename currently used.", + "type" : "string" + }, + "ceph_version" : { + "description" : "Version info currently used by the service.", + "type" : "string" + }, + "ceph_version_short" : { + "description" : "Short version (numerical) info currently used by the service.", + "type" : "string" + }, + "device_id" : { + "description" : "Devices used by the OSD.", + "type" : "string" + }, + "front_addr" : { + "description" : "Bind addresses and ports for frontend traffic to OSDs.", + "type" : "string" + }, + "hostname" : { + "description" : "Hostname on which the service is running.", + "type" : "string" + }, + "id" : { + "description" : "OSD ID.", + "type" : "integer" + }, + "mem_swap_kb" : { + "description" : "Memory of the service currently in swap.", + "type" : "integer" + }, + "mem_total_kb" : { + "description" : "Memory consumption of the service.", + "type" : "integer" + }, + "osd_data" : { + "description" : "Path to the OSD data directory.", + "type" : "string" + }, + "osd_objectstore" : { + "description" : "OSD objectstore type.", + "type" : "string" + } + }, + "type" : "object" + } + }, + "type" : "array" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/cluster/ceph/metadata", + "text" : "metadata" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get ceph status.", + "method" : "GET", + "name" : "status", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "returns" : { + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/cluster/ceph/status", + "text" : "status" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get the status of a specific ceph flag.", + "method" : "GET", + "name" : "get_flag", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "flag" : { + "description" : "The name of the flag name to get.", + "enum" : [ + "nobackfill", + "nodeep-scrub", + "nodown", + "noin", + "noout", + "norebalance", + "norecover", + "noscrub", + "notieragent", + "noup", + "pause" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "boolean" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set or clear (unset) a specific ceph flag", + "method" : "PUT", + "name" : "update_flag", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "flag" : { + "description" : "The ceph flag to update", + "enum" : [ + "nobackfill", + "nodeep-scrub", + "nodown", + "noin", + "noout", + "norebalance", + "norecover", + "noscrub", + "notieragent", + "noup", + "pause" + ], + "type" : "string" + }, + "value" : { + "description" : "The new value of the flag", + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/ceph/flags/{flag}", + "text" : "{flag}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "get the status of all ceph flags", + "method" : "GET", + "name" : "get_all_flags", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "additionalProperties" : 1, + "properties" : { + "description" : { + "description" : "Flag description.", + "type" : "string" + }, + "name" : { + "description" : "Flag name.", + "enum" : [ + "nobackfill", + "nodeep-scrub", + "nodown", + "noin", + "noout", + "norebalance", + "norecover", + "noscrub", + "notieragent", + "noup", + "pause" + ], + "type" : "string" + }, + "value" : { + "description" : "Flag value.", + "type" : "boolean" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set/Unset multiple ceph flags at once.", + "method" : "PUT", + "name" : "set_flags", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "nobackfill" : { + "description" : "Backfilling of PGs is suspended.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "nodeep-scrub" : { + "description" : "Deep Scrubbing is disabled.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "nodown" : { + "description" : "OSD failure reports are being ignored, such that the monitors will not mark OSDs down.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "noin" : { + "description" : "OSDs that were previously marked out will not be marked back in when they start.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "noout" : { + "description" : "OSDs will not automatically be marked out after the configured interval.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "norebalance" : { + "description" : "Rebalancing of PGs is suspended.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "norecover" : { + "description" : "Recovery of PGs is suspended.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "noscrub" : { + "description" : "Scrubbing is disabled.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "notieragent" : { + "description" : "Cache tiering activity is suspended.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "noup" : { + "description" : "OSDs are not allowed to start.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "pause" : { + "description" : "Pauses read and writes.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/cluster/ceph/flags", + "text" : "flags" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Cluster ceph index.", + "method" : "GET", + "name" : "cephindex", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/ceph", + "text" : "ceph" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete realm-sync job definition.", + "method" : "DELETE", + "name" : "delete_job", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read realm-sync job definition.", + "method" : "GET", + "name" : "read_job", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new realm-sync job.", + "method" : "POST", + "name" : "create_job", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Description for the Job.", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable-new" : { + "default" : "1", + "description" : "Enable newly synced users immediately.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "enabled" : { + "default" : 1, + "description" : "Determines if the job is enabled.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "id" : { + "description" : "The ID of the job.", + "format" : "pve-configid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + }, + "realm" : { + "description" : "Authentication domain ID", + "format" : "pve-realm", + "maxLength" : 32, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "remove-vanished" : { + "default" : "none", + "description" : "A semicolon-seperated list of things to remove when they or the user vanishes during a sync. The following values are possible: 'entry' removes the user/group when not returned from the sync. 'properties' removes the set properties on existing user/group that do not appear in the source (even custom ones). 'acl' removes acls when the user/group is not returned from the sync. Instead of a list it also can be 'none' (the default).", + "optional" : 1, + "pattern" : "(?:(?:(?:acl|properties|entry);)*(?:acl|properties|entry))|none", + "type" : "string", + "typetext" : "([acl];[properties];[entry])|none" + }, + "schedule" : { + "description" : "Backup schedule. The format is a subset of `systemd` calendar events.", + "format" : "pve-calendar-event", + "maxLength" : 128, + "type" : "string", + "typetext" : "" + }, + "scope" : { + "description" : "Select what to sync.", + "enum" : [ + "users", + "groups", + "both" + ], + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "and", + [ + "perm", + "/access/realm/{realm}", + [ + "Realm.AllocateUser" + ] + ], + [ + "perm", + "/access/groups", + [ + "User.Modify" + ] + ] + ], + "description" : "'Realm.AllocateUser' on '/access/realm/' and 'User.Modify' permissions to '/access/groups/'." + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update realm-sync job definition.", + "method" : "PUT", + "name" : "update_job", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "description" : "Description for the Job.", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable-new" : { + "default" : "1", + "description" : "Enable newly synced users immediately.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "enabled" : { + "default" : 1, + "description" : "Determines if the job is enabled.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "id" : { + "description" : "The ID of the job.", + "format" : "pve-configid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + }, + "remove-vanished" : { + "default" : "none", + "description" : "A semicolon-seperated list of things to remove when they or the user vanishes during a sync. The following values are possible: 'entry' removes the user/group when not returned from the sync. 'properties' removes the set properties on existing user/group that do not appear in the source (even custom ones). 'acl' removes acls when the user/group is not returned from the sync. Instead of a list it also can be 'none' (the default).", + "optional" : 1, + "pattern" : "(?:(?:(?:acl|properties|entry);)*(?:acl|properties|entry))|none", + "type" : "string", + "typetext" : "([acl];[properties];[entry])|none" + }, + "schedule" : { + "description" : "Backup schedule. The format is a subset of `systemd` calendar events.", + "format" : "pve-calendar-event", + "maxLength" : 128, + "type" : "string", + "typetext" : "" + }, + "scope" : { + "description" : "Select what to sync.", + "enum" : [ + "users", + "groups", + "both" + ], + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "and", + [ + "perm", + "/access/realm/{realm}", + [ + "Realm.AllocateUser" + ] + ], + [ + "perm", + "/access/groups", + [ + "User.Modify" + ] + ] + ], + "description" : "'Realm.AllocateUser' on '/access/realm/' and 'User.Modify' permissions to '/access/groups/'." + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/jobs/realm-sync/{id}", + "text" : "{id}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List configured realm-sync-jobs.", + "method" : "GET", + "name" : "syncjob_index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "description" : "A comment for the job.", + "optional" : 1, + "type" : "string" + }, + "enabled" : { + "description" : "If the job is enabled or not.", + "type" : "boolean" + }, + "id" : { + "description" : "The ID of the entry.", + "type" : "string" + }, + "last-run" : { + "description" : "Last execution time of the job in seconds since the beginning of the UNIX epoch", + "optional" : 1, + "type" : "integer" + }, + "next-run" : { + "description" : "Next planned execution time of the job in seconds since the beginning of the UNIX epoch.", + "optional" : 1, + "type" : "integer" + }, + "realm" : { + "description" : "Authentication domain ID", + "format" : "pve-realm", + "maxLength" : 32, + "type" : "string" + }, + "remove-vanished" : { + "default" : "none", + "description" : "A semicolon-seperated list of things to remove when they or the user vanishes during a sync. The following values are possible: 'entry' removes the user/group when not returned from the sync. 'properties' removes the set properties on existing user/group that do not appear in the source (even custom ones). 'acl' removes acls when the user/group is not returned from the sync. Instead of a list it also can be 'none' (the default).", + "optional" : "1", + "pattern" : "(?:(?:(?:acl|properties|entry);)*(?:acl|properties|entry))|none", + "type" : "string", + "typetext" : "([acl];[properties];[entry])|none" + }, + "schedule" : { + "description" : "The configured sync schedule.", + "type" : "string" + }, + "scope" : { + "description" : "Select what to sync.", + "enum" : [ + "users", + "groups", + "both" + ], + "optional" : "1", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/jobs/realm-sync", + "text" : "realm-sync" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Returns a list of future schedule runtimes.", + "method" : "GET", + "name" : "schedule-analyze", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "iterations" : { + "default" : 10, + "description" : "Number of event-iteration to simulate and return.", + "maximum" : 100, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 100)" + }, + "schedule" : { + "description" : "Job schedule. The format is a subset of `systemd` calendar events.", + "format" : "pve-calendar-event", + "maxLength" : 128, + "type" : "string", + "typetext" : "" + }, + "starttime" : { + "description" : "UNIX timestamp to start the calculation from. Defaults to the current time.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "description" : "An array of the next events since .", + "items" : { + "properties" : { + "timestamp" : { + "description" : "UNIX timestamp for the run.", + "type" : "integer" + }, + "utc" : { + "description" : "UTC timestamp for the run.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/jobs/schedule-analyze", + "text" : "schedule-analyze" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Index for jobs related endpoints.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "description" : "Directory index.", + "items" : { + "properties" : { + "subdir" : { + "description" : "API sub-directory endpoint", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{subdir}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/jobs", + "text" : "jobs" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove Hardware Mapping.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/pci", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get PCI Mapping.", + "method" : "GET", + "name" : "get", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/pci/{id}", + [ + "Mapping.Use" + ] + ], + [ + "perm", + "/mapping/pci/{id}", + [ + "Mapping.Modify" + ] + ], + [ + "perm", + "/mapping/pci/{id}", + [ + "Mapping.Audit" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update a hardware mapping.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "description" : { + "description" : "Description of the logical PCI device.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "id" : { + "description" : "The ID of the logical PCI mapping.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "map" : { + "description" : "A list of maps for the cluster nodes.", + "items" : { + "format" : { + "description" : { + "description" : "Description of the node specific device.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string" + }, + "id" : { + "description" : "The vendor and device ID that is expected. Used for detecting hardware changes", + "pattern" : "(?^:^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}$)", + "type" : "string" + }, + "iommugroup" : { + "description" : "The IOMMU group in which the device is to be expected in. Used for detecting hardware changes.", + "optional" : 1, + "type" : "integer" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string" + }, + "path" : { + "description" : "The path to the device. If the function is omitted, the whole device is mapped. In that case use the attributes of the first device. You can give multiple paths as a semicolon seperated list, the first available will then be chosen on guest start.", + "pattern" : "(?:[a-f0-9]{4,}:[a-f0-9]{2}:[a-f0-9]{2}(?:.[a-f0-9])?;)*[a-f0-9]{4,}:[a-f0-9]{2}:[a-f0-9]{2}(?:.[a-f0-9])?", + "type" : "string" + }, + "subsystem-id" : { + "description" : "The subsystem vendor and device ID that is expected. Used for detecting hardware changes.", + "optional" : 1, + "pattern" : "(?^:^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}$)", + "type" : "string" + } + }, + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "mdev" : { + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/pci/{id}", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/mapping/pci/{id}", + "text" : "{id}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List PCI Hardware Mapping", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "check-node" : { + "description" : "If given, checks the configurations on the given node for correctness, and adds relevant diagnostics for the devices to the response.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or 'Mapping.Audit' permissions on '/mapping/pci/'.", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "checks" : { + "description" : "A list of checks, only present if 'check_node' is set.", + "items" : { + "properties" : { + "message" : { + "description" : "The message of the error", + "type" : "string" + }, + "severity" : { + "description" : "The severity of the error", + "enum" : [ + "warning", + "error" + ], + "type" : "string" + } + }, + "type" : "object" + }, + "optional" : 1, + "type" : "array" + }, + "description" : { + "description" : "A description of the logical mapping.", + "type" : "string" + }, + "id" : { + "description" : "The logical ID of the mapping.", + "type" : "string" + }, + "map" : { + "description" : "The entries of the mapping.", + "items" : { + "description" : "A mapping for a node.", + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new hardware mapping.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "description" : { + "description" : "Description of the logical PCI device.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "id" : { + "description" : "The ID of the logical PCI mapping.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "map" : { + "description" : "A list of maps for the cluster nodes.", + "items" : { + "format" : { + "description" : { + "description" : "Description of the node specific device.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string" + }, + "id" : { + "description" : "The vendor and device ID that is expected. Used for detecting hardware changes", + "pattern" : "(?^:^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}$)", + "type" : "string" + }, + "iommugroup" : { + "description" : "The IOMMU group in which the device is to be expected in. Used for detecting hardware changes.", + "optional" : 1, + "type" : "integer" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string" + }, + "path" : { + "description" : "The path to the device. If the function is omitted, the whole device is mapped. In that case use the attributes of the first device. You can give multiple paths as a semicolon seperated list, the first available will then be chosen on guest start.", + "pattern" : "(?:[a-f0-9]{4,}:[a-f0-9]{2}:[a-f0-9]{2}(?:.[a-f0-9])?;)*[a-f0-9]{4,}:[a-f0-9]{2}:[a-f0-9]{2}(?:.[a-f0-9])?", + "type" : "string" + }, + "subsystem-id" : { + "description" : "The subsystem vendor and device ID that is expected. Used for detecting hardware changes.", + "optional" : 1, + "pattern" : "(?^:^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}$)", + "type" : "string" + } + }, + "type" : "string" + }, + "optional" : 0, + "type" : "array", + "typetext" : "" + }, + "mdev" : { + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/pci", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/mapping/pci", + "text" : "pci" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove Hardware Mapping.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/usb", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get USB Mapping.", + "method" : "GET", + "name" : "get", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/mapping/usb/{id}", + [ + "Mapping.Audit" + ] + ], + [ + "perm", + "/mapping/usb/{id}", + [ + "Mapping.Use" + ] + ], + [ + "perm", + "/mapping/usb/{id}", + [ + "Mapping.Modify" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update a hardware mapping.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "description" : { + "description" : "Description of the logical USB device.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "id" : { + "description" : "The ID of the logical USB mapping.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "map" : { + "description" : "A list of maps for the cluster nodes.", + "items" : { + "format" : { + "description" : { + "description" : "Description of the node specific device.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string" + }, + "id" : { + "description" : "The vendor and device ID that is expected. If a USB path is given, it is only used for detecting hardware changes", + "pattern" : "(?^:^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}$)", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string" + }, + "path" : { + "description" : "The path to the usb device.", + "optional" : 1, + "pattern" : "(?^:^(\\d+)\\-(\\d+(\\.\\d+)*)$)", + "type" : "string" + } + }, + "type" : "string" + }, + "type" : "array", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/usb/{id}", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/mapping/usb/{id}", + "text" : "{id}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List USB Hardware Mappings", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "check-node" : { + "description" : "If given, checks the configurations on the given node for correctness, and adds relevant errors to the devices.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or 'Mapping.Audit' permissions on '/mapping/usb/'.", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "description" : { + "description" : "A description of the logical mapping.", + "type" : "string" + }, + "error" : { + "description" : "A list of errors when 'check_node' is given.", + "items" : { + "properties" : { + "message" : { + "description" : "The message of the error", + "type" : "string" + }, + "severity" : { + "description" : "The severity of the error", + "type" : "string" + } + }, + "type" : "object" + } + }, + "id" : { + "description" : "The logical ID of the mapping.", + "type" : "string" + }, + "map" : { + "description" : "The entries of the mapping.", + "items" : { + "description" : "A mapping for a node.", + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new hardware mapping.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "description" : { + "description" : "Description of the logical USB device.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "id" : { + "description" : "The ID of the logical USB mapping.", + "format" : "pve-configid", + "type" : "string", + "typetext" : "" + }, + "map" : { + "description" : "A list of maps for the cluster nodes.", + "items" : { + "format" : { + "description" : { + "description" : "Description of the node specific device.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string" + }, + "id" : { + "description" : "The vendor and device ID that is expected. If a USB path is given, it is only used for detecting hardware changes", + "pattern" : "(?^:^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}$)", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string" + }, + "path" : { + "description" : "The path to the usb device.", + "optional" : 1, + "pattern" : "(?^:^(\\d+)\\-(\\d+(\\.\\d+)*)$)", + "type" : "string" + } + }, + "type" : "string" + }, + "type" : "array", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/mapping/usb", + [ + "Mapping.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/mapping/usb", + "text" : "usb" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List resource types.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster/mapping", + "text" : "mapping" + }, + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete sdn subnet object configuration.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "subnet" : { + "description" : "The SDN subnet object identifier.", + "format" : "pve-sdn-subnet-id", + "type" : "string", + "typetext" : "" + }, + "vnet" : { + "description" : "The SDN vnet object identifier.", + "format" : "pve-sdn-vnet-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Require 'SDN.Allocate' permission on '/sdn/zones//'", + "user" : "all" + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read sdn subnet configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "pending" : { + "description" : "Display pending config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "running" : { + "description" : "Display running config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "subnet" : { + "description" : "The SDN subnet object identifier.", + "format" : "pve-sdn-subnet-id", + "type" : "string", + "typetext" : "" + }, + "vnet" : { + "description" : "The SDN vnet object identifier.", + "format" : "pve-sdn-vnet-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Require 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/zones//'", + "user" : "all" + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update sdn subnet object configuration.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dhcp-dns-server" : { + "description" : "IP address for the DNS server", + "format" : "ip", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dhcp-range" : { + "description" : "A list of DHCP ranges for this subnet", + "items" : { + "format" : "pve-sdn-dhcp-range", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dnszoneprefix" : { + "description" : "dns domain zone prefix ex: 'adm' -> .adm.mydomain.com", + "format" : "dns-name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "gateway" : { + "description" : "Subnet Gateway: Will be assign on vnet for layer3 zones", + "format" : "ip", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "snat" : { + "description" : "enable masquerade for this subnet if pve-firewall", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "subnet" : { + "description" : "The SDN subnet object identifier.", + "format" : "pve-sdn-subnet-id", + "type" : "string", + "typetext" : "" + }, + "vnet" : { + "description" : "associated vnet", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "description" : "Require 'SDN.Allocate' permission on '/sdn/zones//'", + "user" : "all" + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/sdn/vnets/{vnet}/subnets/{subnet}", + "text" : "{subnet}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "SDN subnets index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "pending" : { + "description" : "Display pending config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "running" : { + "description" : "Display running config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vnet" : { + "description" : "The SDN vnet object identifier.", + "format" : "pve-sdn-vnet-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/zones//'", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{subnet}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new sdn subnet object.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "dhcp-dns-server" : { + "description" : "IP address for the DNS server", + "format" : "ip", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dhcp-range" : { + "description" : "A list of DHCP ranges for this subnet", + "items" : { + "format" : "pve-sdn-dhcp-range", + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "dnszoneprefix" : { + "description" : "dns domain zone prefix ex: 'adm' -> .adm.mydomain.com", + "format" : "dns-name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "gateway" : { + "description" : "Subnet Gateway: Will be assign on vnet for layer3 zones", + "format" : "ip", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "snat" : { + "description" : "enable masquerade for this subnet if pve-firewall", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "subnet" : { + "description" : "The SDN subnet object identifier.", + "format" : "pve-sdn-subnet-id", + "type" : "string", + "typetext" : "" + }, + "type" : { + "enum" : [ + "subnet" + ], + "type" : "string" + }, + "vnet" : { + "description" : "associated vnet", + "optional" : 0, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "description" : "Require 'SDN.Allocate' permission on '/sdn/zones//'", + "user" : "all" + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/sdn/vnets/{vnet}/subnets", + "text" : "subnets" + }, + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete IP Mappings in a VNet", + "method" : "DELETE", + "name" : "ipdelete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "ip" : { + "description" : "The IP address to delete", + "format" : "ip", + "type" : "string", + "typetext" : "" + }, + "mac" : { + "description" : "Unicast MAC address.", + "format" : "mac-addr", + "format_description" : "XX:XX:XX:XX:XX:XX", + "optional" : 1, + "type" : "string", + "typetext" : "", + "verbose_description" : "A common MAC address with the I/G (Individual/Group) bit not set." + }, + "vnet" : { + "description" : "The SDN vnet object identifier.", + "format" : "pve-sdn-vnet-id", + "type" : "string", + "typetext" : "" + }, + "zone" : { + "description" : "The SDN zone object identifier.", + "format" : "pve-sdn-zone-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/zones/{zone}/{vnet}", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create IP Mapping in a VNet", + "method" : "POST", + "name" : "ipcreate", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "ip" : { + "description" : "The IP address to associate with the given MAC address", + "format" : "ip", + "type" : "string", + "typetext" : "" + }, + "mac" : { + "description" : "Unicast MAC address.", + "format" : "mac-addr", + "format_description" : "XX:XX:XX:XX:XX:XX", + "optional" : 1, + "type" : "string", + "typetext" : "", + "verbose_description" : "A common MAC address with the I/G (Individual/Group) bit not set." + }, + "vnet" : { + "description" : "The SDN vnet object identifier.", + "format" : "pve-sdn-vnet-id", + "type" : "string", + "typetext" : "" + }, + "zone" : { + "description" : "The SDN zone object identifier.", + "format" : "pve-sdn-zone-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/zones/{zone}/{vnet}", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update IP Mapping in a VNet", + "method" : "PUT", + "name" : "ipupdate", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "ip" : { + "description" : "The IP address to associate with the given MAC address", + "format" : "ip", + "type" : "string", + "typetext" : "" + }, + "mac" : { + "description" : "Unicast MAC address.", + "format" : "mac-addr", + "format_description" : "XX:XX:XX:XX:XX:XX", + "optional" : 1, + "type" : "string", + "typetext" : "", + "verbose_description" : "A common MAC address with the I/G (Individual/Group) bit not set." + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "vnet" : { + "description" : "The SDN vnet object identifier.", + "format" : "pve-sdn-vnet-id", + "type" : "string", + "typetext" : "" + }, + "zone" : { + "description" : "The SDN zone object identifier.", + "format" : "pve-sdn-zone-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/zones/{zone}/{vnet}", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/sdn/vnets/{vnet}/ips", + "text" : "ips" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete sdn vnet object configuration.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "vnet" : { + "description" : "The SDN vnet object identifier.", + "format" : "pve-sdn-vnet-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Require 'SDN.Allocate' permission on '/sdn/zones//'", + "user" : "all" + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read sdn vnet configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "pending" : { + "description" : "Display pending config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "running" : { + "description" : "Display running config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vnet" : { + "description" : "The SDN vnet object identifier.", + "format" : "pve-sdn-vnet-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Require 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/zones//'", + "user" : "all" + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update sdn vnet object configuration.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "alias" : { + "description" : "alias name of the vnet", + "maxLength" : 256, + "optional" : 1, + "pattern" : "(?^i:[\\(\\)-_.\\w\\d\\s]{0,256})", + "type" : "string" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tag" : { + "description" : "vlan or vxlan id", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "vlanaware" : { + "description" : "Allow vm VLANs to pass through this vnet.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vnet" : { + "description" : "The SDN vnet object identifier.", + "format" : "pve-sdn-vnet-id", + "type" : "string", + "typetext" : "" + }, + "zone" : { + "description" : "zone id", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "description" : "Require 'SDN.Allocate' permission on '/sdn/zones//'", + "user" : "all" + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/sdn/vnets/{vnet}", + "text" : "{vnet}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "SDN vnets index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "pending" : { + "description" : "Display pending config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "running" : { + "description" : "Display running config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/zones//'", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{vnet}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new sdn vnet object.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "alias" : { + "description" : "alias name of the vnet", + "maxLength" : 256, + "optional" : 1, + "pattern" : "(?^i:[\\(\\)-_.\\w\\d\\s]{0,256})", + "type" : "string" + }, + "tag" : { + "description" : "vlan or vxlan id", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "type" : { + "description" : "Type", + "enum" : [ + "vnet" + ], + "optional" : 1, + "type" : "string" + }, + "vlanaware" : { + "description" : "Allow vm VLANs to pass through this vnet.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vnet" : { + "description" : "The SDN vnet object identifier.", + "format" : "pve-sdn-vnet-id", + "type" : "string", + "typetext" : "" + }, + "zone" : { + "description" : "zone id", + "optional" : 0, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/zones/{zone}", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/sdn/vnets", + "text" : "vnets" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete sdn zone object configuration.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "zone" : { + "description" : "The SDN zone object identifier.", + "format" : "pve-sdn-zone-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/zones/{zone}", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read sdn zone configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "pending" : { + "description" : "Display pending config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "running" : { + "description" : "Display running config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "zone" : { + "description" : "The SDN zone object identifier.", + "format" : "pve-sdn-zone-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/zones/{zone}", + [ + "SDN.Allocate" + ] + ] + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update sdn zone object configuration.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "advertise-subnets" : { + "description" : "Advertise evpn subnets if you have silent hosts", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "bridge" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bridge-disable-mac-learning" : { + "description" : "Disable auto mac learning.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "controller" : { + "description" : "Frr router name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dhcp" : { + "description" : "Type of the DHCP backend for this zone", + "enum" : [ + "dnsmasq" + ], + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable-arp-nd-suppression" : { + "description" : "Disable ipv4 arp && ipv6 neighbour discovery suppression", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "dns" : { + "description" : "dns api server", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dnszone" : { + "description" : "dns domain zone ex: mydomain.com", + "format" : "dns-name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dp-id" : { + "description" : "Faucet dataplane id", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "exitnodes" : { + "description" : "List of cluster node names.", + "format" : "pve-node-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "exitnodes-local-routing" : { + "description" : "Allow exitnodes to connect to evpn guests", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "exitnodes-primary" : { + "description" : "Force traffic to this exitnode first.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ipam" : { + "description" : "use a specific ipam", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "mac" : { + "description" : "Anycast logical router mac address", + "format" : "mac-addr", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "mtu" : { + "description" : "MTU", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "nodes" : { + "description" : "List of cluster node names.", + "format" : "pve-node-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "peers" : { + "description" : "peers address list.", + "format" : "ip-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "reversedns" : { + "description" : "reverse dns api server", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "rt-import" : { + "description" : "Route-Target import", + "format" : "pve-sdn-bgp-rt-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tag" : { + "description" : "Service-VLAN Tag", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vlan-protocol" : { + "default" : "802.1q", + "enum" : [ + "802.1q", + "802.1ad" + ], + "optional" : 1, + "type" : "string" + }, + "vrf-vxlan" : { + "description" : "l3vni.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "vxlan-port" : { + "description" : "Vxlan tunnel udp port (default 4789).", + "maximum" : 65536, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 65536)" + }, + "zone" : { + "description" : "The SDN zone object identifier.", + "format" : "pve-sdn-zone-id", + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/zones/{zone}", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/sdn/zones/{zone}", + "text" : "{zone}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "SDN zones index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "pending" : { + "description" : "Display pending config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "running" : { + "description" : "Display running config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "type" : { + "description" : "Only list SDN zones of specific type", + "enum" : [ + "evpn", + "faucet", + "qinq", + "simple", + "vlan", + "vxlan" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "description" : "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/zones/'", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "dhcp" : { + "optional" : 1, + "type" : "string" + }, + "dns" : { + "optional" : 1, + "type" : "string" + }, + "dnszone" : { + "optional" : 1, + "type" : "string" + }, + "ipam" : { + "optional" : 1, + "type" : "string" + }, + "mtu" : { + "optional" : 1, + "type" : "integer" + }, + "nodes" : { + "optional" : 1, + "type" : "string" + }, + "pending" : { + "optional" : 1 + }, + "reversedns" : { + "optional" : 1, + "type" : "string" + }, + "state" : { + "optional" : 1, + "type" : "string" + }, + "type" : { + "type" : "string" + }, + "zone" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{zone}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new sdn zone object.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "advertise-subnets" : { + "description" : "Advertise evpn subnets if you have silent hosts", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "bridge" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bridge-disable-mac-learning" : { + "description" : "Disable auto mac learning.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "controller" : { + "description" : "Frr router name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dhcp" : { + "description" : "Type of the DHCP backend for this zone", + "enum" : [ + "dnsmasq" + ], + "optional" : 1, + "type" : "string" + }, + "disable-arp-nd-suppression" : { + "description" : "Disable ipv4 arp && ipv6 neighbour discovery suppression", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "dns" : { + "description" : "dns api server", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dnszone" : { + "description" : "dns domain zone ex: mydomain.com", + "format" : "dns-name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dp-id" : { + "description" : "Faucet dataplane id", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "exitnodes" : { + "description" : "List of cluster node names.", + "format" : "pve-node-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "exitnodes-local-routing" : { + "description" : "Allow exitnodes to connect to evpn guests", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "exitnodes-primary" : { + "description" : "Force traffic to this exitnode first.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ipam" : { + "description" : "use a specific ipam", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "mac" : { + "description" : "Anycast logical router mac address", + "format" : "mac-addr", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "mtu" : { + "description" : "MTU", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "nodes" : { + "description" : "List of cluster node names.", + "format" : "pve-node-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "peers" : { + "description" : "peers address list.", + "format" : "ip-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "reversedns" : { + "description" : "reverse dns api server", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "rt-import" : { + "description" : "Route-Target import", + "format" : "pve-sdn-bgp-rt-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tag" : { + "description" : "Service-VLAN Tag", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "type" : { + "description" : "Plugin type.", + "enum" : [ + "evpn", + "faucet", + "qinq", + "simple", + "vlan", + "vxlan" + ], + "format" : "pve-configid", + "type" : "string" + }, + "vlan-protocol" : { + "default" : "802.1q", + "enum" : [ + "802.1q", + "802.1ad" + ], + "optional" : 1, + "type" : "string" + }, + "vrf-vxlan" : { + "description" : "l3vni.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "vxlan-port" : { + "description" : "Vxlan tunnel udp port (default 4789).", + "maximum" : 65536, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 65536)" + }, + "zone" : { + "description" : "The SDN zone object identifier.", + "format" : "pve-sdn-zone-id", + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/zones", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/sdn/zones", + "text" : "zones" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete sdn controller object configuration.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "controller" : { + "description" : "The SDN controller object identifier.", + "format" : "pve-sdn-controller-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/controllers", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read sdn controller configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "controller" : { + "description" : "The SDN controller object identifier.", + "format" : "pve-sdn-controller-id", + "type" : "string", + "typetext" : "" + }, + "pending" : { + "description" : "Display pending config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "running" : { + "description" : "Display running config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/controllers/{controller}", + [ + "SDN.Allocate" + ] + ] + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update sdn controller object configuration.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "asn" : { + "description" : "autonomous system number", + "maximum" : 4294967296, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 4294967296)" + }, + "bgp-multipath-as-path-relax" : { + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "controller" : { + "description" : "The SDN controller object identifier.", + "format" : "pve-sdn-controller-id", + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ebgp" : { + "description" : "Enable ebgp. (remote-as external)", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ebgp-multihop" : { + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "isis-domain" : { + "description" : "ISIS domain.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "isis-ifaces" : { + "description" : "ISIS interface.", + "format" : "pve-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "isis-net" : { + "description" : "ISIS network entity title.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "loopback" : { + "description" : "source loopback interface.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "peers" : { + "description" : "peers address list.", + "format" : "ip-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/controllers", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/sdn/controllers/{controller}", + "text" : "{controller}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "SDN controllers index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "pending" : { + "description" : "Display pending config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "running" : { + "description" : "Display running config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "type" : { + "description" : "Only list sdn controllers of specific type", + "enum" : [ + "bgp", + "evpn", + "faucet", + "isis" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "description" : "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/controllers/'", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "controller" : { + "type" : "string" + }, + "pending" : { + "optional" : 1 + }, + "state" : { + "optional" : 1, + "type" : "string" + }, + "type" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{controller}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new sdn controller object.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "asn" : { + "description" : "autonomous system number", + "maximum" : 4294967296, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 4294967296)" + }, + "bgp-multipath-as-path-relax" : { + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "controller" : { + "description" : "The SDN controller object identifier.", + "format" : "pve-sdn-controller-id", + "type" : "string", + "typetext" : "" + }, + "ebgp" : { + "description" : "Enable ebgp. (remote-as external)", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ebgp-multihop" : { + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "isis-domain" : { + "description" : "ISIS domain.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "isis-ifaces" : { + "description" : "ISIS interface.", + "format" : "pve-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "isis-net" : { + "description" : "ISIS network entity title.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "loopback" : { + "description" : "source loopback interface.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "peers" : { + "description" : "peers address list.", + "format" : "ip-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Plugin type.", + "enum" : [ + "bgp", + "evpn", + "faucet", + "isis" + ], + "format" : "pve-configid", + "type" : "string" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/controllers", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/sdn/controllers", + "text" : "controllers" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List PVE IPAM Entries", + "method" : "GET", + "name" : "ipamindex", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "ipam" : { + "description" : "The SDN ipam object identifier.", + "format" : "pve-sdn-ipam-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/zones//'", + "user" : "all" + }, + "protected" : 1, + "returns" : { + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/sdn/ipams/{ipam}/status", + "text" : "status" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete sdn ipam object configuration.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "ipam" : { + "description" : "The SDN ipam object identifier.", + "format" : "pve-sdn-ipam-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/ipams", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read sdn ipam configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "ipam" : { + "description" : "The SDN ipam object identifier.", + "format" : "pve-sdn-ipam-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/ipams/{ipam}", + [ + "SDN.Allocate" + ] + ] + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update sdn ipam object configuration.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ipam" : { + "description" : "The SDN ipam object identifier.", + "format" : "pve-sdn-ipam-id", + "type" : "string", + "typetext" : "" + }, + "section" : { + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "token" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "url" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/ipams", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/sdn/ipams/{ipam}", + "text" : "{ipam}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "SDN ipams index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "type" : { + "description" : "Only list sdn ipams of specific type", + "enum" : [ + "netbox", + "phpipam", + "pve" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "description" : "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/ipams/'", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "ipam" : { + "type" : "string" + }, + "type" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{ipam}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new sdn ipam object.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "ipam" : { + "description" : "The SDN ipam object identifier.", + "format" : "pve-sdn-ipam-id", + "type" : "string", + "typetext" : "" + }, + "section" : { + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "token" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Plugin type.", + "enum" : [ + "netbox", + "phpipam", + "pve" + ], + "format" : "pve-configid", + "type" : "string" + }, + "url" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/ipams", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/sdn/ipams", + "text" : "ipams" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete sdn dns object configuration.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "dns" : { + "description" : "The SDN dns object identifier.", + "format" : "pve-sdn-dns-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/dns", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read sdn dns configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "dns" : { + "description" : "The SDN dns object identifier.", + "format" : "pve-sdn-dns-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/dns/{dns}", + [ + "SDN.Allocate" + ] + ] + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update sdn dns object configuration.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dns" : { + "description" : "The SDN dns object identifier.", + "format" : "pve-sdn-dns-id", + "type" : "string", + "typetext" : "" + }, + "key" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "reversemaskv6" : { + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "ttl" : { + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "url" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/dns", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/sdn/dns/{dns}", + "text" : "{dns}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "SDN dns index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "type" : { + "description" : "Only list sdn dns of specific type", + "enum" : [ + "powerdns" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "description" : "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/dns/'", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "dns" : { + "type" : "string" + }, + "type" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{dns}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new sdn dns object.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "dns" : { + "description" : "The SDN dns object identifier.", + "format" : "pve-sdn-dns-id", + "type" : "string", + "typetext" : "" + }, + "key" : { + "optional" : 0, + "type" : "string", + "typetext" : "" + }, + "reversemaskv6" : { + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "reversev6mask" : { + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "ttl" : { + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "type" : { + "description" : "Plugin type.", + "enum" : [ + "powerdns" + ], + "format" : "pve-configid", + "type" : "string" + }, + "url" : { + "optional" : 0, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/dns", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/cluster/sdn/dns", + "text" : "dns" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/sdn", + [ + "SDN.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "id" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Apply sdn controller changes && reload.", + "method" : "PUT", + "name" : "reload", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/sdn", + [ + "SDN.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/cluster/sdn", + "text" : "sdn" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read cluster log", + "method" : "GET", + "name" : "log", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "max" : { + "description" : "Maximum number of entries.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/log", + "text" : "log" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Resources index (cluster wide).", + "method" : "GET", + "name" : "resources", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "type" : { + "enum" : [ + "vm", + "storage", + "node", + "sdn" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "cgroup-mode" : { + "description" : "The cgroup mode the node operates under (when type == node).", + "optional" : 1, + "type" : "integer" + }, + "content" : { + "description" : "Allowed storage content types (when type == storage).", + "format" : "pve-storage-content-list", + "optional" : 1, + "type" : "string" + }, + "cpu" : { + "description" : "CPU utilization (when type in node,qemu,lxc).", + "minimum" : 0, + "optional" : 1, + "renderer" : "fraction_as_percentage", + "type" : "number" + }, + "disk" : { + "description" : "Used disk space in bytes (when type in storage), used root image spave for VMs (type in qemu,lxc).", + "minimum" : 0, + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "hastate" : { + "description" : "HA service status (for HA managed VMs).", + "optional" : 1, + "type" : "string" + }, + "id" : { + "description" : "Resource id.", + "type" : "string" + }, + "level" : { + "description" : "Support level (when type == node).", + "optional" : 1, + "type" : "string" + }, + "maxcpu" : { + "description" : "Number of available CPUs (when type in node,qemu,lxc).", + "minimum" : 0, + "optional" : 1, + "type" : "number" + }, + "maxdisk" : { + "description" : "Storage size in bytes (when type in storage), root image size for VMs (type in qemu,lxc).", + "minimum" : 0, + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "maxmem" : { + "description" : "Number of available memory in bytes (when type in node,qemu,lxc).", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "mem" : { + "description" : "Used memory in bytes (when type in node,qemu,lxc).", + "minimum" : 0, + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "name" : { + "description" : "Name of the resource.", + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "The cluster node name (when type in node,storage,qemu,lxc).", + "format" : "pve-node", + "optional" : 1, + "type" : "string" + }, + "plugintype" : { + "description" : "More specific type, if available.", + "optional" : 1, + "type" : "string" + }, + "pool" : { + "description" : "The pool name (when type in pool,qemu,lxc).", + "optional" : 1, + "type" : "string" + }, + "status" : { + "description" : "Resource type dependent status.", + "optional" : 1, + "type" : "string" + }, + "storage" : { + "description" : "The storage identifier (when type == storage).", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string" + }, + "type" : { + "description" : "Resource type.", + "enum" : [ + "node", + "storage", + "pool", + "qemu", + "lxc", + "openvz", + "sdn" + ], + "type" : "string" + }, + "uptime" : { + "description" : "Node uptime in seconds (when type in node,qemu,lxc).", + "optional" : 1, + "renderer" : "duration", + "type" : "integer" + }, + "vmid" : { + "description" : "The numerical vmid (when type in qemu,lxc).", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/resources", + "text" : "resources" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List recent tasks (cluster wide).", + "method" : "GET", + "name" : "tasks", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "upid" : { + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/tasks", + "text" : "tasks" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get datacenter options. Without 'Sys.Audit' on '/' not all options are returned.", + "method" : "GET", + "name" : "get_options", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ], + "user" : "all" + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set datacenter options.", + "method" : "PUT", + "name" : "set_options", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "bwlimit" : { + "description" : "Set I/O bandwidth limit for various operations (in KiB/s).", + "format" : { + "clone" : { + "description" : "bandwidth limit in KiB/s for cloning disks", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "default" : { + "description" : "default bandwidth limit in KiB/s", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "migration" : { + "description" : "bandwidth limit in KiB/s for migrating guests (including moving local disks)", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "move" : { + "description" : "bandwidth limit in KiB/s for moving disks", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "restore" : { + "description" : "bandwidth limit in KiB/s for restoring guests from backups", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[clone=] [,default=] [,migration=] [,move=] [,restore=]" + }, + "console" : { + "description" : "Select the default Console viewer. You can either use the builtin java applet (VNC; deprecated and maps to html5), an external virt-viewer comtatible application (SPICE), an HTML5 based vnc viewer (noVNC), or an HTML5 based console client (xtermjs). If the selected viewer is not available (e.g. SPICE not activated for the VM), the fallback is noVNC.", + "enum" : [ + "applet", + "vv", + "html5", + "xtermjs" + ], + "optional" : 1, + "type" : "string" + }, + "crs" : { + "description" : "Cluster resource scheduling settings.", + "format" : { + "ha" : { + "default" : "basic", + "description" : "Use this resource scheduler mode for HA.", + "enum" : [ + "basic", + "static" + ], + "optional" : 1, + "type" : "string", + "verbose_description" : "Configures how the HA manager should select nodes to start or recover services. With 'basic', only the number of services is used, with 'static', static CPU and memory configuration of services is considered." + }, + "ha-rebalance-on-start" : { + "default" : 0, + "description" : "Set to use CRS for selecting a suited node when a HA services request-state changes from stop to start.", + "optional" : 1, + "type" : "boolean" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[ha=] [,ha-rebalance-on-start=<1|0>]" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "description" : { + "description" : "Datacenter description. Shown in the web-interface datacenter notes panel. This is saved as comment inside the configuration file.", + "maxLength" : 65536, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "email_from" : { + "description" : "Specify email address to send notification from (default is root@$hostname)", + "format" : "email-opt", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "fencing" : { + "default" : "watchdog", + "description" : "Set the fencing mode of the HA cluster. Hardware mode needs a valid configuration of fence devices in /etc/pve/ha/fence.cfg. With both all two modes are used.\n\nWARNING: 'hardware' and 'both' are EXPERIMENTAL & WIP", + "enum" : [ + "watchdog", + "hardware", + "both" + ], + "optional" : 1, + "type" : "string" + }, + "ha" : { + "description" : "Cluster wide HA settings.", + "format" : { + "shutdown_policy" : { + "default" : "conditional", + "description" : "The policy for HA services on node shutdown. 'freeze' disables auto-recovery, 'failover' ensures recovery, 'conditional' recovers on poweroff and freezes on reboot. 'migrate' will migrate running services to other nodes, if possible. With 'freeze' or 'failover', HA Services will always get stopped first on shutdown.", + "enum" : [ + "freeze", + "failover", + "conditional", + "migrate" + ], + "type" : "string", + "verbose_description" : "Describes the policy for handling HA services on poweroff or reboot of a node. Freeze will always freeze services which are still located on the node on shutdown, those services won't be recovered by the HA manager. Failover will not mark the services as frozen and thus the services will get recovered to other nodes, if the shutdown node does not come up again quickly (< 1min). 'conditional' chooses automatically depending on the type of shutdown, i.e., on a reboot the service will be frozen but on a poweroff the service will stay as is, and thus get recovered after about 2 minutes. Migrate will try to move all running services to another node when a reboot or shutdown was triggered. The poweroff process will only continue once no running services are located on the node anymore. If the node comes up again, the service will be moved back to the previously powered-off node, at least if no other migration, reloaction or recovery took place." + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "shutdown_policy=" + }, + "http_proxy" : { + "description" : "Specify external http proxy which is used for downloads (example: 'http://username:password@host:port/')", + "optional" : 1, + "pattern" : "http://.*", + "type" : "string" + }, + "keyboard" : { + "description" : "Default keybord layout for vnc server.", + "enum" : [ + "de", + "de-ch", + "da", + "en-gb", + "en-us", + "es", + "fi", + "fr", + "fr-be", + "fr-ca", + "fr-ch", + "hu", + "is", + "it", + "ja", + "lt", + "mk", + "nl", + "no", + "pl", + "pt", + "pt-br", + "sv", + "sl", + "tr" + ], + "optional" : 1, + "type" : "string" + }, + "language" : { + "description" : "Default GUI language.", + "enum" : [ + "ar", + "ca", + "da", + "de", + "en", + "es", + "eu", + "fa", + "fr", + "hr", + "he", + "it", + "ja", + "ka", + "kr", + "nb", + "nl", + "nn", + "pl", + "pt_BR", + "ru", + "sl", + "sv", + "tr", + "ukr", + "zh_CN", + "zh_TW" + ], + "optional" : 1, + "type" : "string" + }, + "mac_prefix" : { + "default" : "BC:24:11", + "description" : "Prefix for the auto-generated MAC addresses of virtual guests. The default 'BC:24:11' is the OUI assigned by the IEEE to Proxmox Server Solutions GmbH for a 24-bit large MAC block. You're allowed to use this in local networks, i.e., those not directly reachable by the public (e.g., in a LAN or behind NAT).", + "format" : "mac-prefix", + "optional" : 1, + "type" : "string", + "typetext" : "", + "verbose_description" : "Prefix for the auto-generated MAC addresses of virtual guests. The default `BC:24:11` is the Organizationally Unique Identifier (OUI) assigned by the IEEE to Proxmox Server Solutions GmbH for a MAC Address Block Large (MA-L). You're allowed to use this in local networks, i.e., those not directly reachable by the public (e.g., in a LAN or NAT/Masquerading).\n \nNote that when you run multiple cluster that (partially) share the networks of their virtual guests, it's highly recommended that you extend the default MAC prefix, or generate a custom (valid) one, to reduce the chance of MAC collisions. For example, add a separate extra hexadecimal to the Proxmox OUI for each cluster, like `BC:24:11:0` for the first, `BC:24:11:1` for the second, and so on.\n Alternatively, you can also separate the networks of the guests logically, e.g., by using VLANs.\n\nFor publicly accessible guests it's recommended that you get your own https://standards.ieee.org/products-programs/regauth/[OUI from the IEEE] registered or coordinate with your, or your hosting providers, network admins." + }, + "max_workers" : { + "description" : "Defines how many workers (per node) are maximal started on actions like 'stopall VMs' or task from the ha-manager.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "migration" : { + "description" : "For cluster wide migration settings.", + "format" : { + "network" : { + "description" : "CIDR of the (sub) network that is used for migration.", + "format" : "CIDR", + "format_description" : "CIDR", + "optional" : 1, + "type" : "string" + }, + "type" : { + "default" : "secure", + "default_key" : 1, + "description" : "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.", + "enum" : [ + "secure", + "insecure" + ], + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[type=] [,network=]" + }, + "migration_unsecure" : { + "description" : "Migration is secure using SSH tunnel by default. For secure private networks you can disable it to speed up migration. Deprecated, use the 'migration' property instead!", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "next-id" : { + "description" : "Control the range for the free VMID auto-selection pool.", + "format" : { + "lower" : { + "default" : 100, + "description" : "Lower, inclusive boundary for free next-id API range.", + "max" : 999999999, + "min" : 100, + "optional" : 1, + "type" : "integer" + }, + "upper" : { + "default" : 1000000, + "description" : "Upper, exclusive boundary for free next-id API range.", + "max" : 1000000000, + "min" : 100, + "optional" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[lower=] [,upper=]" + }, + "notify" : { + "description" : "Cluster-wide notification settings.", + "format" : { + "fencing" : { + "default" : "always", + "description" : "Control if notifications about node fencing should be sent.", + "enum" : [ + "always", + "never" + ], + "optional" : 1, + "type" : "string", + "verbose_description" : "Control if notifications about node fencing should be sent.\n* 'always' always send out notifications\n* 'never' never send out notifications.\nFor production systems, turning off node fencing notifications is notrecommended!\n" + }, + "package-updates" : { + "default" : "auto", + "description" : "Control when the daily update job should send out notifications.", + "enum" : [ + "auto", + "always", + "never" + ], + "optional" : 1, + "type" : "string", + "verbose_description" : "Control how often the daily update job should send out notifications:\n* 'auto' daily for systems with a valid subscription, as those are assumed to be production-ready and thus should know about pending updates.\n* 'always' every update, if there are new pending updates.\n* 'never' never send a notification for new pending updates.\n" + }, + "replication" : { + "default" : "always", + "description" : "Control if notifications for replication failures should be sent.", + "enum" : [ + "always", + "never" + ], + "optional" : 1, + "type" : "string", + "verbose_description" : "Control if notifications for replication failures should be sent.\n* 'always' always send out notifications\n* 'never' never send out notifications.\nFor production systems, turning off replication notifications is notrecommended!\n" + }, + "target-fencing" : { + "description" : "Control where notifications about fenced cluster nodes should be sent to.", + "format_description" : "TARGET", + "optional" : 1, + "type" : "string", + "verbose_description" : "Control where notifications about fenced cluster nodes should be sent to. Has to be the name of a notification target (endpoint or notification group). If the 'target-fencing' parameter is not set, the system will send mails to root via a 'sendmail' notification endpoint." + }, + "target-package-updates" : { + "description" : "Control where notifications about available updates should be sent to.", + "format_description" : "TARGET", + "optional" : 1, + "type" : "string", + "verbose_description" : "Control where notifications about available updates should be sent to. Has to be the name of a notification target (endpoint or notification group). If the 'target-package-updates' parameter is not set, the system will send mails to root via a 'sendmail' notification endpoint." + }, + "target-replication" : { + "description" : "Control where notifications for failed storage replication jobs should be sent to.", + "format_description" : "TARGET", + "optional" : 1, + "type" : "string", + "verbose_description" : "Control where notifications for failed storage replication jobs should be sent to. Has to be the name of a notification target (endpoint or notification group). If the 'target-replication' parameter is not set, the system will send mails to root via a 'sendmail' notification endpoint." + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[fencing=] [,package-updates=] [,replication=] [,target-fencing=] [,target-package-updates=] [,target-replication=]" + }, + "registered-tags" : { + "description" : "A list of tags that require a `Sys.Modify` on '/' to set and delete. Tags set here that are also in 'user-tag-access' also require `Sys.Modify`.", + "optional" : 1, + "pattern" : "(?:(?^i:[a-z0-9_][a-z0-9_\\-\\+\\.]*);)*(?^i:[a-z0-9_][a-z0-9_\\-\\+\\.]*)", + "type" : "string", + "typetext" : "[;...]" + }, + "tag-style" : { + "description" : "Tag style options.", + "format" : { + "case-sensitive" : { + "default" : 0, + "description" : "Controls if filtering for unique tags on update should check case-sensitive.", + "optional" : 1, + "type" : "boolean" + }, + "color-map" : { + "description" : "Manual color mapping for tags (semicolon separated).", + "optional" : 1, + "pattern" : "(?:(?^i:[a-z0-9_][a-z0-9_\\-\\+\\.]*):[0-9a-fA-F]{6}(?::[0-9a-fA-F]{6})?)(?:;(?:(?^i:[a-z0-9_][a-z0-9_\\-\\+\\.]*):[0-9a-fA-F]{6}(?::[0-9a-fA-F]{6})?))*", + "type" : "string", + "typetext" : ":[:][;=...]" + }, + "ordering" : { + "default" : "alphabetical", + "description" : "Controls the sorting of the tags in the web-interface and the API update.", + "enum" : [ + "config", + "alphabetical" + ], + "optional" : 1, + "type" : "string" + }, + "shape" : { + "default" : "circle", + "description" : "Tag shape for the web ui tree. 'full' draws the full tag. 'circle' draws only a circle with the background color. 'dense' only draws a small rectancle (useful when many tags are assigned to each guest).'none' disables showing the tags.", + "enum" : [ + "full", + "circle", + "dense", + "none" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[case-sensitive=<1|0>] [,color-map=:[:][;=...]] [,ordering=] [,shape=]" + }, + "u2f" : { + "description" : "u2f", + "format" : { + "appid" : { + "description" : "U2F AppId URL override. Defaults to the origin.", + "format_description" : "APPID", + "optional" : 1, + "type" : "string" + }, + "origin" : { + "description" : "U2F Origin override. Mostly useful for single nodes with a single URL.", + "format_description" : "URL", + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[appid=] [,origin=]" + }, + "user-tag-access" : { + "description" : "Privilege options for user-settable tags", + "format" : { + "user-allow" : { + "default" : "free", + "description" : "Controls tag usage for users without `Sys.Modify` on `/` by either allowing `none`, a `list`, already `existing` or anything (`free`).", + "enum" : [ + "none", + "list", + "existing", + "free" + ], + "optional" : 1, + "type" : "string", + "verbose_description" : "Controls which tags can be set or deleted on resources a user controls (such as guests). Users with the `Sys.Modify` privilege on `/` are alwaysunrestricted.\n* 'none' no tags are usable.\n* 'list' tags from 'user-allow-list' are usable.\n* 'existing' like list, but already existing tags of resources are also usable.\n* 'free' no tag restrictions.\n" + }, + "user-allow-list" : { + "description" : "List of tags users are allowed to set and delete (semicolon separated) for 'user-allow' values 'list' and 'existing'.", + "optional" : 1, + "pattern" : "(?^i:[a-z0-9_][a-z0-9_\\-\\+\\.]*)(?:;(?^i:[a-z0-9_][a-z0-9_\\-\\+\\.]*))*", + "type" : "string", + "typetext" : "[;...]" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[user-allow=] [,user-allow-list=[;...]]" + }, + "webauthn" : { + "description" : "webauthn configuration", + "format" : { + "allow-subdomains" : { + "default" : 1, + "description" : "Whether to allow the origin to be a subdomain, rather than the exact URL.", + "optional" : 1, + "type" : "boolean" + }, + "id" : { + "description" : "Relying party ID. Must be the domain name without protocol, port or location. Changing this *will* break existing credentials.", + "format_description" : "DOMAINNAME", + "optional" : 1, + "type" : "string" + }, + "origin" : { + "description" : "Site origin. Must be a `https://` URL (or `http://localhost`). Should contain the address users type in their browsers to access the web interface. Changing this *may* break existing credentials.", + "format_description" : "URL", + "optional" : 1, + "type" : "string" + }, + "rp" : { + "description" : "Relying party name. Any text identifier. Changing this *may* break existing credentials.", + "format_description" : "RELYING_PARTY", + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[allow-subdomains=<1|0>] [,id=] [,origin=] [,rp=]" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/cluster/options", + "text" : "options" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get cluster status information.", + "method" : "GET", + "name" : "get_status", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : { + "id" : { + "type" : "string" + }, + "ip" : { + "description" : "[node] IP of the resolved nodename.", + "optional" : 1, + "type" : "string" + }, + "level" : { + "description" : "[node] Proxmox VE Subscription level, indicates if eligible for enterprise support as well as access to the stable Proxmox VE Enterprise Repository.", + "optional" : 1, + "type" : "string" + }, + "local" : { + "description" : "[node] Indicates if this is the responding node.", + "optional" : 1, + "type" : "boolean" + }, + "name" : { + "type" : "string" + }, + "nodeid" : { + "description" : "[node] ID of the node from the corosync configuration.", + "optional" : 1, + "type" : "integer" + }, + "nodes" : { + "description" : "[cluster] Nodes count, including offline nodes.", + "optional" : 1, + "type" : "integer" + }, + "online" : { + "description" : "[node] Indicates if the node is online or offline.", + "optional" : 1, + "type" : "boolean" + }, + "quorate" : { + "description" : "[cluster] Indicates if there is a majority of nodes online to make decisions", + "optional" : 1, + "type" : "boolean" + }, + "type" : { + "description" : "Indicates the type, either cluster or node. The type defines the object properties e.g. quorate available for type cluster.", + "enum" : [ + "cluster", + "node" + ], + "type" : "string" + }, + "version" : { + "description" : "[cluster] Current version of the corosync configuration file.", + "optional" : 1, + "type" : "integer" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/cluster/status", + "text" : "status" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get next free VMID. Pass a VMID to assert that its free (at time of check).", + "method" : "GET", + "name" : "nextid", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "description" : "The next free VMID.", + "type" : "integer" + } + } + }, + "leaf" : 1, + "path" : "/cluster/nextid", + "text" : "nextid" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Cluster index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/cluster", + "text" : "cluster" + }, + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete rule.", + "method" : "DELETE", + "name" : "delete_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get single rule data.", + "method" : "GET", + "name" : "get_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : null, + "returns" : { + "properties" : { + "action" : { + "type" : "string" + }, + "comment" : { + "optional" : 1, + "type" : "string" + }, + "dest" : { + "optional" : 1, + "type" : "string" + }, + "dport" : { + "optional" : 1, + "type" : "string" + }, + "enable" : { + "optional" : 1, + "type" : "integer" + }, + "icmp-type" : { + "optional" : 1, + "type" : "string" + }, + "iface" : { + "optional" : 1, + "type" : "string" + }, + "ipversion" : { + "optional" : 1, + "type" : "integer" + }, + "log" : { + "description" : "Log level for firewall rule", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "optional" : 1, + "type" : "string" + }, + "pos" : { + "type" : "integer" + }, + "proto" : { + "optional" : 1, + "type" : "string" + }, + "source" : { + "optional" : 1, + "type" : "string" + }, + "sport" : { + "optional" : 1, + "type" : "string" + }, + "type" : { + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Modify rule data.", + "method" : "PUT", + "name" : "update_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "action" : { + "description" : "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "comment" : { + "description" : "Descriptive comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dest" : { + "description" : "Restrict packet destination address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dport" : { + "description" : "Restrict TCP/UDP destination port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-dport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Flag to enable/disable a rule.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "icmp-type" : { + "description" : "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'.", + "format" : "pve-fw-icmp-type-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name. You have to use network configuration key names for VMs and containers ('net\\d+'). Host related rules can use arbitrary strings.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "log" : { + "description" : "Log level for firewall rule.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "description" : "Use predefined standard macro.", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "moveto" : { + "description" : "Move rule to new position . Other arguments are ignored.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "proto" : { + "description" : "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.", + "format" : "pve-fw-protocol-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "Restrict packet source address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sport" : { + "description" : "Restrict TCP/UDP source port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-sport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Rule type.", + "enum" : [ + "in", + "out", + "group" + ], + "optional" : 1, + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/firewall/rules/{pos}", + "text" : "{pos}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List rules.", + "method" : "GET", + "name" : "get_rules", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : null, + "returns" : { + "items" : { + "properties" : { + "pos" : { + "type" : "integer" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{pos}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new rule.", + "method" : "POST", + "name" : "create_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "action" : { + "description" : "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.", + "maxLength" : 20, + "minLength" : 2, + "optional" : 0, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "comment" : { + "description" : "Descriptive comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dest" : { + "description" : "Restrict packet destination address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dport" : { + "description" : "Restrict TCP/UDP destination port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-dport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Flag to enable/disable a rule.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "icmp-type" : { + "description" : "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'.", + "format" : "pve-fw-icmp-type-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name. You have to use network configuration key names for VMs and containers ('net\\d+'). Host related rules can use arbitrary strings.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "log" : { + "description" : "Log level for firewall rule.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "description" : "Use predefined standard macro.", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "proto" : { + "description" : "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.", + "format" : "pve-fw-protocol-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "Restrict packet source address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sport" : { + "description" : "Restrict TCP/UDP source port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-sport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Rule type.", + "enum" : [ + "in", + "out", + "group" + ], + "optional" : 0, + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu/{vmid}/firewall/rules", + "text" : "rules" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove IP or Network alias.", + "method" : "DELETE", + "name" : "remove_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read alias.", + "method" : "GET", + "name" : "read_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update IP or Network alias.", + "method" : "PUT", + "name" : "update_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDR", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "rename" : { + "description" : "Rename an existing alias.", + "maxLength" : 64, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/firewall/aliases/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List aliases", + "method" : "GET", + "name" : "get_aliases", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "cidr" : { + "type" : "string" + }, + "comment" : { + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 0, + "type" : "string" + }, + "name" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create IP or Network Alias.", + "method" : "POST", + "name" : "create_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDR", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu/{vmid}/firewall/aliases", + "text" : "aliases" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove IP or Network from IPSet.", + "method" : "DELETE", + "name" : "remove_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read IP or Network settings from IPSet.", + "method" : "GET", + "name" : "read_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update IP or Network settings", + "method" : "PUT", + "name" : "update_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "nomatch" : { + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/firewall/ipset/{name}/{cidr}", + "text" : "{cidr}" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete IPSet", + "method" : "DELETE", + "name" : "delete_ipset", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "description" : "Delete all members of the IPSet, if there are any.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "List IPSet content", + "method" : "GET", + "name" : "get_ipset", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "cidr" : { + "type" : "string" + }, + "comment" : { + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 0, + "type" : "string" + }, + "nomatch" : { + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{cidr}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Add IP or Network to IPSet.", + "method" : "POST", + "name" : "create_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "nomatch" : { + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu/{vmid}/firewall/ipset/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List IPSets", + "method" : "GET", + "name" : "ipset_index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 0, + "type" : "string" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new IPSet", + "method" : "POST", + "name" : "create_ipset", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "rename" : { + "description" : "Rename an existing IPSet. You can set 'rename' to the same value as 'name' to update the 'comment' of an existing IPSet.", + "maxLength" : 64, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu/{vmid}/firewall/ipset", + "text" : "ipset" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get VM firewall options.", + "method" : "GET", + "name" : "get_options", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "properties" : { + "dhcp" : { + "default" : 0, + "description" : "Enable DHCP.", + "optional" : 1, + "type" : "boolean" + }, + "enable" : { + "default" : 0, + "description" : "Enable/disable firewall rules.", + "optional" : 1, + "type" : "boolean" + }, + "ipfilter" : { + "description" : "Enable default IP filters. This is equivalent to adding an empty ipfilter-net ipset for every interface. Such ipsets implicitly contain sane default restrictions such as restricting IPv6 link local addresses to the one derived from the interface's MAC address. For containers the configured IP addresses will be implicitly added.", + "optional" : 1, + "type" : "boolean" + }, + "log_level_in" : { + "description" : "Log level for incoming traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "log_level_out" : { + "description" : "Log level for outgoing traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macfilter" : { + "default" : 1, + "description" : "Enable/disable MAC address filter.", + "optional" : 1, + "type" : "boolean" + }, + "ndp" : { + "default" : 0, + "description" : "Enable NDP (Neighbor Discovery Protocol).", + "optional" : 1, + "type" : "boolean" + }, + "policy_in" : { + "description" : "Input policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + }, + "policy_out" : { + "description" : "Output policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + }, + "radv" : { + "description" : "Allow sending Router Advertisement.", + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set Firewall options.", + "method" : "PUT", + "name" : "set_options", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dhcp" : { + "default" : 0, + "description" : "Enable DHCP.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "default" : 0, + "description" : "Enable/disable firewall rules.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ipfilter" : { + "description" : "Enable default IP filters. This is equivalent to adding an empty ipfilter-net ipset for every interface. Such ipsets implicitly contain sane default restrictions such as restricting IPv6 link local addresses to the one derived from the interface's MAC address. For containers the configured IP addresses will be implicitly added.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "log_level_in" : { + "description" : "Log level for incoming traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "log_level_out" : { + "description" : "Log level for outgoing traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macfilter" : { + "default" : 1, + "description" : "Enable/disable MAC address filter.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ndp" : { + "default" : 0, + "description" : "Enable NDP (Neighbor Discovery Protocol).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "policy_in" : { + "description" : "Input policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + }, + "policy_out" : { + "description" : "Output policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + }, + "radv" : { + "description" : "Allow sending Router Advertisement.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/firewall/options", + "text" : "options" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read firewall log", + "method" : "GET", + "name" : "log", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "limit" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "since" : { + "description" : "Display log since this UNIX epoch.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "start" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "until" : { + "description" : "Display log until this UNIX epoch.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Console" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "n" : { + "description" : "Line number", + "type" : "integer" + }, + "t" : { + "description" : "Line text", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/firewall/log", + "text" : "log" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Lists possible IPSet/Alias reference which are allowed in source/dest properties.", + "method" : "GET", + "name" : "refs", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Only list references of specified type.", + "enum" : [ + "alias", + "ipset" + ], + "optional" : 1, + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "name" : { + "type" : "string" + }, + "ref" : { + "type" : "string" + }, + "scope" : { + "type" : "string" + }, + "type" : { + "enum" : [ + "alias", + "ipset" + ], + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/firewall/refs", + "text" : "refs" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu/{vmid}/firewall", + "text" : "firewall" + }, + { + "children" : [ + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Execute fsfreeze-freeze.", + "method" : "POST", + "name" : "fsfreeze-freeze", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/fsfreeze-freeze", + "text" : "fsfreeze-freeze" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Execute fsfreeze-status.", + "method" : "POST", + "name" : "fsfreeze-status", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/fsfreeze-status", + "text" : "fsfreeze-status" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Execute fsfreeze-thaw.", + "method" : "POST", + "name" : "fsfreeze-thaw", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/fsfreeze-thaw", + "text" : "fsfreeze-thaw" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Execute fstrim.", + "method" : "POST", + "name" : "fstrim", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/fstrim", + "text" : "fstrim" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Execute get-fsinfo.", + "method" : "GET", + "name" : "get-fsinfo", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/get-fsinfo", + "text" : "get-fsinfo" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Execute get-host-name.", + "method" : "GET", + "name" : "get-host-name", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/get-host-name", + "text" : "get-host-name" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Execute get-memory-block-info.", + "method" : "GET", + "name" : "get-memory-block-info", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/get-memory-block-info", + "text" : "get-memory-block-info" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Execute get-memory-blocks.", + "method" : "GET", + "name" : "get-memory-blocks", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/get-memory-blocks", + "text" : "get-memory-blocks" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Execute get-osinfo.", + "method" : "GET", + "name" : "get-osinfo", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/get-osinfo", + "text" : "get-osinfo" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Execute get-time.", + "method" : "GET", + "name" : "get-time", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/get-time", + "text" : "get-time" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Execute get-timezone.", + "method" : "GET", + "name" : "get-timezone", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/get-timezone", + "text" : "get-timezone" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Execute get-users.", + "method" : "GET", + "name" : "get-users", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/get-users", + "text" : "get-users" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Execute get-vcpus.", + "method" : "GET", + "name" : "get-vcpus", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/get-vcpus", + "text" : "get-vcpus" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Execute info.", + "method" : "GET", + "name" : "info", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/info", + "text" : "info" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Execute network-get-interfaces.", + "method" : "GET", + "name" : "network-get-interfaces", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/network-get-interfaces", + "text" : "network-get-interfaces" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Execute ping.", + "method" : "POST", + "name" : "ping", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/ping", + "text" : "ping" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Execute shutdown.", + "method" : "POST", + "name" : "shutdown", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/shutdown", + "text" : "shutdown" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Execute suspend-disk.", + "method" : "POST", + "name" : "suspend-disk", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/suspend-disk", + "text" : "suspend-disk" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Execute suspend-hybrid.", + "method" : "POST", + "name" : "suspend-hybrid", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/suspend-hybrid", + "text" : "suspend-hybrid" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Execute suspend-ram.", + "method" : "POST", + "name" : "suspend-ram", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/suspend-ram", + "text" : "suspend-ram" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Sets the password for the given user to the given password", + "method" : "POST", + "name" : "set-user-password", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "crypted" : { + "default" : 0, + "description" : "set to 1 if the password has already been passed through crypt()", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "The new password.", + "maxLength" : 1024, + "minLength" : 5, + "type" : "string", + "typetext" : "" + }, + "username" : { + "description" : "The user to set the password for.", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/set-user-password", + "text" : "set-user-password" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Executes the given command in the vm via the guest-agent and returns an object with the pid.", + "method" : "POST", + "name" : "exec", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "command" : { + "description" : "The command as a list of program + arguments.", + "items" : { + "description" : "A single part of the program + arguments.", + "format" : "string" + }, + "type" : "array", + "typetext" : "" + }, + "input-data" : { + "description" : "Data to pass as 'input-data' to the guest. Usually treated as STDIN to 'command'.", + "maxLength" : 65536, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "pid" : { + "description" : "The PID of the process started by the guest-agent.", + "type" : "integer" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/exec", + "text" : "exec" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Gets the status of the given pid started by the guest-agent", + "method" : "GET", + "name" : "exec-status", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pid" : { + "description" : "The PID to query", + "type" : "integer", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "err-data" : { + "description" : "stderr of the process", + "optional" : 1, + "type" : "string" + }, + "err-truncated" : { + "description" : "true if stderr was not fully captured", + "optional" : 1, + "type" : "boolean" + }, + "exitcode" : { + "description" : "process exit code if it was normally terminated.", + "optional" : 1, + "type" : "integer" + }, + "exited" : { + "description" : "Tells if the given command has exited yet.", + "type" : "boolean" + }, + "out-data" : { + "description" : "stdout of the process", + "optional" : 1, + "type" : "string" + }, + "out-truncated" : { + "description" : "true if stdout was not fully captured", + "optional" : 1, + "type" : "boolean" + }, + "signal" : { + "description" : "signal number or exception code if the process was abnormally terminated.", + "optional" : 1, + "type" : "integer" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/exec-status", + "text" : "exec-status" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Reads the given file via guest agent. Is limited to 16777216 bytes.", + "method" : "GET", + "name" : "file-read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "file" : { + "description" : "The path to the file", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a `content` property.", + "properties" : { + "content" : { + "description" : "The content of the file, maximum 16777216", + "type" : "string" + }, + "truncated" : { + "description" : "If set to 1, the output is truncated and not complete", + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/file-read", + "text" : "file-read" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Writes the given file via guest agent.", + "method" : "POST", + "name" : "file-write", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "content" : { + "description" : "The content to write into the file.", + "maxLength" : 61440, + "type" : "string", + "typetext" : "" + }, + "encode" : { + "default" : 1, + "description" : "If set, the content will be encoded as base64 (required by QEMU).Otherwise the content needs to be encoded beforehand - defaults to true.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "file" : { + "description" : "The path to the file.", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/agent/file-write", + "text" : "file-write" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "QEMU Guest Agent command index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 1, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "description" : "Returns the list of QEMU Guest Agent commands", + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Execute QEMU Guest Agent commands.", + "method" : "POST", + "name" : "agent", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "command" : { + "description" : "The QGA command.", + "enum" : [ + "fsfreeze-freeze", + "fsfreeze-status", + "fsfreeze-thaw", + "fstrim", + "get-fsinfo", + "get-host-name", + "get-memory-block-info", + "get-memory-blocks", + "get-osinfo", + "get-time", + "get-timezone", + "get-users", + "get-vcpus", + "info", + "network-get-interfaces", + "ping", + "shutdown", + "suspend-disk", + "suspend-hybrid", + "suspend-ram" + ], + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Returns an object with a single `result` property.", + "type" : "object" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu/{vmid}/agent", + "text" : "agent" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read VM RRD statistics (returns PNG)", + "method" : "GET", + "name" : "rrd", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cf" : { + "description" : "The RRD consolidation function", + "enum" : [ + "AVERAGE", + "MAX" + ], + "optional" : 1, + "type" : "string" + }, + "ds" : { + "description" : "The list of datasources you want to display.", + "format" : "pve-configid-list", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "timeframe" : { + "description" : "Specify the time frame you are interested in.", + "enum" : [ + "hour", + "day", + "week", + "month", + "year" + ], + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "properties" : { + "filename" : { + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/rrd", + "text" : "rrd" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read VM RRD statistics", + "method" : "GET", + "name" : "rrddata", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cf" : { + "description" : "The RRD consolidation function", + "enum" : [ + "AVERAGE", + "MAX" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "timeframe" : { + "description" : "Specify the time frame you are interested in.", + "enum" : [ + "hour", + "day", + "week", + "month", + "year" + ], + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/rrddata", + "text" : "rrddata" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get the virtual machine configuration with pending configuration changes applied. Set the 'current' parameter to get the current configuration instead.", + "method" : "GET", + "name" : "vm_config", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "current" : { + "default" : 0, + "description" : "Get current values (instead of pending values).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapshot" : { + "description" : "Fetch config values from given snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "description" : "The VM configuration.", + "properties" : { + "acpi" : { + "default" : 1, + "description" : "Enable/disable ACPI.", + "optional" : 1, + "type" : "boolean" + }, + "affinity" : { + "description" : "List of host cores used to execute guest processes, for example: 0,5,8-11", + "format" : "pve-cpuset", + "optional" : 1, + "type" : "string" + }, + "agent" : { + "description" : "Enable/disable communication with the QEMU Guest Agent and its properties.", + "format" : { + "enabled" : { + "default" : 0, + "default_key" : 1, + "description" : "Enable/disable communication with a QEMU Guest Agent (QGA) running in the VM.", + "type" : "boolean" + }, + "freeze-fs-on-backup" : { + "default" : 1, + "description" : "Freeze/thaw guest filesystems on backup for consistency.", + "optional" : 1, + "type" : "boolean" + }, + "fstrim_cloned_disks" : { + "default" : 0, + "description" : "Run fstrim after moving a disk or migrating the VM.", + "optional" : 1, + "type" : "boolean" + }, + "type" : { + "default" : "virtio", + "description" : "Select the agent type", + "enum" : [ + "virtio", + "isa" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "arch" : { + "description" : "Virtual processor architecture. Defaults to the host.", + "enum" : [ + "x86_64", + "aarch64" + ], + "optional" : 1, + "type" : "string" + }, + "args" : { + "description" : "Arbitrary arguments passed to kvm.", + "optional" : 1, + "type" : "string", + "verbose_description" : "Arbitrary arguments passed to kvm, for example:\n\nargs: -no-reboot -smbios 'type=0,vendor=FOO'\n\nNOTE: this option is for experts only.\n" + }, + "audio0" : { + "description" : "Configure a audio device, useful in combination with QXL/Spice.", + "format" : { + "device" : { + "description" : "Configure an audio device.", + "enum" : [ + "ich9-intel-hda", + "intel-hda", + "AC97" + ], + "type" : "string" + }, + "driver" : { + "default" : "spice", + "description" : "Driver backend for the audio device.", + "enum" : [ + "spice", + "none" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "autostart" : { + "default" : 0, + "description" : "Automatic restart after crash (currently ignored).", + "optional" : 1, + "type" : "boolean" + }, + "balloon" : { + "description" : "Amount of target RAM for the VM in MiB. Using zero disables the ballon driver.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "bios" : { + "default" : "seabios", + "description" : "Select BIOS implementation.", + "enum" : [ + "seabios", + "ovmf" + ], + "optional" : 1, + "type" : "string" + }, + "boot" : { + "description" : "Specify guest boot order. Use the 'order=' sub-property as usage with no key or 'legacy=' is deprecated.", + "format" : "pve-qm-boot", + "optional" : 1, + "type" : "string" + }, + "bootdisk" : { + "description" : "Enable booting from specified disk. Deprecated: Use 'boot: order=foo;bar' instead.", + "format" : "pve-qm-bootdisk", + "optional" : 1, + "pattern" : "(ide|sata|scsi|virtio)\\d+", + "type" : "string" + }, + "cdrom" : { + "description" : "This is an alias for option -ide2", + "format" : "pve-qm-ide", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "cicustom" : { + "description" : "cloud-init: Specify custom files to replace the automatically generated ones at start.", + "format" : "pve-qm-cicustom", + "optional" : 1, + "type" : "string" + }, + "cipassword" : { + "description" : "cloud-init: Password to assign the user. Using this is generally not recommended. Use ssh keys instead. Also note that older cloud-init versions do not support hashed passwords.", + "optional" : 1, + "type" : "string" + }, + "citype" : { + "description" : "Specifies the cloud-init configuration format. The default depends on the configured operating system type (`ostype`. We use the `nocloud` format for Linux, and `configdrive2` for windows.", + "enum" : [ + "configdrive2", + "nocloud", + "opennebula" + ], + "optional" : 1, + "type" : "string" + }, + "ciupgrade" : { + "default" : 1, + "description" : "cloud-init: do an automatic package upgrade after the first boot.", + "optional" : 1, + "type" : "boolean" + }, + "ciuser" : { + "description" : "cloud-init: User name to change ssh keys and password for instead of the image's configured default user.", + "optional" : 1, + "type" : "string" + }, + "cores" : { + "default" : 1, + "description" : "The number of cores per socket.", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cpu" : { + "description" : "Emulated CPU type.", + "format" : "pve-vm-cpu-conf", + "optional" : 1, + "type" : "string" + }, + "cpulimit" : { + "default" : 0, + "description" : "Limit of CPU usage.", + "maximum" : 128, + "minimum" : 0, + "optional" : 1, + "type" : "number", + "verbose_description" : "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has total of '2' CPU time. Value '0' indicates no CPU limit." + }, + "cpuunits" : { + "default" : "cgroup v1: 1024, cgroup v2: 100", + "description" : "CPU weight for a VM, will be clamped to [1, 10000] in cgroup v2.", + "maximum" : 262144, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "verbose_description" : "CPU weight for a VM. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this VM gets. Number is relative to weights of all the other running VMs." + }, + "description" : { + "description" : "Description for the VM. Shown in the web-interface VM's summary. This is saved as comment inside the configuration file.", + "maxLength" : 8192, + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "SHA1 digest of configuration file. This can be used to prevent concurrent modifications.", + "type" : "string" + }, + "efidisk0" : { + "description" : "Configure a disk for storing EFI vars.", + "format" : { + "efitype" : { + "default" : "2m", + "description" : "Size and type of the OVMF EFI vars. '4m' is newer and recommended, and required for Secure Boot. For backwards compatibility, '2m' is used if not otherwise specified. Ignored for VMs with arch=aarch64 (ARM).", + "enum" : [ + "2m", + "4m" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "pre-enrolled-keys" : { + "default" : 0, + "description" : "Use am EFI vars template with distribution-specific and Microsoft Standard keys enrolled, if used with 'efitype=4m'. Note that this will enable Secure Boot by default, though it can still be turned off from within the VM.", + "optional" : 1, + "type" : "boolean" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string" + }, + "freeze" : { + "description" : "Freeze CPU at startup (use 'c' monitor command to start execution).", + "optional" : 1, + "type" : "boolean" + }, + "hookscript" : { + "description" : "Script that will be executed during various steps in the vms lifetime.", + "format" : "pve-volume-id", + "optional" : 1, + "type" : "string" + }, + "hostpci[n]" : { + "description" : "Map host PCI devices into guest.", + "format" : "pve-qm-hostpci", + "optional" : 1, + "type" : "string", + "verbose_description" : "Map host PCI devices into guest.\n\nNOTE: This option allows direct access to host hardware. So it is no longer\npossible to migrate such machines - use with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "hotplug" : { + "default" : "network,disk,usb", + "description" : "Selectively enable hotplug features. This is a comma separated list of hotplug features: 'network', 'disk', 'cpu', 'memory', 'usb' and 'cloudinit'. Use '0' to disable hotplug completely. Using '1' as value is an alias for the default `network,disk,usb`. USB hotplugging is possible for guests with machine version >= 7.1 and ostype l26 or windows > 7.", + "format" : "pve-hotplug-features", + "optional" : 1, + "type" : "string" + }, + "hugepages" : { + "description" : "Enable/disable hugepages memory.", + "enum" : [ + "any", + "2", + "1024" + ], + "optional" : 1, + "type" : "string" + }, + "ide[n]" : { + "description" : "Use volume as IDE hard disk or CD-ROM (n is 0 to 3).", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "model" : { + "description" : "The drive's reported model name, url-encoded, up to 40 bytes long.", + "format" : "urlencoded", + "format_description" : "model", + "maxLength" : 120, + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "ipconfig[n]" : { + "description" : "cloud-init: Specify IP addresses and gateways for the corresponding interface.\n\nIP addresses use CIDR notation, gateways are optional but need an IP of the same type specified.\n\nThe special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit\ngateway should be provided.\nFor IPv6 the special string 'auto' can be used to use stateless autoconfiguration. This requires\ncloud-init 19.4 or newer.\n\nIf cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using\ndhcp on IPv4.\n", + "format" : "pve-qm-ipconfig", + "optional" : 1, + "type" : "string" + }, + "ivshmem" : { + "description" : "Inter-VM shared memory. Useful for direct communication between VMs, or to the host.", + "format" : { + "name" : { + "description" : "The name of the file. Will be prefixed with 'pve-shm-'. Default is the VMID. Will be deleted when the VM is stopped.", + "format_description" : "string", + "optional" : 1, + "pattern" : "[a-zA-Z0-9\\-]+", + "type" : "string" + }, + "size" : { + "description" : "The size of the file in MB.", + "minimum" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string" + }, + "keephugepages" : { + "default" : 0, + "description" : "Use together with hugepages. If enabled, hugepages will not not be deleted after VM shutdown and can be used for subsequent starts.", + "optional" : 1, + "type" : "boolean" + }, + "keyboard" : { + "default" : null, + "description" : "Keyboard layout for VNC server. This option is generally not required and is often better handled from within the guest OS.", + "enum" : [ + "de", + "de-ch", + "da", + "en-gb", + "en-us", + "es", + "fi", + "fr", + "fr-be", + "fr-ca", + "fr-ch", + "hu", + "is", + "it", + "ja", + "lt", + "mk", + "nl", + "no", + "pl", + "pt", + "pt-br", + "sv", + "sl", + "tr" + ], + "optional" : 1, + "type" : "string" + }, + "kvm" : { + "default" : 1, + "description" : "Enable/disable KVM hardware virtualization.", + "optional" : 1, + "type" : "boolean" + }, + "localtime" : { + "description" : "Set the real time clock (RTC) to local time. This is enabled by default if the `ostype` indicates a Microsoft Windows OS.", + "optional" : 1, + "type" : "boolean" + }, + "lock" : { + "description" : "Lock/unlock the VM.", + "enum" : [ + "backup", + "clone", + "create", + "migrate", + "rollback", + "snapshot", + "snapshot-delete", + "suspending", + "suspended" + ], + "optional" : 1, + "type" : "string" + }, + "machine" : { + "description" : "Specifies the QEMU machine type.", + "maxLength" : 40, + "optional" : 1, + "pattern" : "(pc|pc(-i440fx)?-\\d+(\\.\\d+)+(\\+pve\\d+)?(\\.pxe)?|q35|pc-q35-\\d+(\\.\\d+)+(\\+pve\\d+)?(\\.pxe)?|virt(?:-\\d+(\\.\\d+)+)?(\\+pve\\d+)?)", + "type" : "string" + }, + "memory" : { + "description" : "Memory properties.", + "format" : { + "current" : { + "default" : 512, + "default_key" : 1, + "description" : "Current amount of online RAM for the VM in MiB. This is the maximum available memory when you use the balloon device.", + "minimum" : 16, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string" + }, + "migrate_downtime" : { + "default" : 0.1, + "description" : "Set maximum tolerated downtime (in seconds) for migrations.", + "minimum" : 0, + "optional" : 1, + "type" : "number" + }, + "migrate_speed" : { + "default" : 0, + "description" : "Set maximum speed (in MB/s) for migrations. Value 0 is no limit.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "name" : { + "description" : "Set a name for the VM. Only used on the configuration web interface.", + "format" : "dns-name", + "optional" : 1, + "type" : "string" + }, + "nameserver" : { + "description" : "cloud-init: Sets DNS server IP address for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.", + "format" : "address-list", + "optional" : 1, + "type" : "string" + }, + "net[n]" : { + "description" : "Specify network devices.", + "format" : { + "bridge" : { + "description" : "Bridge to attach the network device to. The Proxmox VE standard bridge\nis called 'vmbr0'.\n\nIf you do not specify a bridge, we create a kvm user (NATed) network\ndevice, which provides DHCP and DNS services. The following addresses\nare used:\n\n 10.0.2.2 Gateway\n 10.0.2.3 DNS Server\n 10.0.2.4 SMB Server\n\nThe DHCP server assign addresses to the guest starting from 10.0.2.15.\n", + "format" : "pve-bridge-id", + "format_description" : "bridge", + "optional" : 1, + "type" : "string" + }, + "e1000" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82540em" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82544gc" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82545em" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000e" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "firewall" : { + "description" : "Whether this interface should be protected by the firewall.", + "optional" : 1, + "type" : "boolean" + }, + "i82551" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "i82557b" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "i82559er" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "link_down" : { + "description" : "Whether this interface should be disconnected (like pulling the plug).", + "optional" : 1, + "type" : "boolean" + }, + "macaddr" : { + "description" : "MAC address. That address must be unique withing your network. This is automatically generated if not specified.", + "format" : "mac-addr", + "format_description" : "XX:XX:XX:XX:XX:XX", + "optional" : 1, + "type" : "string", + "verbose_description" : "A common MAC address with the I/G (Individual/Group) bit not set." + }, + "model" : { + "default_key" : 1, + "description" : "Network Card Model. The 'virtio' model provides the best performance with very low CPU overhead. If your guest does not support this driver, it is usually best to use 'e1000'.", + "enum" : [ + "e1000", + "e1000-82540em", + "e1000-82544gc", + "e1000-82545em", + "e1000e", + "i82551", + "i82557b", + "i82559er", + "ne2k_isa", + "ne2k_pci", + "pcnet", + "rtl8139", + "virtio", + "vmxnet3" + ], + "type" : "string" + }, + "mtu" : { + "description" : "Force MTU, for VirtIO only. Set to '1' to use the bridge MTU", + "maximum" : 65520, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "ne2k_isa" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "ne2k_pci" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "pcnet" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "queues" : { + "description" : "Number of packet queues to be used on the device.", + "maximum" : 64, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "rate" : { + "description" : "Rate limit in mbps (megabytes per second) as floating point number.", + "minimum" : 0, + "optional" : 1, + "type" : "number" + }, + "rtl8139" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "tag" : { + "description" : "VLAN tag to apply to packets on this interface.", + "maximum" : 4094, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "trunks" : { + "description" : "VLAN trunks to pass through this interface.", + "format_description" : "vlanid[;vlanid...]", + "optional" : 1, + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "virtio" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "vmxnet3" : { + "alias" : "macaddr", + "keyAlias" : "model" + } + }, + "optional" : 1, + "type" : "string" + }, + "numa" : { + "default" : 0, + "description" : "Enable/disable NUMA.", + "optional" : 1, + "type" : "boolean" + }, + "numa[n]" : { + "description" : "NUMA topology.", + "format" : { + "cpus" : { + "description" : "CPUs accessing this NUMA node.", + "format_description" : "id[-id];...", + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "hostnodes" : { + "description" : "Host NUMA nodes to use.", + "format_description" : "id[-id];...", + "optional" : 1, + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "memory" : { + "description" : "Amount of memory this NUMA node provides.", + "optional" : 1, + "type" : "number" + }, + "policy" : { + "description" : "NUMA allocation policy.", + "enum" : [ + "preferred", + "bind", + "interleave" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "onboot" : { + "default" : 0, + "description" : "Specifies whether a VM will be started during system bootup.", + "optional" : 1, + "type" : "boolean" + }, + "ostype" : { + "description" : "Specify guest operating system.", + "enum" : [ + "other", + "wxp", + "w2k", + "w2k3", + "w2k8", + "wvista", + "win7", + "win8", + "win10", + "win11", + "l24", + "l26", + "solaris" + ], + "optional" : 1, + "type" : "string", + "verbose_description" : "Specify guest operating system. This is used to enable special\noptimization/features for specific operating systems:\n\n[horizontal]\nother;; unspecified OS\nwxp;; Microsoft Windows XP\nw2k;; Microsoft Windows 2000\nw2k3;; Microsoft Windows 2003\nw2k8;; Microsoft Windows 2008\nwvista;; Microsoft Windows Vista\nwin7;; Microsoft Windows 7\nwin8;; Microsoft Windows 8/2012/2012r2\nwin10;; Microsoft Windows 10/2016/2019\nwin11;; Microsoft Windows 11/2022\nl24;; Linux 2.4 Kernel\nl26;; Linux 2.6 - 6.X Kernel\nsolaris;; Solaris/OpenSolaris/OpenIndiania kernel\n" + }, + "parallel[n]" : { + "description" : "Map host parallel devices (n is 0 to 2).", + "optional" : 1, + "pattern" : "/dev/parport\\d+|/dev/usb/lp\\d+", + "type" : "string", + "verbose_description" : "Map host parallel devices (n is 0 to 2).\n\nNOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such\nmachines - use with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "protection" : { + "default" : 0, + "description" : "Sets the protection flag of the VM. This will disable the remove VM and remove disk operations.", + "optional" : 1, + "type" : "boolean" + }, + "reboot" : { + "default" : 1, + "description" : "Allow reboot. If set to '0' the VM exit on reboot.", + "optional" : 1, + "type" : "boolean" + }, + "rng0" : { + "description" : "Configure a VirtIO-based Random Number Generator.", + "format" : { + "max_bytes" : { + "default" : 1024, + "description" : "Maximum bytes of entropy allowed to get injected into the guest every 'period' milliseconds. Prefer a lower value when using '/dev/random' as source. Use `0` to disable limiting (potentially dangerous!).", + "optional" : 1, + "type" : "integer" + }, + "period" : { + "default" : 1000, + "description" : "Every 'period' milliseconds the entropy-injection quota is reset, allowing the guest to retrieve another 'max_bytes' of entropy.", + "optional" : 1, + "type" : "integer" + }, + "source" : { + "default_key" : 1, + "description" : "The file on the host to gather entropy from. In most cases '/dev/urandom' should be preferred over '/dev/random' to avoid entropy-starvation issues on the host. Using urandom does *not* decrease security in any meaningful way, as it's still seeded from real entropy, and the bytes provided will most likely be mixed with real entropy on the guest as well. '/dev/hwrng' can be used to pass through a hardware RNG from the host.", + "enum" : [ + "/dev/urandom", + "/dev/random", + "/dev/hwrng" + ], + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "sata[n]" : { + "description" : "Use volume as SATA hard disk or CD-ROM (n is 0 to 5).", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "scsi[n]" : { + "description" : "Use volume as SCSI hard disk or CD-ROM (n is 0 to 30).", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iothread" : { + "description" : "Whether to use iothreads for this drive", + "optional" : 1, + "type" : "boolean" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "queues" : { + "description" : "Number of queues.", + "minimum" : 2, + "optional" : 1, + "type" : "integer" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "ro" : { + "description" : "Whether the drive is read-only.", + "optional" : 1, + "type" : "boolean" + }, + "scsiblock" : { + "default" : 0, + "description" : "whether to use scsi-block for full passthrough of host block device\n\nWARNING: can lead to I/O errors in combination with low memory or high memory fragmentation on host", + "optional" : 1, + "type" : "boolean" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "scsihw" : { + "default" : "lsi", + "description" : "SCSI controller model", + "enum" : [ + "lsi", + "lsi53c810", + "virtio-scsi-pci", + "virtio-scsi-single", + "megasas", + "pvscsi" + ], + "optional" : 1, + "type" : "string" + }, + "searchdomain" : { + "description" : "cloud-init: Sets DNS search domains for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.", + "optional" : 1, + "type" : "string" + }, + "serial[n]" : { + "description" : "Create a serial device inside the VM (n is 0 to 3)", + "optional" : 1, + "pattern" : "(/dev/.+|socket)", + "type" : "string", + "verbose_description" : "Create a serial device inside the VM (n is 0 to 3), and pass through a\nhost serial device (i.e. /dev/ttyS0), or create a unix socket on the\nhost side (use 'qm terminal' to open a terminal connection).\n\nNOTE: If you pass through a host serial device, it is no longer possible to migrate such machines -\nuse with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "shares" : { + "default" : 1000, + "description" : "Amount of memory shares for auto-ballooning. The larger the number is, the more memory this VM gets. Number is relative to weights of all other running VMs. Using zero disables auto-ballooning. Auto-ballooning is done by pvestatd.", + "maximum" : 50000, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "smbios1" : { + "description" : "Specify SMBIOS type 1 fields.", + "format" : "pve-qm-smbios1", + "maxLength" : 512, + "optional" : 1, + "type" : "string" + }, + "smp" : { + "default" : 1, + "description" : "The number of CPUs. Please use option -sockets instead.", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "sockets" : { + "default" : 1, + "description" : "The number of CPU sockets.", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "spice_enhancements" : { + "description" : "Configure additional enhancements for SPICE.", + "format" : { + "foldersharing" : { + "default" : "0", + "description" : "Enable folder sharing via SPICE. Needs Spice-WebDAV daemon installed in the VM.", + "optional" : 1, + "type" : "boolean" + }, + "videostreaming" : { + "default" : "off", + "description" : "Enable video streaming. Uses compression for detected video streams.", + "enum" : [ + "off", + "all", + "filter" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "sshkeys" : { + "description" : "cloud-init: Setup public SSH keys (one key per line, OpenSSH format).", + "format" : "urlencoded", + "optional" : 1, + "type" : "string" + }, + "startdate" : { + "default" : "now", + "description" : "Set the initial date of the real time clock. Valid format for date are:'now' or '2006-06-17T16:01:21' or '2006-06-17'.", + "optional" : 1, + "pattern" : "(now|\\d{4}-\\d{1,2}-\\d{1,2}(T\\d{1,2}:\\d{1,2}:\\d{1,2})?)", + "type" : "string", + "typetext" : "(now | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS)" + }, + "startup" : { + "description" : "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped.", + "format" : "pve-startup-order", + "optional" : 1, + "type" : "string", + "typetext" : "[[order=]\\d+] [,up=\\d+] [,down=\\d+] " + }, + "tablet" : { + "default" : 1, + "description" : "Enable/disable the USB tablet device.", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Enable/disable the USB tablet device. This device is usually needed to allow absolute mouse positioning with VNC. Else the mouse runs out of sync with normal VNC clients. If you're running lots of console-only guests on one host, you may consider disabling this to save some context switches. This is turned off by default if you use spice (`qm set --vga qxl`)." + }, + "tags" : { + "description" : "Tags of the VM. This is only meta information.", + "format" : "pve-tag-list", + "optional" : 1, + "type" : "string" + }, + "tdf" : { + "default" : 0, + "description" : "Enable/disable time drift fix.", + "optional" : 1, + "type" : "boolean" + }, + "template" : { + "default" : 0, + "description" : "Enable/disable Template.", + "optional" : 1, + "type" : "boolean" + }, + "tpmstate0" : { + "description" : "Configure a Disk for storing TPM state. The format is fixed to 'raw'.", + "format" : { + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "version" : { + "default" : "v2.0", + "description" : "The TPM interface version. v2.0 is newer and should be preferred. Note that this cannot be changed later on.", + "enum" : [ + "v1.2", + "v2.0" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string" + }, + "unused[n]" : { + "description" : "Reference to unused volumes. This is used internally, and should not be modified manually.", + "format" : { + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id", + "format_description" : "volume", + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string" + }, + "usb[n]" : { + "description" : "Configure an USB device (n is 0 to 4, for machine version >= 7.1 and ostype l26 or windows > 7, n can be up to 14).", + "format" : { + "host" : { + "default_key" : 1, + "description" : "The Host USB device or port or the value 'spice'. HOSTUSBDEVICE syntax is:\n\n 'bus-port(.port)*' (decimal numbers) or\n 'vendor_id:product_id' (hexadeciaml numbers) or\n 'spice'\n\nYou can use the 'lsusb -t' command to list existing usb devices.\n\nNOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such\nmachines - use with special care.\n\nThe value 'spice' can be used to add a usb redirection devices for spice.\n\nEither this or the 'mapping' key must be set.\n", + "format_description" : "HOSTUSBDEVICE|spice", + "optional" : 1, + "pattern" : "(?^:(?:(?:(?^:(0x)?([0-9A-Fa-f]{4}):(0x)?([0-9A-Fa-f]{4})))|(?:(?^:(\\d+)\\-(\\d+(\\.\\d+)*)))|[Ss][Pp][Ii][Cc][Ee]))", + "type" : "string" + }, + "mapping" : { + "description" : "The ID of a cluster wide mapping. Either this or the default-key 'host' must be set.", + "format" : "pve-configid", + "format_description" : "mapping-id", + "optional" : 1, + "type" : "string" + }, + "usb3" : { + "default" : 0, + "description" : "Specifies whether if given host option is a USB3 device or port. For modern guests (machine version >= 7.1 and ostype l26 and windows > 7), this flag is irrelevant (all devices are plugged into a xhci controller).", + "optional" : 1, + "type" : "boolean" + } + }, + "optional" : 1, + "type" : "string" + }, + "vcpus" : { + "default" : 0, + "description" : "Number of hotplugged vcpus.", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "vga" : { + "description" : "Configure the VGA hardware.", + "format" : { + "clipboard" : { + "description" : "Enable a specific clipboard. If not set, depending on the display type the SPICE one will be added.", + "enum" : [ + "vnc" + ], + "optional" : 1, + "type" : "string" + }, + "memory" : { + "description" : "Sets the VGA memory (in MiB). Has no effect with serial display.", + "maximum" : 512, + "minimum" : 4, + "optional" : 1, + "type" : "integer" + }, + "type" : { + "default" : "std", + "default_key" : 1, + "description" : "Select the VGA type.", + "enum" : [ + "cirrus", + "qxl", + "qxl2", + "qxl3", + "qxl4", + "none", + "serial0", + "serial1", + "serial2", + "serial3", + "std", + "virtio", + "virtio-gl", + "vmware" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "verbose_description" : "Configure the VGA Hardware. If you want to use high resolution modes (>= 1280x1024x16) you may need to increase the vga memory option. Since QEMU 2.9 the default VGA display type is 'std' for all OS types besides some Windows versions (XP and older) which use 'cirrus'. The 'qxl' option enables the SPICE display server. For win* OS you can select how many independent displays you want, Linux guests can add displays them self.\nYou can also run without any graphic card, using a serial device as terminal." + }, + "virtio[n]" : { + "description" : "Use volume as VIRTIO hard disk (n is 0 to 15).", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iothread" : { + "description" : "Whether to use iothreads for this drive", + "optional" : 1, + "type" : "boolean" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "ro" : { + "description" : "Whether the drive is read-only.", + "optional" : 1, + "type" : "boolean" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "vmgenid" : { + "default" : "1 (autogenerated)", + "description" : "Set VM Generation ID. Use '1' to autogenerate on create or update, pass '0' to disable explicitly.", + "format_description" : "UUID", + "optional" : 1, + "pattern" : "(?:[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}|[01])", + "type" : "string", + "verbose_description" : "The VM generation ID (vmgenid) device exposes a 128-bit integer value identifier to the guest OS. This allows to notify the guest operating system when the virtual machine is executed with a different configuration (e.g. snapshot execution or creation from a template). The guest operating system notices the change, and is then able to react as appropriate by marking its copies of distributed databases as dirty, re-initializing its random number generator, etc.\nNote that auto-creation only works when done through API/CLI create or update methods, but not when manually editing the config file." + }, + "vmstatestorage" : { + "description" : "Default storage for VM state volumes/files.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string" + }, + "watchdog" : { + "description" : "Create a virtual hardware watchdog device.", + "format" : "pve-qm-watchdog", + "optional" : 1, + "type" : "string", + "verbose_description" : "Create a virtual hardware watchdog device. Once enabled (by a guest action), the watchdog must be periodically polled by an agent inside the guest or else the watchdog will reset the guest (or execute the respective action specified)" + } + }, + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Set virtual machine options (asynchrounous API).", + "method" : "POST", + "name" : "update_vm_async", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "acpi" : { + "default" : 1, + "description" : "Enable/disable ACPI.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "affinity" : { + "description" : "List of host cores used to execute guest processes, for example: 0,5,8-11", + "format" : "pve-cpuset", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "agent" : { + "description" : "Enable/disable communication with the QEMU Guest Agent and its properties.", + "format" : { + "enabled" : { + "default" : 0, + "default_key" : 1, + "description" : "Enable/disable communication with a QEMU Guest Agent (QGA) running in the VM.", + "type" : "boolean" + }, + "freeze-fs-on-backup" : { + "default" : 1, + "description" : "Freeze/thaw guest filesystems on backup for consistency.", + "optional" : 1, + "type" : "boolean" + }, + "fstrim_cloned_disks" : { + "default" : 0, + "description" : "Run fstrim after moving a disk or migrating the VM.", + "optional" : 1, + "type" : "boolean" + }, + "type" : { + "default" : "virtio", + "description" : "Select the agent type", + "enum" : [ + "virtio", + "isa" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[enabled=]<1|0> [,freeze-fs-on-backup=<1|0>] [,fstrim_cloned_disks=<1|0>] [,type=]" + }, + "arch" : { + "description" : "Virtual processor architecture. Defaults to the host.", + "enum" : [ + "x86_64", + "aarch64" + ], + "optional" : 1, + "type" : "string" + }, + "args" : { + "description" : "Arbitrary arguments passed to kvm.", + "optional" : 1, + "type" : "string", + "typetext" : "", + "verbose_description" : "Arbitrary arguments passed to kvm, for example:\n\nargs: -no-reboot -smbios 'type=0,vendor=FOO'\n\nNOTE: this option is for experts only.\n" + }, + "audio0" : { + "description" : "Configure a audio device, useful in combination with QXL/Spice.", + "format" : { + "device" : { + "description" : "Configure an audio device.", + "enum" : [ + "ich9-intel-hda", + "intel-hda", + "AC97" + ], + "type" : "string" + }, + "driver" : { + "default" : "spice", + "description" : "Driver backend for the audio device.", + "enum" : [ + "spice", + "none" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "device= [,driver=]" + }, + "autostart" : { + "default" : 0, + "description" : "Automatic restart after crash (currently ignored).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "background_delay" : { + "description" : "Time to wait for the task to finish. We return 'null' if the task finish within that time.", + "maximum" : 30, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 30)" + }, + "balloon" : { + "description" : "Amount of target RAM for the VM in MiB. Using zero disables the ballon driver.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "bios" : { + "default" : "seabios", + "description" : "Select BIOS implementation.", + "enum" : [ + "seabios", + "ovmf" + ], + "optional" : 1, + "type" : "string" + }, + "boot" : { + "description" : "Specify guest boot order. Use the 'order=' sub-property as usage with no key or 'legacy=' is deprecated.", + "format" : "pve-qm-boot", + "optional" : 1, + "type" : "string", + "typetext" : "[[legacy=]<[acdn]{1,4}>] [,order=]" + }, + "bootdisk" : { + "description" : "Enable booting from specified disk. Deprecated: Use 'boot: order=foo;bar' instead.", + "format" : "pve-qm-bootdisk", + "optional" : 1, + "pattern" : "(ide|sata|scsi|virtio)\\d+", + "type" : "string" + }, + "cdrom" : { + "description" : "This is an alias for option -ide2", + "format" : "pve-qm-ide", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "cicustom" : { + "description" : "cloud-init: Specify custom files to replace the automatically generated ones at start.", + "format" : "pve-qm-cicustom", + "optional" : 1, + "type" : "string", + "typetext" : "[meta=] [,network=] [,user=] [,vendor=]" + }, + "cipassword" : { + "description" : "cloud-init: Password to assign the user. Using this is generally not recommended. Use ssh keys instead. Also note that older cloud-init versions do not support hashed passwords.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "citype" : { + "description" : "Specifies the cloud-init configuration format. The default depends on the configured operating system type (`ostype`. We use the `nocloud` format for Linux, and `configdrive2` for windows.", + "enum" : [ + "configdrive2", + "nocloud", + "opennebula" + ], + "optional" : 1, + "type" : "string" + }, + "ciupgrade" : { + "default" : 1, + "description" : "cloud-init: do an automatic package upgrade after the first boot.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ciuser" : { + "description" : "cloud-init: User name to change ssh keys and password for instead of the image's configured default user.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "cores" : { + "default" : 1, + "description" : "The number of cores per socket.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "cpu" : { + "description" : "Emulated CPU type.", + "format" : "pve-vm-cpu-conf", + "optional" : 1, + "type" : "string", + "typetext" : "[[cputype=]] [,flags=<+FLAG[;-FLAG...]>] [,hidden=<1|0>] [,hv-vendor-id=] [,phys-bits=<8-64|host>] [,reported-model=]" + }, + "cpulimit" : { + "default" : 0, + "description" : "Limit of CPU usage.", + "maximum" : 128, + "minimum" : 0, + "optional" : 1, + "type" : "number", + "typetext" : " (0 - 128)", + "verbose_description" : "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has total of '2' CPU time. Value '0' indicates no CPU limit." + }, + "cpuunits" : { + "default" : "cgroup v1: 1024, cgroup v2: 100", + "description" : "CPU weight for a VM, will be clamped to [1, 10000] in cgroup v2.", + "maximum" : 262144, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 262144)", + "verbose_description" : "CPU weight for a VM. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this VM gets. Number is relative to weights of all the other running VMs." + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "description" : { + "description" : "Description for the VM. Shown in the web-interface VM's summary. This is saved as comment inside the configuration file.", + "maxLength" : 8192, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "efidisk0" : { + "description" : "Configure a disk for storing EFI vars. Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Note that SIZE_IN_GiB is ignored here and that the default EFI vars are copied to the volume instead. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "efitype" : { + "default" : "2m", + "description" : "Size and type of the OVMF EFI vars. '4m' is newer and recommended, and required for Secure Boot. For backwards compatibility, '2m' is used if not otherwise specified. Ignored for VMs with arch=aarch64 (ARM).", + "enum" : [ + "2m", + "4m" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "pre-enrolled-keys" : { + "default" : 0, + "description" : "Use am EFI vars template with distribution-specific and Microsoft Standard keys enrolled, if used with 'efitype=4m'. Note that this will enable Secure Boot by default, though it can still be turned off from within the VM.", + "optional" : 1, + "type" : "boolean" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,efitype=<2m|4m>] [,format=] [,import-from=] [,pre-enrolled-keys=<1|0>] [,size=]" + }, + "force" : { + "description" : "Force physical removal. Without this, we simple remove the disk from the config file and create an additional configuration entry called 'unused[n]', which contains the volume ID. Unlink of unused[n] always cause physical removal.", + "optional" : 1, + "requires" : "delete", + "type" : "boolean", + "typetext" : "" + }, + "freeze" : { + "description" : "Freeze CPU at startup (use 'c' monitor command to start execution).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "hookscript" : { + "description" : "Script that will be executed during various steps in the vms lifetime.", + "format" : "pve-volume-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "hostpci[n]" : { + "description" : "Map host PCI devices into guest.", + "format" : "pve-qm-hostpci", + "optional" : 1, + "type" : "string", + "typetext" : "[[host=]] [,device-id=] [,legacy-igd=<1|0>] [,mapping=] [,mdev=] [,pcie=<1|0>] [,rombar=<1|0>] [,romfile=] [,sub-device-id=] [,sub-vendor-id=] [,vendor-id=] [,x-vga=<1|0>]", + "verbose_description" : "Map host PCI devices into guest.\n\nNOTE: This option allows direct access to host hardware. So it is no longer\npossible to migrate such machines - use with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "hotplug" : { + "default" : "network,disk,usb", + "description" : "Selectively enable hotplug features. This is a comma separated list of hotplug features: 'network', 'disk', 'cpu', 'memory', 'usb' and 'cloudinit'. Use '0' to disable hotplug completely. Using '1' as value is an alias for the default `network,disk,usb`. USB hotplugging is possible for guests with machine version >= 7.1 and ostype l26 or windows > 7.", + "format" : "pve-hotplug-features", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "hugepages" : { + "description" : "Enable/disable hugepages memory.", + "enum" : [ + "any", + "2", + "1024" + ], + "optional" : 1, + "type" : "string" + }, + "ide[n]" : { + "description" : "Use volume as IDE hard disk or CD-ROM (n is 0 to 3). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "model" : { + "description" : "The drive's reported model name, url-encoded, up to 40 bytes long.", + "format" : "urlencoded", + "format_description" : "model", + "maxLength" : 120, + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,model=] [,replicate=<1|0>] [,rerror=] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,ssd=<1|0>] [,trans=] [,werror=] [,wwn=]" + }, + "ipconfig[n]" : { + "description" : "cloud-init: Specify IP addresses and gateways for the corresponding interface.\n\nIP addresses use CIDR notation, gateways are optional but need an IP of the same type specified.\n\nThe special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit\ngateway should be provided.\nFor IPv6 the special string 'auto' can be used to use stateless autoconfiguration. This requires\ncloud-init 19.4 or newer.\n\nIf cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using\ndhcp on IPv4.\n", + "format" : "pve-qm-ipconfig", + "optional" : 1, + "type" : "string", + "typetext" : "[gw=] [,gw6=] [,ip=] [,ip6=]" + }, + "ivshmem" : { + "description" : "Inter-VM shared memory. Useful for direct communication between VMs, or to the host.", + "format" : { + "name" : { + "description" : "The name of the file. Will be prefixed with 'pve-shm-'. Default is the VMID. Will be deleted when the VM is stopped.", + "format_description" : "string", + "optional" : 1, + "pattern" : "[a-zA-Z0-9\\-]+", + "type" : "string" + }, + "size" : { + "description" : "The size of the file in MB.", + "minimum" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "size= [,name=]" + }, + "keephugepages" : { + "default" : 0, + "description" : "Use together with hugepages. If enabled, hugepages will not not be deleted after VM shutdown and can be used for subsequent starts.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "keyboard" : { + "default" : null, + "description" : "Keyboard layout for VNC server. This option is generally not required and is often better handled from within the guest OS.", + "enum" : [ + "de", + "de-ch", + "da", + "en-gb", + "en-us", + "es", + "fi", + "fr", + "fr-be", + "fr-ca", + "fr-ch", + "hu", + "is", + "it", + "ja", + "lt", + "mk", + "nl", + "no", + "pl", + "pt", + "pt-br", + "sv", + "sl", + "tr" + ], + "optional" : 1, + "type" : "string" + }, + "kvm" : { + "default" : 1, + "description" : "Enable/disable KVM hardware virtualization.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "localtime" : { + "description" : "Set the real time clock (RTC) to local time. This is enabled by default if the `ostype` indicates a Microsoft Windows OS.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "lock" : { + "description" : "Lock/unlock the VM.", + "enum" : [ + "backup", + "clone", + "create", + "migrate", + "rollback", + "snapshot", + "snapshot-delete", + "suspending", + "suspended" + ], + "optional" : 1, + "type" : "string" + }, + "machine" : { + "description" : "Specifies the QEMU machine type.", + "maxLength" : 40, + "optional" : 1, + "pattern" : "(pc|pc(-i440fx)?-\\d+(\\.\\d+)+(\\+pve\\d+)?(\\.pxe)?|q35|pc-q35-\\d+(\\.\\d+)+(\\+pve\\d+)?(\\.pxe)?|virt(?:-\\d+(\\.\\d+)+)?(\\+pve\\d+)?)", + "type" : "string" + }, + "memory" : { + "description" : "Memory properties.", + "format" : { + "current" : { + "default" : 512, + "default_key" : 1, + "description" : "Current amount of online RAM for the VM in MiB. This is the maximum available memory when you use the balloon device.", + "minimum" : 16, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[current=]" + }, + "migrate_downtime" : { + "default" : 0.1, + "description" : "Set maximum tolerated downtime (in seconds) for migrations.", + "minimum" : 0, + "optional" : 1, + "type" : "number", + "typetext" : " (0 - N)" + }, + "migrate_speed" : { + "default" : 0, + "description" : "Set maximum speed (in MB/s) for migrations. Value 0 is no limit.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "name" : { + "description" : "Set a name for the VM. Only used on the configuration web interface.", + "format" : "dns-name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "nameserver" : { + "description" : "cloud-init: Sets DNS server IP address for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.", + "format" : "address-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "net[n]" : { + "description" : "Specify network devices.", + "format" : { + "bridge" : { + "description" : "Bridge to attach the network device to. The Proxmox VE standard bridge\nis called 'vmbr0'.\n\nIf you do not specify a bridge, we create a kvm user (NATed) network\ndevice, which provides DHCP and DNS services. The following addresses\nare used:\n\n 10.0.2.2 Gateway\n 10.0.2.3 DNS Server\n 10.0.2.4 SMB Server\n\nThe DHCP server assign addresses to the guest starting from 10.0.2.15.\n", + "format" : "pve-bridge-id", + "format_description" : "bridge", + "optional" : 1, + "type" : "string" + }, + "e1000" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82540em" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82544gc" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82545em" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000e" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "firewall" : { + "description" : "Whether this interface should be protected by the firewall.", + "optional" : 1, + "type" : "boolean" + }, + "i82551" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "i82557b" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "i82559er" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "link_down" : { + "description" : "Whether this interface should be disconnected (like pulling the plug).", + "optional" : 1, + "type" : "boolean" + }, + "macaddr" : { + "description" : "MAC address. That address must be unique withing your network. This is automatically generated if not specified.", + "format" : "mac-addr", + "format_description" : "XX:XX:XX:XX:XX:XX", + "optional" : 1, + "type" : "string", + "verbose_description" : "A common MAC address with the I/G (Individual/Group) bit not set." + }, + "model" : { + "default_key" : 1, + "description" : "Network Card Model. The 'virtio' model provides the best performance with very low CPU overhead. If your guest does not support this driver, it is usually best to use 'e1000'.", + "enum" : [ + "e1000", + "e1000-82540em", + "e1000-82544gc", + "e1000-82545em", + "e1000e", + "i82551", + "i82557b", + "i82559er", + "ne2k_isa", + "ne2k_pci", + "pcnet", + "rtl8139", + "virtio", + "vmxnet3" + ], + "type" : "string" + }, + "mtu" : { + "description" : "Force MTU, for VirtIO only. Set to '1' to use the bridge MTU", + "maximum" : 65520, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "ne2k_isa" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "ne2k_pci" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "pcnet" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "queues" : { + "description" : "Number of packet queues to be used on the device.", + "maximum" : 64, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "rate" : { + "description" : "Rate limit in mbps (megabytes per second) as floating point number.", + "minimum" : 0, + "optional" : 1, + "type" : "number" + }, + "rtl8139" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "tag" : { + "description" : "VLAN tag to apply to packets on this interface.", + "maximum" : 4094, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "trunks" : { + "description" : "VLAN trunks to pass through this interface.", + "format_description" : "vlanid[;vlanid...]", + "optional" : 1, + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "virtio" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "vmxnet3" : { + "alias" : "macaddr", + "keyAlias" : "model" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[model=] [,bridge=] [,firewall=<1|0>] [,link_down=<1|0>] [,macaddr=] [,mtu=] [,queues=] [,rate=] [,tag=] [,trunks=] [,=]" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "numa" : { + "default" : 0, + "description" : "Enable/disable NUMA.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "numa[n]" : { + "description" : "NUMA topology.", + "format" : { + "cpus" : { + "description" : "CPUs accessing this NUMA node.", + "format_description" : "id[-id];...", + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "hostnodes" : { + "description" : "Host NUMA nodes to use.", + "format_description" : "id[-id];...", + "optional" : 1, + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "memory" : { + "description" : "Amount of memory this NUMA node provides.", + "optional" : 1, + "type" : "number" + }, + "policy" : { + "description" : "NUMA allocation policy.", + "enum" : [ + "preferred", + "bind", + "interleave" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "cpus= [,hostnodes=] [,memory=] [,policy=]" + }, + "onboot" : { + "default" : 0, + "description" : "Specifies whether a VM will be started during system bootup.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ostype" : { + "description" : "Specify guest operating system.", + "enum" : [ + "other", + "wxp", + "w2k", + "w2k3", + "w2k8", + "wvista", + "win7", + "win8", + "win10", + "win11", + "l24", + "l26", + "solaris" + ], + "optional" : 1, + "type" : "string", + "verbose_description" : "Specify guest operating system. This is used to enable special\noptimization/features for specific operating systems:\n\n[horizontal]\nother;; unspecified OS\nwxp;; Microsoft Windows XP\nw2k;; Microsoft Windows 2000\nw2k3;; Microsoft Windows 2003\nw2k8;; Microsoft Windows 2008\nwvista;; Microsoft Windows Vista\nwin7;; Microsoft Windows 7\nwin8;; Microsoft Windows 8/2012/2012r2\nwin10;; Microsoft Windows 10/2016/2019\nwin11;; Microsoft Windows 11/2022\nl24;; Linux 2.4 Kernel\nl26;; Linux 2.6 - 6.X Kernel\nsolaris;; Solaris/OpenSolaris/OpenIndiania kernel\n" + }, + "parallel[n]" : { + "description" : "Map host parallel devices (n is 0 to 2).", + "optional" : 1, + "pattern" : "/dev/parport\\d+|/dev/usb/lp\\d+", + "type" : "string", + "verbose_description" : "Map host parallel devices (n is 0 to 2).\n\nNOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such\nmachines - use with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "protection" : { + "default" : 0, + "description" : "Sets the protection flag of the VM. This will disable the remove VM and remove disk operations.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "reboot" : { + "default" : 1, + "description" : "Allow reboot. If set to '0' the VM exit on reboot.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "revert" : { + "description" : "Revert a pending change.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "rng0" : { + "description" : "Configure a VirtIO-based Random Number Generator.", + "format" : { + "max_bytes" : { + "default" : 1024, + "description" : "Maximum bytes of entropy allowed to get injected into the guest every 'period' milliseconds. Prefer a lower value when using '/dev/random' as source. Use `0` to disable limiting (potentially dangerous!).", + "optional" : 1, + "type" : "integer" + }, + "period" : { + "default" : 1000, + "description" : "Every 'period' milliseconds the entropy-injection quota is reset, allowing the guest to retrieve another 'max_bytes' of entropy.", + "optional" : 1, + "type" : "integer" + }, + "source" : { + "default_key" : 1, + "description" : "The file on the host to gather entropy from. In most cases '/dev/urandom' should be preferred over '/dev/random' to avoid entropy-starvation issues on the host. Using urandom does *not* decrease security in any meaningful way, as it's still seeded from real entropy, and the bytes provided will most likely be mixed with real entropy on the guest as well. '/dev/hwrng' can be used to pass through a hardware RNG from the host.", + "enum" : [ + "/dev/urandom", + "/dev/random", + "/dev/hwrng" + ], + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[source=] [,max_bytes=] [,period=]" + }, + "sata[n]" : { + "description" : "Use volume as SATA hard disk or CD-ROM (n is 0 to 5). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,replicate=<1|0>] [,rerror=] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,ssd=<1|0>] [,trans=] [,werror=] [,wwn=]" + }, + "scsi[n]" : { + "description" : "Use volume as SCSI hard disk or CD-ROM (n is 0 to 30). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iothread" : { + "description" : "Whether to use iothreads for this drive", + "optional" : 1, + "type" : "boolean" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "queues" : { + "description" : "Number of queues.", + "minimum" : 2, + "optional" : 1, + "type" : "integer" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "ro" : { + "description" : "Whether the drive is read-only.", + "optional" : 1, + "type" : "boolean" + }, + "scsiblock" : { + "default" : 0, + "description" : "whether to use scsi-block for full passthrough of host block device\n\nWARNING: can lead to I/O errors in combination with low memory or high memory fragmentation on host", + "optional" : 1, + "type" : "boolean" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,iothread=<1|0>] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,queues=] [,replicate=<1|0>] [,rerror=] [,ro=<1|0>] [,scsiblock=<1|0>] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,ssd=<1|0>] [,trans=] [,werror=] [,wwn=]" + }, + "scsihw" : { + "default" : "lsi", + "description" : "SCSI controller model", + "enum" : [ + "lsi", + "lsi53c810", + "virtio-scsi-pci", + "virtio-scsi-single", + "megasas", + "pvscsi" + ], + "optional" : 1, + "type" : "string" + }, + "searchdomain" : { + "description" : "cloud-init: Sets DNS search domains for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "serial[n]" : { + "description" : "Create a serial device inside the VM (n is 0 to 3)", + "optional" : 1, + "pattern" : "(/dev/.+|socket)", + "type" : "string", + "verbose_description" : "Create a serial device inside the VM (n is 0 to 3), and pass through a\nhost serial device (i.e. /dev/ttyS0), or create a unix socket on the\nhost side (use 'qm terminal' to open a terminal connection).\n\nNOTE: If you pass through a host serial device, it is no longer possible to migrate such machines -\nuse with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "shares" : { + "default" : 1000, + "description" : "Amount of memory shares for auto-ballooning. The larger the number is, the more memory this VM gets. Number is relative to weights of all other running VMs. Using zero disables auto-ballooning. Auto-ballooning is done by pvestatd.", + "maximum" : 50000, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 50000)" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "smbios1" : { + "description" : "Specify SMBIOS type 1 fields.", + "format" : "pve-qm-smbios1", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "[base64=<1|0>] [,family=] [,manufacturer=] [,product=] [,serial=] [,sku=] [,uuid=] [,version=]" + }, + "smp" : { + "default" : 1, + "description" : "The number of CPUs. Please use option -sockets instead.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "sockets" : { + "default" : 1, + "description" : "The number of CPU sockets.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "spice_enhancements" : { + "description" : "Configure additional enhancements for SPICE.", + "format" : { + "foldersharing" : { + "default" : "0", + "description" : "Enable folder sharing via SPICE. Needs Spice-WebDAV daemon installed in the VM.", + "optional" : 1, + "type" : "boolean" + }, + "videostreaming" : { + "default" : "off", + "description" : "Enable video streaming. Uses compression for detected video streams.", + "enum" : [ + "off", + "all", + "filter" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[foldersharing=<1|0>] [,videostreaming=]" + }, + "sshkeys" : { + "description" : "cloud-init: Setup public SSH keys (one key per line, OpenSSH format).", + "format" : "urlencoded", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "startdate" : { + "default" : "now", + "description" : "Set the initial date of the real time clock. Valid format for date are:'now' or '2006-06-17T16:01:21' or '2006-06-17'.", + "optional" : 1, + "pattern" : "(now|\\d{4}-\\d{1,2}-\\d{1,2}(T\\d{1,2}:\\d{1,2}:\\d{1,2})?)", + "type" : "string", + "typetext" : "(now | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS)" + }, + "startup" : { + "description" : "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped.", + "format" : "pve-startup-order", + "optional" : 1, + "type" : "string", + "typetext" : "[[order=]\\d+] [,up=\\d+] [,down=\\d+] " + }, + "tablet" : { + "default" : 1, + "description" : "Enable/disable the USB tablet device.", + "optional" : 1, + "type" : "boolean", + "typetext" : "", + "verbose_description" : "Enable/disable the USB tablet device. This device is usually needed to allow absolute mouse positioning with VNC. Else the mouse runs out of sync with normal VNC clients. If you're running lots of console-only guests on one host, you may consider disabling this to save some context switches. This is turned off by default if you use spice (`qm set --vga qxl`)." + }, + "tags" : { + "description" : "Tags of the VM. This is only meta information.", + "format" : "pve-tag-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tdf" : { + "default" : 0, + "description" : "Enable/disable time drift fix.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "template" : { + "default" : 0, + "description" : "Enable/disable Template.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "tpmstate0" : { + "description" : "Configure a Disk for storing TPM state. The format is fixed to 'raw'. Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Note that SIZE_IN_GiB is ignored here and 4 MiB will be used instead. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "version" : { + "default" : "v2.0", + "description" : "The TPM interface version. v2.0 is newer and should be preferred. Note that this cannot be changed later on.", + "enum" : [ + "v1.2", + "v2.0" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,import-from=] [,size=] [,version=]" + }, + "unused[n]" : { + "description" : "Reference to unused volumes. This is used internally, and should not be modified manually.", + "format" : { + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id", + "format_description" : "volume", + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=]" + }, + "usb[n]" : { + "description" : "Configure an USB device (n is 0 to 4, for machine version >= 7.1 and ostype l26 or windows > 7, n can be up to 14).", + "format" : { + "host" : { + "default_key" : 1, + "description" : "The Host USB device or port or the value 'spice'. HOSTUSBDEVICE syntax is:\n\n 'bus-port(.port)*' (decimal numbers) or\n 'vendor_id:product_id' (hexadeciaml numbers) or\n 'spice'\n\nYou can use the 'lsusb -t' command to list existing usb devices.\n\nNOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such\nmachines - use with special care.\n\nThe value 'spice' can be used to add a usb redirection devices for spice.\n\nEither this or the 'mapping' key must be set.\n", + "format_description" : "HOSTUSBDEVICE|spice", + "optional" : 1, + "pattern" : "(?^:(?:(?:(?^:(0x)?([0-9A-Fa-f]{4}):(0x)?([0-9A-Fa-f]{4})))|(?:(?^:(\\d+)\\-(\\d+(\\.\\d+)*)))|[Ss][Pp][Ii][Cc][Ee]))", + "type" : "string" + }, + "mapping" : { + "description" : "The ID of a cluster wide mapping. Either this or the default-key 'host' must be set.", + "format" : "pve-configid", + "format_description" : "mapping-id", + "optional" : 1, + "type" : "string" + }, + "usb3" : { + "default" : 0, + "description" : "Specifies whether if given host option is a USB3 device or port. For modern guests (machine version >= 7.1 and ostype l26 and windows > 7), this flag is irrelevant (all devices are plugged into a xhci controller).", + "optional" : 1, + "type" : "boolean" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[[host=]] [,mapping=] [,usb3=<1|0>]" + }, + "vcpus" : { + "default" : 0, + "description" : "Number of hotplugged vcpus.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "vga" : { + "description" : "Configure the VGA hardware.", + "format" : { + "clipboard" : { + "description" : "Enable a specific clipboard. If not set, depending on the display type the SPICE one will be added.", + "enum" : [ + "vnc" + ], + "optional" : 1, + "type" : "string" + }, + "memory" : { + "description" : "Sets the VGA memory (in MiB). Has no effect with serial display.", + "maximum" : 512, + "minimum" : 4, + "optional" : 1, + "type" : "integer" + }, + "type" : { + "default" : "std", + "default_key" : 1, + "description" : "Select the VGA type.", + "enum" : [ + "cirrus", + "qxl", + "qxl2", + "qxl3", + "qxl4", + "none", + "serial0", + "serial1", + "serial2", + "serial3", + "std", + "virtio", + "virtio-gl", + "vmware" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[[type=]] [,clipboard=] [,memory=]", + "verbose_description" : "Configure the VGA Hardware. If you want to use high resolution modes (>= 1280x1024x16) you may need to increase the vga memory option. Since QEMU 2.9 the default VGA display type is 'std' for all OS types besides some Windows versions (XP and older) which use 'cirrus'. The 'qxl' option enables the SPICE display server. For win* OS you can select how many independent displays you want, Linux guests can add displays them self.\nYou can also run without any graphic card, using a serial device as terminal." + }, + "virtio[n]" : { + "description" : "Use volume as VIRTIO hard disk (n is 0 to 15). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iothread" : { + "description" : "Whether to use iothreads for this drive", + "optional" : 1, + "type" : "boolean" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "ro" : { + "description" : "Whether the drive is read-only.", + "optional" : 1, + "type" : "boolean" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,iothread=<1|0>] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,replicate=<1|0>] [,rerror=] [,ro=<1|0>] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,trans=] [,werror=]" + }, + "vmgenid" : { + "default" : "1 (autogenerated)", + "description" : "Set VM Generation ID. Use '1' to autogenerate on create or update, pass '0' to disable explicitly.", + "format_description" : "UUID", + "optional" : 1, + "pattern" : "(?:[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}|[01])", + "type" : "string", + "verbose_description" : "The VM generation ID (vmgenid) device exposes a 128-bit integer value identifier to the guest OS. This allows to notify the guest operating system when the virtual machine is executed with a different configuration (e.g. snapshot execution or creation from a template). The guest operating system notices the change, and is then able to react as appropriate by marking its copies of distributed databases as dirty, re-initializing its random number generator, etc.\nNote that auto-creation only works when done through API/CLI create or update methods, but not when manually editing the config file." + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "vmstatestorage" : { + "description" : "Default storage for VM state volumes/files.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "watchdog" : { + "description" : "Create a virtual hardware watchdog device.", + "format" : "pve-qm-watchdog", + "optional" : 1, + "type" : "string", + "typetext" : "[[model=]] [,action=]", + "verbose_description" : "Create a virtual hardware watchdog device. Once enabled (by a guest action), the watchdog must be periodically polled by an agent inside the guest or else the watchdog will reset the guest (or execute the respective action specified)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Disk", + "VM.Config.CDROM", + "VM.Config.CPU", + "VM.Config.Memory", + "VM.Config.Network", + "VM.Config.HWType", + "VM.Config.Options", + "VM.Config.Cloudinit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "optional" : 1, + "type" : "string" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.", + "method" : "PUT", + "name" : "update_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "acpi" : { + "default" : 1, + "description" : "Enable/disable ACPI.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "affinity" : { + "description" : "List of host cores used to execute guest processes, for example: 0,5,8-11", + "format" : "pve-cpuset", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "agent" : { + "description" : "Enable/disable communication with the QEMU Guest Agent and its properties.", + "format" : { + "enabled" : { + "default" : 0, + "default_key" : 1, + "description" : "Enable/disable communication with a QEMU Guest Agent (QGA) running in the VM.", + "type" : "boolean" + }, + "freeze-fs-on-backup" : { + "default" : 1, + "description" : "Freeze/thaw guest filesystems on backup for consistency.", + "optional" : 1, + "type" : "boolean" + }, + "fstrim_cloned_disks" : { + "default" : 0, + "description" : "Run fstrim after moving a disk or migrating the VM.", + "optional" : 1, + "type" : "boolean" + }, + "type" : { + "default" : "virtio", + "description" : "Select the agent type", + "enum" : [ + "virtio", + "isa" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[enabled=]<1|0> [,freeze-fs-on-backup=<1|0>] [,fstrim_cloned_disks=<1|0>] [,type=]" + }, + "arch" : { + "description" : "Virtual processor architecture. Defaults to the host.", + "enum" : [ + "x86_64", + "aarch64" + ], + "optional" : 1, + "type" : "string" + }, + "args" : { + "description" : "Arbitrary arguments passed to kvm.", + "optional" : 1, + "type" : "string", + "typetext" : "", + "verbose_description" : "Arbitrary arguments passed to kvm, for example:\n\nargs: -no-reboot -smbios 'type=0,vendor=FOO'\n\nNOTE: this option is for experts only.\n" + }, + "audio0" : { + "description" : "Configure a audio device, useful in combination with QXL/Spice.", + "format" : { + "device" : { + "description" : "Configure an audio device.", + "enum" : [ + "ich9-intel-hda", + "intel-hda", + "AC97" + ], + "type" : "string" + }, + "driver" : { + "default" : "spice", + "description" : "Driver backend for the audio device.", + "enum" : [ + "spice", + "none" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "device= [,driver=]" + }, + "autostart" : { + "default" : 0, + "description" : "Automatic restart after crash (currently ignored).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "balloon" : { + "description" : "Amount of target RAM for the VM in MiB. Using zero disables the ballon driver.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "bios" : { + "default" : "seabios", + "description" : "Select BIOS implementation.", + "enum" : [ + "seabios", + "ovmf" + ], + "optional" : 1, + "type" : "string" + }, + "boot" : { + "description" : "Specify guest boot order. Use the 'order=' sub-property as usage with no key or 'legacy=' is deprecated.", + "format" : "pve-qm-boot", + "optional" : 1, + "type" : "string", + "typetext" : "[[legacy=]<[acdn]{1,4}>] [,order=]" + }, + "bootdisk" : { + "description" : "Enable booting from specified disk. Deprecated: Use 'boot: order=foo;bar' instead.", + "format" : "pve-qm-bootdisk", + "optional" : 1, + "pattern" : "(ide|sata|scsi|virtio)\\d+", + "type" : "string" + }, + "cdrom" : { + "description" : "This is an alias for option -ide2", + "format" : "pve-qm-ide", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "cicustom" : { + "description" : "cloud-init: Specify custom files to replace the automatically generated ones at start.", + "format" : "pve-qm-cicustom", + "optional" : 1, + "type" : "string", + "typetext" : "[meta=] [,network=] [,user=] [,vendor=]" + }, + "cipassword" : { + "description" : "cloud-init: Password to assign the user. Using this is generally not recommended. Use ssh keys instead. Also note that older cloud-init versions do not support hashed passwords.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "citype" : { + "description" : "Specifies the cloud-init configuration format. The default depends on the configured operating system type (`ostype`. We use the `nocloud` format for Linux, and `configdrive2` for windows.", + "enum" : [ + "configdrive2", + "nocloud", + "opennebula" + ], + "optional" : 1, + "type" : "string" + }, + "ciupgrade" : { + "default" : 1, + "description" : "cloud-init: do an automatic package upgrade after the first boot.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ciuser" : { + "description" : "cloud-init: User name to change ssh keys and password for instead of the image's configured default user.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "cores" : { + "default" : 1, + "description" : "The number of cores per socket.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "cpu" : { + "description" : "Emulated CPU type.", + "format" : "pve-vm-cpu-conf", + "optional" : 1, + "type" : "string", + "typetext" : "[[cputype=]] [,flags=<+FLAG[;-FLAG...]>] [,hidden=<1|0>] [,hv-vendor-id=] [,phys-bits=<8-64|host>] [,reported-model=]" + }, + "cpulimit" : { + "default" : 0, + "description" : "Limit of CPU usage.", + "maximum" : 128, + "minimum" : 0, + "optional" : 1, + "type" : "number", + "typetext" : " (0 - 128)", + "verbose_description" : "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has total of '2' CPU time. Value '0' indicates no CPU limit." + }, + "cpuunits" : { + "default" : "cgroup v1: 1024, cgroup v2: 100", + "description" : "CPU weight for a VM, will be clamped to [1, 10000] in cgroup v2.", + "maximum" : 262144, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 262144)", + "verbose_description" : "CPU weight for a VM. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this VM gets. Number is relative to weights of all the other running VMs." + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "description" : { + "description" : "Description for the VM. Shown in the web-interface VM's summary. This is saved as comment inside the configuration file.", + "maxLength" : 8192, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "efidisk0" : { + "description" : "Configure a disk for storing EFI vars. Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Note that SIZE_IN_GiB is ignored here and that the default EFI vars are copied to the volume instead. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "efitype" : { + "default" : "2m", + "description" : "Size and type of the OVMF EFI vars. '4m' is newer and recommended, and required for Secure Boot. For backwards compatibility, '2m' is used if not otherwise specified. Ignored for VMs with arch=aarch64 (ARM).", + "enum" : [ + "2m", + "4m" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "pre-enrolled-keys" : { + "default" : 0, + "description" : "Use am EFI vars template with distribution-specific and Microsoft Standard keys enrolled, if used with 'efitype=4m'. Note that this will enable Secure Boot by default, though it can still be turned off from within the VM.", + "optional" : 1, + "type" : "boolean" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,efitype=<2m|4m>] [,format=] [,import-from=] [,pre-enrolled-keys=<1|0>] [,size=]" + }, + "force" : { + "description" : "Force physical removal. Without this, we simple remove the disk from the config file and create an additional configuration entry called 'unused[n]', which contains the volume ID. Unlink of unused[n] always cause physical removal.", + "optional" : 1, + "requires" : "delete", + "type" : "boolean", + "typetext" : "" + }, + "freeze" : { + "description" : "Freeze CPU at startup (use 'c' monitor command to start execution).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "hookscript" : { + "description" : "Script that will be executed during various steps in the vms lifetime.", + "format" : "pve-volume-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "hostpci[n]" : { + "description" : "Map host PCI devices into guest.", + "format" : "pve-qm-hostpci", + "optional" : 1, + "type" : "string", + "typetext" : "[[host=]] [,device-id=] [,legacy-igd=<1|0>] [,mapping=] [,mdev=] [,pcie=<1|0>] [,rombar=<1|0>] [,romfile=] [,sub-device-id=] [,sub-vendor-id=] [,vendor-id=] [,x-vga=<1|0>]", + "verbose_description" : "Map host PCI devices into guest.\n\nNOTE: This option allows direct access to host hardware. So it is no longer\npossible to migrate such machines - use with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "hotplug" : { + "default" : "network,disk,usb", + "description" : "Selectively enable hotplug features. This is a comma separated list of hotplug features: 'network', 'disk', 'cpu', 'memory', 'usb' and 'cloudinit'. Use '0' to disable hotplug completely. Using '1' as value is an alias for the default `network,disk,usb`. USB hotplugging is possible for guests with machine version >= 7.1 and ostype l26 or windows > 7.", + "format" : "pve-hotplug-features", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "hugepages" : { + "description" : "Enable/disable hugepages memory.", + "enum" : [ + "any", + "2", + "1024" + ], + "optional" : 1, + "type" : "string" + }, + "ide[n]" : { + "description" : "Use volume as IDE hard disk or CD-ROM (n is 0 to 3). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "model" : { + "description" : "The drive's reported model name, url-encoded, up to 40 bytes long.", + "format" : "urlencoded", + "format_description" : "model", + "maxLength" : 120, + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,model=] [,replicate=<1|0>] [,rerror=] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,ssd=<1|0>] [,trans=] [,werror=] [,wwn=]" + }, + "ipconfig[n]" : { + "description" : "cloud-init: Specify IP addresses and gateways for the corresponding interface.\n\nIP addresses use CIDR notation, gateways are optional but need an IP of the same type specified.\n\nThe special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit\ngateway should be provided.\nFor IPv6 the special string 'auto' can be used to use stateless autoconfiguration. This requires\ncloud-init 19.4 or newer.\n\nIf cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using\ndhcp on IPv4.\n", + "format" : "pve-qm-ipconfig", + "optional" : 1, + "type" : "string", + "typetext" : "[gw=] [,gw6=] [,ip=] [,ip6=]" + }, + "ivshmem" : { + "description" : "Inter-VM shared memory. Useful for direct communication between VMs, or to the host.", + "format" : { + "name" : { + "description" : "The name of the file. Will be prefixed with 'pve-shm-'. Default is the VMID. Will be deleted when the VM is stopped.", + "format_description" : "string", + "optional" : 1, + "pattern" : "[a-zA-Z0-9\\-]+", + "type" : "string" + }, + "size" : { + "description" : "The size of the file in MB.", + "minimum" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "size= [,name=]" + }, + "keephugepages" : { + "default" : 0, + "description" : "Use together with hugepages. If enabled, hugepages will not not be deleted after VM shutdown and can be used for subsequent starts.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "keyboard" : { + "default" : null, + "description" : "Keyboard layout for VNC server. This option is generally not required and is often better handled from within the guest OS.", + "enum" : [ + "de", + "de-ch", + "da", + "en-gb", + "en-us", + "es", + "fi", + "fr", + "fr-be", + "fr-ca", + "fr-ch", + "hu", + "is", + "it", + "ja", + "lt", + "mk", + "nl", + "no", + "pl", + "pt", + "pt-br", + "sv", + "sl", + "tr" + ], + "optional" : 1, + "type" : "string" + }, + "kvm" : { + "default" : 1, + "description" : "Enable/disable KVM hardware virtualization.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "localtime" : { + "description" : "Set the real time clock (RTC) to local time. This is enabled by default if the `ostype` indicates a Microsoft Windows OS.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "lock" : { + "description" : "Lock/unlock the VM.", + "enum" : [ + "backup", + "clone", + "create", + "migrate", + "rollback", + "snapshot", + "snapshot-delete", + "suspending", + "suspended" + ], + "optional" : 1, + "type" : "string" + }, + "machine" : { + "description" : "Specifies the QEMU machine type.", + "maxLength" : 40, + "optional" : 1, + "pattern" : "(pc|pc(-i440fx)?-\\d+(\\.\\d+)+(\\+pve\\d+)?(\\.pxe)?|q35|pc-q35-\\d+(\\.\\d+)+(\\+pve\\d+)?(\\.pxe)?|virt(?:-\\d+(\\.\\d+)+)?(\\+pve\\d+)?)", + "type" : "string" + }, + "memory" : { + "description" : "Memory properties.", + "format" : { + "current" : { + "default" : 512, + "default_key" : 1, + "description" : "Current amount of online RAM for the VM in MiB. This is the maximum available memory when you use the balloon device.", + "minimum" : 16, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[current=]" + }, + "migrate_downtime" : { + "default" : 0.1, + "description" : "Set maximum tolerated downtime (in seconds) for migrations.", + "minimum" : 0, + "optional" : 1, + "type" : "number", + "typetext" : " (0 - N)" + }, + "migrate_speed" : { + "default" : 0, + "description" : "Set maximum speed (in MB/s) for migrations. Value 0 is no limit.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "name" : { + "description" : "Set a name for the VM. Only used on the configuration web interface.", + "format" : "dns-name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "nameserver" : { + "description" : "cloud-init: Sets DNS server IP address for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.", + "format" : "address-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "net[n]" : { + "description" : "Specify network devices.", + "format" : { + "bridge" : { + "description" : "Bridge to attach the network device to. The Proxmox VE standard bridge\nis called 'vmbr0'.\n\nIf you do not specify a bridge, we create a kvm user (NATed) network\ndevice, which provides DHCP and DNS services. The following addresses\nare used:\n\n 10.0.2.2 Gateway\n 10.0.2.3 DNS Server\n 10.0.2.4 SMB Server\n\nThe DHCP server assign addresses to the guest starting from 10.0.2.15.\n", + "format" : "pve-bridge-id", + "format_description" : "bridge", + "optional" : 1, + "type" : "string" + }, + "e1000" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82540em" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82544gc" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82545em" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000e" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "firewall" : { + "description" : "Whether this interface should be protected by the firewall.", + "optional" : 1, + "type" : "boolean" + }, + "i82551" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "i82557b" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "i82559er" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "link_down" : { + "description" : "Whether this interface should be disconnected (like pulling the plug).", + "optional" : 1, + "type" : "boolean" + }, + "macaddr" : { + "description" : "MAC address. That address must be unique withing your network. This is automatically generated if not specified.", + "format" : "mac-addr", + "format_description" : "XX:XX:XX:XX:XX:XX", + "optional" : 1, + "type" : "string", + "verbose_description" : "A common MAC address with the I/G (Individual/Group) bit not set." + }, + "model" : { + "default_key" : 1, + "description" : "Network Card Model. The 'virtio' model provides the best performance with very low CPU overhead. If your guest does not support this driver, it is usually best to use 'e1000'.", + "enum" : [ + "e1000", + "e1000-82540em", + "e1000-82544gc", + "e1000-82545em", + "e1000e", + "i82551", + "i82557b", + "i82559er", + "ne2k_isa", + "ne2k_pci", + "pcnet", + "rtl8139", + "virtio", + "vmxnet3" + ], + "type" : "string" + }, + "mtu" : { + "description" : "Force MTU, for VirtIO only. Set to '1' to use the bridge MTU", + "maximum" : 65520, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "ne2k_isa" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "ne2k_pci" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "pcnet" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "queues" : { + "description" : "Number of packet queues to be used on the device.", + "maximum" : 64, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "rate" : { + "description" : "Rate limit in mbps (megabytes per second) as floating point number.", + "minimum" : 0, + "optional" : 1, + "type" : "number" + }, + "rtl8139" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "tag" : { + "description" : "VLAN tag to apply to packets on this interface.", + "maximum" : 4094, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "trunks" : { + "description" : "VLAN trunks to pass through this interface.", + "format_description" : "vlanid[;vlanid...]", + "optional" : 1, + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "virtio" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "vmxnet3" : { + "alias" : "macaddr", + "keyAlias" : "model" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[model=] [,bridge=] [,firewall=<1|0>] [,link_down=<1|0>] [,macaddr=] [,mtu=] [,queues=] [,rate=] [,tag=] [,trunks=] [,=]" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "numa" : { + "default" : 0, + "description" : "Enable/disable NUMA.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "numa[n]" : { + "description" : "NUMA topology.", + "format" : { + "cpus" : { + "description" : "CPUs accessing this NUMA node.", + "format_description" : "id[-id];...", + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "hostnodes" : { + "description" : "Host NUMA nodes to use.", + "format_description" : "id[-id];...", + "optional" : 1, + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "memory" : { + "description" : "Amount of memory this NUMA node provides.", + "optional" : 1, + "type" : "number" + }, + "policy" : { + "description" : "NUMA allocation policy.", + "enum" : [ + "preferred", + "bind", + "interleave" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "cpus= [,hostnodes=] [,memory=] [,policy=]" + }, + "onboot" : { + "default" : 0, + "description" : "Specifies whether a VM will be started during system bootup.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ostype" : { + "description" : "Specify guest operating system.", + "enum" : [ + "other", + "wxp", + "w2k", + "w2k3", + "w2k8", + "wvista", + "win7", + "win8", + "win10", + "win11", + "l24", + "l26", + "solaris" + ], + "optional" : 1, + "type" : "string", + "verbose_description" : "Specify guest operating system. This is used to enable special\noptimization/features for specific operating systems:\n\n[horizontal]\nother;; unspecified OS\nwxp;; Microsoft Windows XP\nw2k;; Microsoft Windows 2000\nw2k3;; Microsoft Windows 2003\nw2k8;; Microsoft Windows 2008\nwvista;; Microsoft Windows Vista\nwin7;; Microsoft Windows 7\nwin8;; Microsoft Windows 8/2012/2012r2\nwin10;; Microsoft Windows 10/2016/2019\nwin11;; Microsoft Windows 11/2022\nl24;; Linux 2.4 Kernel\nl26;; Linux 2.6 - 6.X Kernel\nsolaris;; Solaris/OpenSolaris/OpenIndiania kernel\n" + }, + "parallel[n]" : { + "description" : "Map host parallel devices (n is 0 to 2).", + "optional" : 1, + "pattern" : "/dev/parport\\d+|/dev/usb/lp\\d+", + "type" : "string", + "verbose_description" : "Map host parallel devices (n is 0 to 2).\n\nNOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such\nmachines - use with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "protection" : { + "default" : 0, + "description" : "Sets the protection flag of the VM. This will disable the remove VM and remove disk operations.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "reboot" : { + "default" : 1, + "description" : "Allow reboot. If set to '0' the VM exit on reboot.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "revert" : { + "description" : "Revert a pending change.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "rng0" : { + "description" : "Configure a VirtIO-based Random Number Generator.", + "format" : { + "max_bytes" : { + "default" : 1024, + "description" : "Maximum bytes of entropy allowed to get injected into the guest every 'period' milliseconds. Prefer a lower value when using '/dev/random' as source. Use `0` to disable limiting (potentially dangerous!).", + "optional" : 1, + "type" : "integer" + }, + "period" : { + "default" : 1000, + "description" : "Every 'period' milliseconds the entropy-injection quota is reset, allowing the guest to retrieve another 'max_bytes' of entropy.", + "optional" : 1, + "type" : "integer" + }, + "source" : { + "default_key" : 1, + "description" : "The file on the host to gather entropy from. In most cases '/dev/urandom' should be preferred over '/dev/random' to avoid entropy-starvation issues on the host. Using urandom does *not* decrease security in any meaningful way, as it's still seeded from real entropy, and the bytes provided will most likely be mixed with real entropy on the guest as well. '/dev/hwrng' can be used to pass through a hardware RNG from the host.", + "enum" : [ + "/dev/urandom", + "/dev/random", + "/dev/hwrng" + ], + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[source=] [,max_bytes=] [,period=]" + }, + "sata[n]" : { + "description" : "Use volume as SATA hard disk or CD-ROM (n is 0 to 5). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,replicate=<1|0>] [,rerror=] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,ssd=<1|0>] [,trans=] [,werror=] [,wwn=]" + }, + "scsi[n]" : { + "description" : "Use volume as SCSI hard disk or CD-ROM (n is 0 to 30). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iothread" : { + "description" : "Whether to use iothreads for this drive", + "optional" : 1, + "type" : "boolean" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "queues" : { + "description" : "Number of queues.", + "minimum" : 2, + "optional" : 1, + "type" : "integer" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "ro" : { + "description" : "Whether the drive is read-only.", + "optional" : 1, + "type" : "boolean" + }, + "scsiblock" : { + "default" : 0, + "description" : "whether to use scsi-block for full passthrough of host block device\n\nWARNING: can lead to I/O errors in combination with low memory or high memory fragmentation on host", + "optional" : 1, + "type" : "boolean" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,iothread=<1|0>] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,queues=] [,replicate=<1|0>] [,rerror=] [,ro=<1|0>] [,scsiblock=<1|0>] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,ssd=<1|0>] [,trans=] [,werror=] [,wwn=]" + }, + "scsihw" : { + "default" : "lsi", + "description" : "SCSI controller model", + "enum" : [ + "lsi", + "lsi53c810", + "virtio-scsi-pci", + "virtio-scsi-single", + "megasas", + "pvscsi" + ], + "optional" : 1, + "type" : "string" + }, + "searchdomain" : { + "description" : "cloud-init: Sets DNS search domains for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "serial[n]" : { + "description" : "Create a serial device inside the VM (n is 0 to 3)", + "optional" : 1, + "pattern" : "(/dev/.+|socket)", + "type" : "string", + "verbose_description" : "Create a serial device inside the VM (n is 0 to 3), and pass through a\nhost serial device (i.e. /dev/ttyS0), or create a unix socket on the\nhost side (use 'qm terminal' to open a terminal connection).\n\nNOTE: If you pass through a host serial device, it is no longer possible to migrate such machines -\nuse with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "shares" : { + "default" : 1000, + "description" : "Amount of memory shares for auto-ballooning. The larger the number is, the more memory this VM gets. Number is relative to weights of all other running VMs. Using zero disables auto-ballooning. Auto-ballooning is done by pvestatd.", + "maximum" : 50000, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 50000)" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "smbios1" : { + "description" : "Specify SMBIOS type 1 fields.", + "format" : "pve-qm-smbios1", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "[base64=<1|0>] [,family=] [,manufacturer=] [,product=] [,serial=] [,sku=] [,uuid=] [,version=]" + }, + "smp" : { + "default" : 1, + "description" : "The number of CPUs. Please use option -sockets instead.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "sockets" : { + "default" : 1, + "description" : "The number of CPU sockets.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "spice_enhancements" : { + "description" : "Configure additional enhancements for SPICE.", + "format" : { + "foldersharing" : { + "default" : "0", + "description" : "Enable folder sharing via SPICE. Needs Spice-WebDAV daemon installed in the VM.", + "optional" : 1, + "type" : "boolean" + }, + "videostreaming" : { + "default" : "off", + "description" : "Enable video streaming. Uses compression for detected video streams.", + "enum" : [ + "off", + "all", + "filter" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[foldersharing=<1|0>] [,videostreaming=]" + }, + "sshkeys" : { + "description" : "cloud-init: Setup public SSH keys (one key per line, OpenSSH format).", + "format" : "urlencoded", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "startdate" : { + "default" : "now", + "description" : "Set the initial date of the real time clock. Valid format for date are:'now' or '2006-06-17T16:01:21' or '2006-06-17'.", + "optional" : 1, + "pattern" : "(now|\\d{4}-\\d{1,2}-\\d{1,2}(T\\d{1,2}:\\d{1,2}:\\d{1,2})?)", + "type" : "string", + "typetext" : "(now | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS)" + }, + "startup" : { + "description" : "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped.", + "format" : "pve-startup-order", + "optional" : 1, + "type" : "string", + "typetext" : "[[order=]\\d+] [,up=\\d+] [,down=\\d+] " + }, + "tablet" : { + "default" : 1, + "description" : "Enable/disable the USB tablet device.", + "optional" : 1, + "type" : "boolean", + "typetext" : "", + "verbose_description" : "Enable/disable the USB tablet device. This device is usually needed to allow absolute mouse positioning with VNC. Else the mouse runs out of sync with normal VNC clients. If you're running lots of console-only guests on one host, you may consider disabling this to save some context switches. This is turned off by default if you use spice (`qm set --vga qxl`)." + }, + "tags" : { + "description" : "Tags of the VM. This is only meta information.", + "format" : "pve-tag-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tdf" : { + "default" : 0, + "description" : "Enable/disable time drift fix.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "template" : { + "default" : 0, + "description" : "Enable/disable Template.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "tpmstate0" : { + "description" : "Configure a Disk for storing TPM state. The format is fixed to 'raw'. Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Note that SIZE_IN_GiB is ignored here and 4 MiB will be used instead. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "version" : { + "default" : "v2.0", + "description" : "The TPM interface version. v2.0 is newer and should be preferred. Note that this cannot be changed later on.", + "enum" : [ + "v1.2", + "v2.0" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,import-from=] [,size=] [,version=]" + }, + "unused[n]" : { + "description" : "Reference to unused volumes. This is used internally, and should not be modified manually.", + "format" : { + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id", + "format_description" : "volume", + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=]" + }, + "usb[n]" : { + "description" : "Configure an USB device (n is 0 to 4, for machine version >= 7.1 and ostype l26 or windows > 7, n can be up to 14).", + "format" : { + "host" : { + "default_key" : 1, + "description" : "The Host USB device or port or the value 'spice'. HOSTUSBDEVICE syntax is:\n\n 'bus-port(.port)*' (decimal numbers) or\n 'vendor_id:product_id' (hexadeciaml numbers) or\n 'spice'\n\nYou can use the 'lsusb -t' command to list existing usb devices.\n\nNOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such\nmachines - use with special care.\n\nThe value 'spice' can be used to add a usb redirection devices for spice.\n\nEither this or the 'mapping' key must be set.\n", + "format_description" : "HOSTUSBDEVICE|spice", + "optional" : 1, + "pattern" : "(?^:(?:(?:(?^:(0x)?([0-9A-Fa-f]{4}):(0x)?([0-9A-Fa-f]{4})))|(?:(?^:(\\d+)\\-(\\d+(\\.\\d+)*)))|[Ss][Pp][Ii][Cc][Ee]))", + "type" : "string" + }, + "mapping" : { + "description" : "The ID of a cluster wide mapping. Either this or the default-key 'host' must be set.", + "format" : "pve-configid", + "format_description" : "mapping-id", + "optional" : 1, + "type" : "string" + }, + "usb3" : { + "default" : 0, + "description" : "Specifies whether if given host option is a USB3 device or port. For modern guests (machine version >= 7.1 and ostype l26 and windows > 7), this flag is irrelevant (all devices are plugged into a xhci controller).", + "optional" : 1, + "type" : "boolean" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[[host=]] [,mapping=] [,usb3=<1|0>]" + }, + "vcpus" : { + "default" : 0, + "description" : "Number of hotplugged vcpus.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "vga" : { + "description" : "Configure the VGA hardware.", + "format" : { + "clipboard" : { + "description" : "Enable a specific clipboard. If not set, depending on the display type the SPICE one will be added.", + "enum" : [ + "vnc" + ], + "optional" : 1, + "type" : "string" + }, + "memory" : { + "description" : "Sets the VGA memory (in MiB). Has no effect with serial display.", + "maximum" : 512, + "minimum" : 4, + "optional" : 1, + "type" : "integer" + }, + "type" : { + "default" : "std", + "default_key" : 1, + "description" : "Select the VGA type.", + "enum" : [ + "cirrus", + "qxl", + "qxl2", + "qxl3", + "qxl4", + "none", + "serial0", + "serial1", + "serial2", + "serial3", + "std", + "virtio", + "virtio-gl", + "vmware" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[[type=]] [,clipboard=] [,memory=]", + "verbose_description" : "Configure the VGA Hardware. If you want to use high resolution modes (>= 1280x1024x16) you may need to increase the vga memory option. Since QEMU 2.9 the default VGA display type is 'std' for all OS types besides some Windows versions (XP and older) which use 'cirrus'. The 'qxl' option enables the SPICE display server. For win* OS you can select how many independent displays you want, Linux guests can add displays them self.\nYou can also run without any graphic card, using a serial device as terminal." + }, + "virtio[n]" : { + "description" : "Use volume as VIRTIO hard disk (n is 0 to 15). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iothread" : { + "description" : "Whether to use iothreads for this drive", + "optional" : 1, + "type" : "boolean" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "ro" : { + "description" : "Whether the drive is read-only.", + "optional" : 1, + "type" : "boolean" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,iothread=<1|0>] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,replicate=<1|0>] [,rerror=] [,ro=<1|0>] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,trans=] [,werror=]" + }, + "vmgenid" : { + "default" : "1 (autogenerated)", + "description" : "Set VM Generation ID. Use '1' to autogenerate on create or update, pass '0' to disable explicitly.", + "format_description" : "UUID", + "optional" : 1, + "pattern" : "(?:[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}|[01])", + "type" : "string", + "verbose_description" : "The VM generation ID (vmgenid) device exposes a 128-bit integer value identifier to the guest OS. This allows to notify the guest operating system when the virtual machine is executed with a different configuration (e.g. snapshot execution or creation from a template). The guest operating system notices the change, and is then able to react as appropriate by marking its copies of distributed databases as dirty, re-initializing its random number generator, etc.\nNote that auto-creation only works when done through API/CLI create or update methods, but not when manually editing the config file." + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "vmstatestorage" : { + "description" : "Default storage for VM state volumes/files.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "watchdog" : { + "description" : "Create a virtual hardware watchdog device.", + "format" : "pve-qm-watchdog", + "optional" : 1, + "type" : "string", + "typetext" : "[[model=]] [,action=]", + "verbose_description" : "Create a virtual hardware watchdog device. Once enabled (by a guest action), the watchdog must be periodically polled by an agent inside the guest or else the watchdog will reset the guest (or execute the respective action specified)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Disk", + "VM.Config.CDROM", + "VM.Config.CPU", + "VM.Config.Memory", + "VM.Config.Network", + "VM.Config.HWType", + "VM.Config.Options", + "VM.Config.Cloudinit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/config", + "text" : "config" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get the virtual machine configuration with both current and pending values.", + "method" : "GET", + "name" : "vm_pending", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "delete" : { + "description" : "Indicates a pending delete request if present and not 0. The value 2 indicates a force-delete request.", + "maximum" : 2, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "key" : { + "description" : "Configuration option name.", + "type" : "string" + }, + "pending" : { + "description" : "Pending value.", + "optional" : 1, + "type" : "string" + }, + "value" : { + "description" : "Current value.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/pending", + "text" : "pending" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get automatically generated cloudinit config.", + "method" : "GET", + "name" : "cloudinit_generated_config_dump", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Config type.", + "enum" : [ + "user", + "network", + "meta" + ], + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/cloudinit/dump", + "text" : "dump" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get the cloudinit configuration with both current and pending values.", + "method" : "GET", + "name" : "cloudinit_pending", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "delete" : { + "description" : "Indicates a pending delete request if present and not 0. ", + "maximum" : 1, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "key" : { + "description" : "Configuration option name.", + "type" : "string" + }, + "pending" : { + "description" : "The new pending value.", + "optional" : 1, + "type" : "string" + }, + "value" : { + "description" : "Value as it was used to generate the current cloudinit image.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Regenerate and change cloudinit config drive.", + "method" : "PUT", + "name" : "cloudinit_update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Cloudinit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu/{vmid}/cloudinit", + "text" : "cloudinit" + }, + { + "info" : { + "PUT" : { + "allowtoken" : 1, + "description" : "Unlink/delete disk images.", + "method" : "PUT", + "name" : "unlink", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "description" : "Force physical removal. Without this, we simple remove the disk from the config file and create an additional configuration entry called 'unused[n]', which contains the volume ID. Unlink of unused[n] always cause physical removal.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "idlist" : { + "description" : "A list of disk IDs you want to delete.", + "format" : "pve-configid-list", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Disk" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/unlink", + "text" : "unlink" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Creates a TCP VNC proxy connections.", + "method" : "POST", + "name" : "vncproxy", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "generate-password" : { + "default" : 0, + "description" : "Generates a random password to be used as ticket instead of the API ticket.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "websocket" : { + "description" : "Prepare for websocket upgrade (only required when using serial terminal, otherwise upgrade is always possible).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "cert" : { + "type" : "string" + }, + "password" : { + "description" : "Returned if requested with 'generate-password' param. Consists of printable ASCII characters ('!' .. '~').", + "optional" : 1, + "type" : "string" + }, + "port" : { + "type" : "integer" + }, + "ticket" : { + "type" : "string" + }, + "upid" : { + "type" : "string" + }, + "user" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/vncproxy", + "text" : "vncproxy" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Creates a TCP proxy connections.", + "method" : "POST", + "name" : "termproxy", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "serial" : { + "description" : "opens a serial terminal (defaults to display)", + "enum" : [ + "serial0", + "serial1", + "serial2", + "serial3" + ], + "optional" : 1, + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "port" : { + "type" : "integer" + }, + "ticket" : { + "type" : "string" + }, + "upid" : { + "type" : "string" + }, + "user" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/termproxy", + "text" : "termproxy" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Opens a weksocket for VNC traffic.", + "method" : "GET", + "name" : "vncwebsocket", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "port" : { + "description" : "Port number returned by previous vncproxy call.", + "maximum" : 5999, + "minimum" : 5900, + "type" : "integer", + "typetext" : " (5900 - 5999)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "vncticket" : { + "description" : "Ticket from previous call to vncproxy.", + "maxLength" : 512, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Console" + ] + ], + "description" : "You also need to pass a valid ticket (vncticket)." + }, + "returns" : { + "properties" : { + "port" : { + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/vncwebsocket", + "text" : "vncwebsocket" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Returns a SPICE configuration to connect to the VM.", + "method" : "POST", + "name" : "spiceproxy", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "proxy" : { + "description" : "SPICE proxy server. This can be used by the client to specify the proxy server. All nodes in a cluster runs 'spiceproxy', so it is up to the client to choose one. By default, we return the node where the VM is currently running. As reasonable setting is to use same node you use to connect to the API (This is window.location.hostname for the JS GUI).", + "format" : "address", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Console" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "additionalProperties" : 1, + "description" : "Returned values can be directly passed to the 'remote-viewer' application.", + "properties" : { + "host" : { + "type" : "string" + }, + "password" : { + "type" : "string" + }, + "proxy" : { + "type" : "string" + }, + "tls-port" : { + "type" : "integer" + }, + "type" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/spiceproxy", + "text" : "spiceproxy" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get virtual machine status.", + "method" : "GET", + "name" : "vm_status", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "agent" : { + "description" : "QEMU Guest Agent is enabled in config.", + "optional" : 1, + "type" : "boolean" + }, + "clipboard" : { + "description" : "Enable a specific clipboard. If not set, depending on the display type the SPICE one will be added.", + "enum" : [ + "vnc" + ], + "optional" : 1, + "type" : "string" + }, + "cpus" : { + "description" : "Maximum usable CPUs.", + "optional" : 1, + "type" : "number" + }, + "ha" : { + "description" : "HA manager service status.", + "type" : "object" + }, + "lock" : { + "description" : "The current config lock, if any.", + "optional" : 1, + "type" : "string" + }, + "maxdisk" : { + "description" : "Root disk size in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "maxmem" : { + "description" : "Maximum memory in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "name" : { + "description" : "VM name.", + "optional" : 1, + "type" : "string" + }, + "pid" : { + "description" : "PID of running qemu process.", + "optional" : 1, + "type" : "integer" + }, + "qmpstatus" : { + "description" : "VM run state from the 'query-status' QMP monitor command.", + "optional" : 1, + "type" : "string" + }, + "running-machine" : { + "description" : "The currently running machine type (if running).", + "optional" : 1, + "type" : "string" + }, + "running-qemu" : { + "description" : "The currently running QEMU version (if running).", + "optional" : 1, + "type" : "string" + }, + "spice" : { + "description" : "QEMU VGA configuration supports spice.", + "optional" : 1, + "type" : "boolean" + }, + "status" : { + "description" : "QEMU process status.", + "enum" : [ + "stopped", + "running" + ], + "type" : "string" + }, + "tags" : { + "description" : "The current configured tags, if any", + "optional" : 1, + "type" : "string" + }, + "uptime" : { + "description" : "Uptime.", + "optional" : 1, + "renderer" : "duration", + "type" : "integer" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/status/current", + "text" : "current" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Start virtual machine.", + "method" : "POST", + "name" : "vm_start", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force-cpu" : { + "description" : "Override QEMU's -cpu argument with the given string.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "machine" : { + "description" : "Specifies the QEMU machine type.", + "maxLength" : 40, + "optional" : 1, + "pattern" : "(pc|pc(-i440fx)?-\\d+(\\.\\d+)+(\\+pve\\d+)?(\\.pxe)?|q35|pc-q35-\\d+(\\.\\d+)+(\\+pve\\d+)?(\\.pxe)?|virt(?:-\\d+(\\.\\d+)+)?(\\+pve\\d+)?)", + "type" : "string" + }, + "migratedfrom" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "migration_network" : { + "description" : "CIDR of the (sub) network that is used for migration.", + "format" : "CIDR", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "migration_type" : { + "description" : "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.", + "enum" : [ + "secure", + "insecure" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "stateuri" : { + "description" : "Some command save/restore state from this location.", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "targetstorage" : { + "description" : "Mapping from source to target storages. Providing only a single storage ID maps all source storages to that storage. Providing the special value '1' will map each source storage to itself.", + "format" : "storage-pair-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "timeout" : { + "default" : "max(30, vm memory in GiB)", + "description" : "Wait maximal timeout seconds.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/status/start", + "text" : "start" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Stop virtual machine. The qemu process will exit immediately. Thisis akin to pulling the power plug of a running computer and may damage the VM data", + "method" : "POST", + "name" : "vm_stop", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "keepActive" : { + "default" : 0, + "description" : "Do not deactivate storage volumes.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "migratedfrom" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "timeout" : { + "description" : "Wait maximal timeout seconds.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/status/stop", + "text" : "stop" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Reset virtual machine.", + "method" : "POST", + "name" : "vm_reset", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/status/reset", + "text" : "reset" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Shutdown virtual machine. This is similar to pressing the power button on a physical machine.This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.", + "method" : "POST", + "name" : "vm_shutdown", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "forceStop" : { + "default" : 0, + "description" : "Make sure the VM stops.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "keepActive" : { + "default" : 0, + "description" : "Do not deactivate storage volumes.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "timeout" : { + "description" : "Wait maximal timeout seconds.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/status/shutdown", + "text" : "shutdown" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Reboot the VM by shutting it down, and starting it again. Applies pending changes.", + "method" : "POST", + "name" : "vm_reboot", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "timeout" : { + "description" : "Wait maximal timeout seconds for the shutdown.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/status/reboot", + "text" : "reboot" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Suspend virtual machine.", + "method" : "POST", + "name" : "vm_suspend", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "statestorage" : { + "description" : "The storage for the VM state", + "format" : "pve-storage-id", + "optional" : 1, + "requires" : "todisk", + "type" : "string", + "typetext" : "" + }, + "todisk" : { + "default" : 0, + "description" : "If set, suspends the VM to disk. Will be resumed on next VM start.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ], + "description" : "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk', you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace' on the storage for the vmstate." + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/status/suspend", + "text" : "suspend" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Resume virtual machine.", + "method" : "POST", + "name" : "vm_resume", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "nocheck" : { + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/status/resume", + "text" : "resume" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index", + "method" : "GET", + "name" : "vmcmdidx", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "subdir" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{subdir}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu/{vmid}/status", + "text" : "status" + }, + { + "info" : { + "PUT" : { + "allowtoken" : 1, + "description" : "Send key event to virtual machine.", + "method" : "PUT", + "name" : "vm_sendkey", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "key" : { + "description" : "The key (qemu monitor encoding).", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Console" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/sendkey", + "text" : "sendkey" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Check if feature for virtual machine is available.", + "method" : "GET", + "name" : "vm_feature", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "feature" : { + "description" : "Feature to check.", + "enum" : [ + "snapshot", + "clone", + "copy" + ], + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "hasFeature" : { + "type" : "boolean" + }, + "nodes" : { + "items" : { + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/feature", + "text" : "feature" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Create a copy of virtual machine/template.", + "method" : "POST", + "name" : "clone_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "bwlimit" : { + "default" : "clone limit from datacenter or storage config", + "description" : "Override I/O bandwidth limit (in KiB/s).", + "minimum" : "0", + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "description" : { + "description" : "Description for the new VM.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "format" : { + "description" : "Target format for file storage. Only valid for full clone.", + "enum" : [ + "raw", + "qcow2", + "vmdk" + ], + "optional" : 1, + "type" : "string" + }, + "full" : { + "description" : "Create a full copy of all disks. This is always done when you clone a normal VM. For VM templates, we try to create a linked clone by default.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "description" : "Set a name for the new VM.", + "format" : "dns-name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "newid" : { + "description" : "VMID for the clone.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pool" : { + "description" : "Add the new VM to the specified pool.", + "format" : "pve-poolid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "Target storage for full clone.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "target" : { + "description" : "Target node. Only allowed if the original VM is on shared storage.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "and", + [ + "perm", + "/vms/{vmid}", + [ + "VM.Clone" + ] + ], + [ + "or", + [ + "perm", + "/vms/{newid}", + [ + "VM.Allocate" + ] + ], + [ + "perm", + "/pool/{pool}", + [ + "VM.Allocate" + ], + "require_param", + "pool" + ] + ] + ], + "description" : "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions on /vms/{newid} (or on the VM pool /pool/{pool}). You also need 'Datastore.AllocateSpace' on any used storage and 'SDN.Use' on any used bridge/vnet" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/clone", + "text" : "clone" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Move volume to different storage or to a different VM.", + "method" : "POST", + "name" : "move_vm_disk", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "bwlimit" : { + "default" : "move limit from datacenter or storage config", + "description" : "Override I/O bandwidth limit (in KiB/s).", + "minimum" : "0", + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "delete" : { + "default" : 0, + "description" : "Delete the original disk after successful copy. By default the original disk is kept as unused disk.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has different SHA1\"\n\t\t .\" digest. This can be used to prevent concurrent modifications.", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disk" : { + "description" : "The disk you want to move.", + "enum" : [ + "ide0", + "ide1", + "ide2", + "ide3", + "scsi0", + "scsi1", + "scsi2", + "scsi3", + "scsi4", + "scsi5", + "scsi6", + "scsi7", + "scsi8", + "scsi9", + "scsi10", + "scsi11", + "scsi12", + "scsi13", + "scsi14", + "scsi15", + "scsi16", + "scsi17", + "scsi18", + "scsi19", + "scsi20", + "scsi21", + "scsi22", + "scsi23", + "scsi24", + "scsi25", + "scsi26", + "scsi27", + "scsi28", + "scsi29", + "scsi30", + "virtio0", + "virtio1", + "virtio2", + "virtio3", + "virtio4", + "virtio5", + "virtio6", + "virtio7", + "virtio8", + "virtio9", + "virtio10", + "virtio11", + "virtio12", + "virtio13", + "virtio14", + "virtio15", + "sata0", + "sata1", + "sata2", + "sata3", + "sata4", + "sata5", + "efidisk0", + "tpmstate0", + "unused0", + "unused1", + "unused2", + "unused3", + "unused4", + "unused5", + "unused6", + "unused7", + "unused8", + "unused9", + "unused10", + "unused11", + "unused12", + "unused13", + "unused14", + "unused15", + "unused16", + "unused17", + "unused18", + "unused19", + "unused20", + "unused21", + "unused22", + "unused23", + "unused24", + "unused25", + "unused26", + "unused27", + "unused28", + "unused29", + "unused30", + "unused31", + "unused32", + "unused33", + "unused34", + "unused35", + "unused36", + "unused37", + "unused38", + "unused39", + "unused40", + "unused41", + "unused42", + "unused43", + "unused44", + "unused45", + "unused46", + "unused47", + "unused48", + "unused49", + "unused50", + "unused51", + "unused52", + "unused53", + "unused54", + "unused55", + "unused56", + "unused57", + "unused58", + "unused59", + "unused60", + "unused61", + "unused62", + "unused63", + "unused64", + "unused65", + "unused66", + "unused67", + "unused68", + "unused69", + "unused70", + "unused71", + "unused72", + "unused73", + "unused74", + "unused75", + "unused76", + "unused77", + "unused78", + "unused79", + "unused80", + "unused81", + "unused82", + "unused83", + "unused84", + "unused85", + "unused86", + "unused87", + "unused88", + "unused89", + "unused90", + "unused91", + "unused92", + "unused93", + "unused94", + "unused95", + "unused96", + "unused97", + "unused98", + "unused99", + "unused100", + "unused101", + "unused102", + "unused103", + "unused104", + "unused105", + "unused106", + "unused107", + "unused108", + "unused109", + "unused110", + "unused111", + "unused112", + "unused113", + "unused114", + "unused115", + "unused116", + "unused117", + "unused118", + "unused119", + "unused120", + "unused121", + "unused122", + "unused123", + "unused124", + "unused125", + "unused126", + "unused127", + "unused128", + "unused129", + "unused130", + "unused131", + "unused132", + "unused133", + "unused134", + "unused135", + "unused136", + "unused137", + "unused138", + "unused139", + "unused140", + "unused141", + "unused142", + "unused143", + "unused144", + "unused145", + "unused146", + "unused147", + "unused148", + "unused149", + "unused150", + "unused151", + "unused152", + "unused153", + "unused154", + "unused155", + "unused156", + "unused157", + "unused158", + "unused159", + "unused160", + "unused161", + "unused162", + "unused163", + "unused164", + "unused165", + "unused166", + "unused167", + "unused168", + "unused169", + "unused170", + "unused171", + "unused172", + "unused173", + "unused174", + "unused175", + "unused176", + "unused177", + "unused178", + "unused179", + "unused180", + "unused181", + "unused182", + "unused183", + "unused184", + "unused185", + "unused186", + "unused187", + "unused188", + "unused189", + "unused190", + "unused191", + "unused192", + "unused193", + "unused194", + "unused195", + "unused196", + "unused197", + "unused198", + "unused199", + "unused200", + "unused201", + "unused202", + "unused203", + "unused204", + "unused205", + "unused206", + "unused207", + "unused208", + "unused209", + "unused210", + "unused211", + "unused212", + "unused213", + "unused214", + "unused215", + "unused216", + "unused217", + "unused218", + "unused219", + "unused220", + "unused221", + "unused222", + "unused223", + "unused224", + "unused225", + "unused226", + "unused227", + "unused228", + "unused229", + "unused230", + "unused231", + "unused232", + "unused233", + "unused234", + "unused235", + "unused236", + "unused237", + "unused238", + "unused239", + "unused240", + "unused241", + "unused242", + "unused243", + "unused244", + "unused245", + "unused246", + "unused247", + "unused248", + "unused249", + "unused250", + "unused251", + "unused252", + "unused253", + "unused254", + "unused255" + ], + "type" : "string" + }, + "format" : { + "description" : "Target Format.", + "enum" : [ + "raw", + "qcow2", + "vmdk" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "Target storage.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "target-digest" : { + "description" : "Prevent changes if the current config file of the target VM has a\"\n\t\t .\" different SHA1 digest. This can be used to detect concurrent modifications.", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "target-disk" : { + "description" : "The config key the disk will be moved to on the target VM (for example, ide0 or scsi1). Default is the source disk key.", + "enum" : [ + "ide0", + "ide1", + "ide2", + "ide3", + "scsi0", + "scsi1", + "scsi2", + "scsi3", + "scsi4", + "scsi5", + "scsi6", + "scsi7", + "scsi8", + "scsi9", + "scsi10", + "scsi11", + "scsi12", + "scsi13", + "scsi14", + "scsi15", + "scsi16", + "scsi17", + "scsi18", + "scsi19", + "scsi20", + "scsi21", + "scsi22", + "scsi23", + "scsi24", + "scsi25", + "scsi26", + "scsi27", + "scsi28", + "scsi29", + "scsi30", + "virtio0", + "virtio1", + "virtio2", + "virtio3", + "virtio4", + "virtio5", + "virtio6", + "virtio7", + "virtio8", + "virtio9", + "virtio10", + "virtio11", + "virtio12", + "virtio13", + "virtio14", + "virtio15", + "sata0", + "sata1", + "sata2", + "sata3", + "sata4", + "sata5", + "efidisk0", + "tpmstate0", + "unused0", + "unused1", + "unused2", + "unused3", + "unused4", + "unused5", + "unused6", + "unused7", + "unused8", + "unused9", + "unused10", + "unused11", + "unused12", + "unused13", + "unused14", + "unused15", + "unused16", + "unused17", + "unused18", + "unused19", + "unused20", + "unused21", + "unused22", + "unused23", + "unused24", + "unused25", + "unused26", + "unused27", + "unused28", + "unused29", + "unused30", + "unused31", + "unused32", + "unused33", + "unused34", + "unused35", + "unused36", + "unused37", + "unused38", + "unused39", + "unused40", + "unused41", + "unused42", + "unused43", + "unused44", + "unused45", + "unused46", + "unused47", + "unused48", + "unused49", + "unused50", + "unused51", + "unused52", + "unused53", + "unused54", + "unused55", + "unused56", + "unused57", + "unused58", + "unused59", + "unused60", + "unused61", + "unused62", + "unused63", + "unused64", + "unused65", + "unused66", + "unused67", + "unused68", + "unused69", + "unused70", + "unused71", + "unused72", + "unused73", + "unused74", + "unused75", + "unused76", + "unused77", + "unused78", + "unused79", + "unused80", + "unused81", + "unused82", + "unused83", + "unused84", + "unused85", + "unused86", + "unused87", + "unused88", + "unused89", + "unused90", + "unused91", + "unused92", + "unused93", + "unused94", + "unused95", + "unused96", + "unused97", + "unused98", + "unused99", + "unused100", + "unused101", + "unused102", + "unused103", + "unused104", + "unused105", + "unused106", + "unused107", + "unused108", + "unused109", + "unused110", + "unused111", + "unused112", + "unused113", + "unused114", + "unused115", + "unused116", + "unused117", + "unused118", + "unused119", + "unused120", + "unused121", + "unused122", + "unused123", + "unused124", + "unused125", + "unused126", + "unused127", + "unused128", + "unused129", + "unused130", + "unused131", + "unused132", + "unused133", + "unused134", + "unused135", + "unused136", + "unused137", + "unused138", + "unused139", + "unused140", + "unused141", + "unused142", + "unused143", + "unused144", + "unused145", + "unused146", + "unused147", + "unused148", + "unused149", + "unused150", + "unused151", + "unused152", + "unused153", + "unused154", + "unused155", + "unused156", + "unused157", + "unused158", + "unused159", + "unused160", + "unused161", + "unused162", + "unused163", + "unused164", + "unused165", + "unused166", + "unused167", + "unused168", + "unused169", + "unused170", + "unused171", + "unused172", + "unused173", + "unused174", + "unused175", + "unused176", + "unused177", + "unused178", + "unused179", + "unused180", + "unused181", + "unused182", + "unused183", + "unused184", + "unused185", + "unused186", + "unused187", + "unused188", + "unused189", + "unused190", + "unused191", + "unused192", + "unused193", + "unused194", + "unused195", + "unused196", + "unused197", + "unused198", + "unused199", + "unused200", + "unused201", + "unused202", + "unused203", + "unused204", + "unused205", + "unused206", + "unused207", + "unused208", + "unused209", + "unused210", + "unused211", + "unused212", + "unused213", + "unused214", + "unused215", + "unused216", + "unused217", + "unused218", + "unused219", + "unused220", + "unused221", + "unused222", + "unused223", + "unused224", + "unused225", + "unused226", + "unused227", + "unused228", + "unused229", + "unused230", + "unused231", + "unused232", + "unused233", + "unused234", + "unused235", + "unused236", + "unused237", + "unused238", + "unused239", + "unused240", + "unused241", + "unused242", + "unused243", + "unused244", + "unused245", + "unused246", + "unused247", + "unused248", + "unused249", + "unused250", + "unused251", + "unused252", + "unused253", + "unused254", + "unused255" + ], + "optional" : 1, + "type" : "string" + }, + "target-vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Disk" + ] + ], + "description" : "You need 'VM.Config.Disk' permissions on /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage. To move a disk to another VM, you need the permissions on the target VM as well." + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/move_disk", + "text" : "move_disk" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get preconditions for migration.", + "method" : "GET", + "name" : "migrate_vm_precondition", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "target" : { + "description" : "Target node.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Migrate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "allowed_nodes" : { + "description" : "List nodes allowed for offline migration, only passed if VM is offline", + "optional" : 1, + "type" : "array" + }, + "local_disks" : { + "description" : "List local disks including CD-Rom, unsused and not referenced disks", + "type" : "array" + }, + "local_resources" : { + "description" : "List local resources e.g. pci, usb", + "type" : "array" + }, + "mapped-resources" : { + "description" : "List of mapped resources e.g. pci, usb", + "type" : "array" + }, + "not_allowed_nodes" : { + "description" : "List not allowed nodes with additional informations, only passed if VM is offline", + "optional" : 1, + "type" : "object" + }, + "running" : { + "type" : "boolean" + } + }, + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Migrate virtual machine. Creates a new migration task.", + "method" : "POST", + "name" : "migrate_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "bwlimit" : { + "default" : "migrate limit from datacenter or storage config", + "description" : "Override I/O bandwidth limit (in KiB/s).", + "minimum" : "0", + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "force" : { + "description" : "Allow to migrate VMs which use local devices. Only root may use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "migration_network" : { + "description" : "CIDR of the (sub) network that is used for migration.", + "format" : "CIDR", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "migration_type" : { + "description" : "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.", + "enum" : [ + "secure", + "insecure" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "online" : { + "description" : "Use online/live migration if VM is running. Ignored if VM is stopped.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "target" : { + "description" : "Target node.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "targetstorage" : { + "description" : "Mapping from source to target storages. Providing only a single storage ID maps all source storages to that storage. Providing the special value '1' will map each source storage to itself.", + "format" : "storage-pair-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "with-local-disks" : { + "description" : "Enable live storage migration for local disk", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Migrate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/migrate", + "text" : "migrate" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!", + "method" : "POST", + "name" : "remote_migrate_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "bwlimit" : { + "default" : "migrate limit from datacenter or storage config", + "description" : "Override I/O bandwidth limit (in KiB/s).", + "minimum" : "0", + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "delete" : { + "default" : 0, + "description" : "Delete the original VM and related data after successful migration. By default the original VM is kept on the source cluster in a stopped state.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "online" : { + "description" : "Use online/live migration if VM is running. Ignored if VM is stopped.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "target-bridge" : { + "description" : "Mapping from source to target bridges. Providing only a single bridge ID maps all source bridges to that bridge. Providing the special value '1' will map each source bridge to itself.", + "format" : "bridge-pair-list", + "type" : "string", + "typetext" : "" + }, + "target-endpoint" : { + "description" : "Remote target endpoint", + "format" : "proxmox-remote", + "type" : "string", + "typetext" : "apitoken= ,host= [,fingerprint=] [,port=]" + }, + "target-storage" : { + "description" : "Mapping from source to target storages. Providing only a single storage ID maps all source storages to that storage. Providing the special value '1' will map each source storage to itself.", + "format" : "storage-pair-list", + "optional" : 0, + "type" : "string", + "typetext" : "" + }, + "target-vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Migrate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/remote_migrate", + "text" : "remote_migrate" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Execute QEMU monitor commands.", + "method" : "POST", + "name" : "monitor", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "command" : { + "description" : "The monitor command.", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Monitor" + ] + ], + "description" : "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/monitor", + "text" : "monitor" + }, + { + "info" : { + "PUT" : { + "allowtoken" : 1, + "description" : "Extend volume size.", + "method" : "PUT", + "name" : "resize_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disk" : { + "description" : "The disk you want to resize.", + "enum" : [ + "ide0", + "ide1", + "ide2", + "ide3", + "scsi0", + "scsi1", + "scsi2", + "scsi3", + "scsi4", + "scsi5", + "scsi6", + "scsi7", + "scsi8", + "scsi9", + "scsi10", + "scsi11", + "scsi12", + "scsi13", + "scsi14", + "scsi15", + "scsi16", + "scsi17", + "scsi18", + "scsi19", + "scsi20", + "scsi21", + "scsi22", + "scsi23", + "scsi24", + "scsi25", + "scsi26", + "scsi27", + "scsi28", + "scsi29", + "scsi30", + "virtio0", + "virtio1", + "virtio2", + "virtio3", + "virtio4", + "virtio5", + "virtio6", + "virtio7", + "virtio8", + "virtio9", + "virtio10", + "virtio11", + "virtio12", + "virtio13", + "virtio14", + "virtio15", + "sata0", + "sata1", + "sata2", + "sata3", + "sata4", + "sata5", + "efidisk0", + "tpmstate0" + ], + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "size" : { + "description" : "The new size. With the `+` sign the value is added to the actual size of the volume and without it, the value is taken as an absolute one. Shrinking disk size is not supported.", + "pattern" : "\\+?\\d+(\\.\\d+)?[KMGT]?", + "type" : "string" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Disk" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/resize", + "text" : "resize" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get snapshot configuration", + "method" : "GET", + "name" : "get_snapshot_config", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Snapshot", + "VM.Snapshot.Rollback", + "VM.Audit" + ], + "any", + 1 + ] + }, + "proxyto" : "node", + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update snapshot metadata.", + "method" : "PUT", + "name" : "update_snapshot_config", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "description" : { + "description" : "A textual description or comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Snapshot" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/snapshot/{snapname}/config", + "text" : "config" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Rollback VM state to specified snapshot.", + "method" : "POST", + "name" : "rollback", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "start" : { + "default" : 0, + "description" : "Whether the VM should get started after rolling back successfully. (Note: VMs will be automatically started if the snapshot includes RAM.)", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Snapshot", + "VM.Snapshot.Rollback" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/snapshot/{snapname}/rollback", + "text" : "rollback" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete a VM snapshot.", + "method" : "DELETE", + "name" : "delsnapshot", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "description" : "For removal from config file, even if removing disk snapshots fails.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Snapshot" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "", + "method" : "GET", + "name" : "snapshot_cmd_idx", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{cmd}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu/{vmid}/snapshot/{snapname}", + "text" : "{snapname}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List all snapshots.", + "method" : "GET", + "name" : "snapshot_list", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "description" : { + "description" : "Snapshot description.", + "type" : "string" + }, + "name" : { + "description" : "Snapshot identifier. Value 'current' identifies the current VM.", + "type" : "string" + }, + "parent" : { + "description" : "Parent snapshot identifier.", + "optional" : 1, + "type" : "string" + }, + "snaptime" : { + "description" : "Snapshot creation time", + "optional" : 1, + "renderer" : "timestamp", + "type" : "integer" + }, + "vmstate" : { + "description" : "Snapshot includes RAM.", + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Snapshot a VM.", + "method" : "POST", + "name" : "snapshot", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "description" : { + "description" : "A textual description or comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "vmstate" : { + "description" : "Save the vmstate", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Snapshot" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu/{vmid}/snapshot", + "text" : "snapshot" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Create a Template.", + "method" : "POST", + "name" : "template", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "disk" : { + "description" : "If you want to convert only 1 disk to base image.", + "enum" : [ + "ide0", + "ide1", + "ide2", + "ide3", + "scsi0", + "scsi1", + "scsi2", + "scsi3", + "scsi4", + "scsi5", + "scsi6", + "scsi7", + "scsi8", + "scsi9", + "scsi10", + "scsi11", + "scsi12", + "scsi13", + "scsi14", + "scsi15", + "scsi16", + "scsi17", + "scsi18", + "scsi19", + "scsi20", + "scsi21", + "scsi22", + "scsi23", + "scsi24", + "scsi25", + "scsi26", + "scsi27", + "scsi28", + "scsi29", + "scsi30", + "virtio0", + "virtio1", + "virtio2", + "virtio3", + "virtio4", + "virtio5", + "virtio6", + "virtio7", + "virtio8", + "virtio9", + "virtio10", + "virtio11", + "virtio12", + "virtio13", + "virtio14", + "virtio15", + "sata0", + "sata1", + "sata2", + "sata3", + "sata4", + "sata5", + "efidisk0", + "tpmstate0" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Allocate" + ] + ], + "description" : "You need 'VM.Allocate' permissions on /vms/{vmid}" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/template", + "text" : "template" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Migration tunnel endpoint - only for internal use by VM migration.", + "method" : "POST", + "name" : "mtunnel", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "bridges" : { + "description" : "List of network bridges to check availability. Will be checked again for actually used bridges during migration.", + "format" : "pve-bridge-id-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storages" : { + "description" : "List of storages to check permission and availability. Will be checked again for all actually used storages during migration.", + "format" : "pve-storage-id-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "and", + [ + "perm", + "/vms/{vmid}", + [ + "VM.Allocate" + ] + ], + [ + "perm", + "/", + [ + "Sys.Incoming" + ] + ] + ], + "description" : "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming on '/'. Further permission checks happen during the actual migration." + }, + "protected" : 1, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "socket" : { + "type" : "string" + }, + "ticket" : { + "type" : "string" + }, + "upid" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/mtunnel", + "text" : "mtunnel" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.", + "method" : "GET", + "name" : "mtunnelwebsocket", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "socket" : { + "description" : "unix socket to forward to", + "type" : "string", + "typetext" : "" + }, + "ticket" : { + "description" : "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "description" : "You need to pass a ticket valid for the selected socket. Tickets can be created via the mtunnel API call, which will check permissions accordingly.", + "user" : "all" + }, + "returns" : { + "properties" : { + "port" : { + "optional" : 1, + "type" : "string" + }, + "socket" : { + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/qemu/{vmid}/mtunnelwebsocket", + "text" : "mtunnelwebsocket" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Destroy the VM and all used/owned volumes. Removes any VM specific permissions and firewall rules", + "method" : "DELETE", + "name" : "destroy_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "destroy-unreferenced-disks" : { + "default" : 0, + "description" : "If set, destroy additionally all disks not referenced in the config but with a matching VMID from all enabled storages.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "purge" : { + "description" : "Remove VMID from configurations, like backup & replication jobs and HA.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Allocate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Directory index", + "method" : "GET", + "name" : "vmdiridx", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "subdir" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{subdir}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu/{vmid}", + "text" : "{vmid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Virtual machine index (per node).", + "method" : "GET", + "name" : "vmlist", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "full" : { + "description" : "Determine the full status of active VMs.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Only list VMs where you have VM.Audit permissons on /vms/.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "cpus" : { + "description" : "Maximum usable CPUs.", + "optional" : 1, + "type" : "number" + }, + "lock" : { + "description" : "The current config lock, if any.", + "optional" : 1, + "type" : "string" + }, + "maxdisk" : { + "description" : "Root disk size in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "maxmem" : { + "description" : "Maximum memory in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "name" : { + "description" : "VM name.", + "optional" : 1, + "type" : "string" + }, + "pid" : { + "description" : "PID of running qemu process.", + "optional" : 1, + "type" : "integer" + }, + "qmpstatus" : { + "description" : "VM run state from the 'query-status' QMP monitor command.", + "optional" : 1, + "type" : "string" + }, + "running-machine" : { + "description" : "The currently running machine type (if running).", + "optional" : 1, + "type" : "string" + }, + "running-qemu" : { + "description" : "The currently running QEMU version (if running).", + "optional" : 1, + "type" : "string" + }, + "status" : { + "description" : "QEMU process status.", + "enum" : [ + "stopped", + "running" + ], + "type" : "string" + }, + "tags" : { + "description" : "The current configured tags, if any", + "optional" : 1, + "type" : "string" + }, + "uptime" : { + "description" : "Uptime.", + "optional" : 1, + "renderer" : "duration", + "type" : "integer" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{vmid}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create or restore a virtual machine.", + "method" : "POST", + "name" : "create_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "acpi" : { + "default" : 1, + "description" : "Enable/disable ACPI.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "affinity" : { + "description" : "List of host cores used to execute guest processes, for example: 0,5,8-11", + "format" : "pve-cpuset", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "agent" : { + "description" : "Enable/disable communication with the QEMU Guest Agent and its properties.", + "format" : { + "enabled" : { + "default" : 0, + "default_key" : 1, + "description" : "Enable/disable communication with a QEMU Guest Agent (QGA) running in the VM.", + "type" : "boolean" + }, + "freeze-fs-on-backup" : { + "default" : 1, + "description" : "Freeze/thaw guest filesystems on backup for consistency.", + "optional" : 1, + "type" : "boolean" + }, + "fstrim_cloned_disks" : { + "default" : 0, + "description" : "Run fstrim after moving a disk or migrating the VM.", + "optional" : 1, + "type" : "boolean" + }, + "type" : { + "default" : "virtio", + "description" : "Select the agent type", + "enum" : [ + "virtio", + "isa" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[enabled=]<1|0> [,freeze-fs-on-backup=<1|0>] [,fstrim_cloned_disks=<1|0>] [,type=]" + }, + "arch" : { + "description" : "Virtual processor architecture. Defaults to the host.", + "enum" : [ + "x86_64", + "aarch64" + ], + "optional" : 1, + "type" : "string" + }, + "archive" : { + "description" : "The backup archive. Either the file system path to a .tar or .vma file (use '-' to pipe data from stdin) or a proxmox storage backup volume identifier.", + "maxLength" : 255, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "args" : { + "description" : "Arbitrary arguments passed to kvm.", + "optional" : 1, + "type" : "string", + "typetext" : "", + "verbose_description" : "Arbitrary arguments passed to kvm, for example:\n\nargs: -no-reboot -smbios 'type=0,vendor=FOO'\n\nNOTE: this option is for experts only.\n" + }, + "audio0" : { + "description" : "Configure a audio device, useful in combination with QXL/Spice.", + "format" : { + "device" : { + "description" : "Configure an audio device.", + "enum" : [ + "ich9-intel-hda", + "intel-hda", + "AC97" + ], + "type" : "string" + }, + "driver" : { + "default" : "spice", + "description" : "Driver backend for the audio device.", + "enum" : [ + "spice", + "none" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "device= [,driver=]" + }, + "autostart" : { + "default" : 0, + "description" : "Automatic restart after crash (currently ignored).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "balloon" : { + "description" : "Amount of target RAM for the VM in MiB. Using zero disables the ballon driver.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "bios" : { + "default" : "seabios", + "description" : "Select BIOS implementation.", + "enum" : [ + "seabios", + "ovmf" + ], + "optional" : 1, + "type" : "string" + }, + "boot" : { + "description" : "Specify guest boot order. Use the 'order=' sub-property as usage with no key or 'legacy=' is deprecated.", + "format" : "pve-qm-boot", + "optional" : 1, + "type" : "string", + "typetext" : "[[legacy=]<[acdn]{1,4}>] [,order=]" + }, + "bootdisk" : { + "description" : "Enable booting from specified disk. Deprecated: Use 'boot: order=foo;bar' instead.", + "format" : "pve-qm-bootdisk", + "optional" : 1, + "pattern" : "(ide|sata|scsi|virtio)\\d+", + "type" : "string" + }, + "bwlimit" : { + "default" : "restore limit from datacenter or storage config", + "description" : "Override I/O bandwidth limit (in KiB/s).", + "minimum" : "0", + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "cdrom" : { + "description" : "This is an alias for option -ide2", + "format" : "pve-qm-ide", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "cicustom" : { + "description" : "cloud-init: Specify custom files to replace the automatically generated ones at start.", + "format" : "pve-qm-cicustom", + "optional" : 1, + "type" : "string", + "typetext" : "[meta=] [,network=] [,user=] [,vendor=]" + }, + "cipassword" : { + "description" : "cloud-init: Password to assign the user. Using this is generally not recommended. Use ssh keys instead. Also note that older cloud-init versions do not support hashed passwords.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "citype" : { + "description" : "Specifies the cloud-init configuration format. The default depends on the configured operating system type (`ostype`. We use the `nocloud` format for Linux, and `configdrive2` for windows.", + "enum" : [ + "configdrive2", + "nocloud", + "opennebula" + ], + "optional" : 1, + "type" : "string" + }, + "ciupgrade" : { + "default" : 1, + "description" : "cloud-init: do an automatic package upgrade after the first boot.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ciuser" : { + "description" : "cloud-init: User name to change ssh keys and password for instead of the image's configured default user.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "cores" : { + "default" : 1, + "description" : "The number of cores per socket.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "cpu" : { + "description" : "Emulated CPU type.", + "format" : "pve-vm-cpu-conf", + "optional" : 1, + "type" : "string", + "typetext" : "[[cputype=]] [,flags=<+FLAG[;-FLAG...]>] [,hidden=<1|0>] [,hv-vendor-id=] [,phys-bits=<8-64|host>] [,reported-model=]" + }, + "cpulimit" : { + "default" : 0, + "description" : "Limit of CPU usage.", + "maximum" : 128, + "minimum" : 0, + "optional" : 1, + "type" : "number", + "typetext" : " (0 - 128)", + "verbose_description" : "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has total of '2' CPU time. Value '0' indicates no CPU limit." + }, + "cpuunits" : { + "default" : "cgroup v1: 1024, cgroup v2: 100", + "description" : "CPU weight for a VM, will be clamped to [1, 10000] in cgroup v2.", + "maximum" : 262144, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 262144)", + "verbose_description" : "CPU weight for a VM. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this VM gets. Number is relative to weights of all the other running VMs." + }, + "description" : { + "description" : "Description for the VM. Shown in the web-interface VM's summary. This is saved as comment inside the configuration file.", + "maxLength" : 8192, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "efidisk0" : { + "description" : "Configure a disk for storing EFI vars. Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Note that SIZE_IN_GiB is ignored here and that the default EFI vars are copied to the volume instead. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "efitype" : { + "default" : "2m", + "description" : "Size and type of the OVMF EFI vars. '4m' is newer and recommended, and required for Secure Boot. For backwards compatibility, '2m' is used if not otherwise specified. Ignored for VMs with arch=aarch64 (ARM).", + "enum" : [ + "2m", + "4m" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "pre-enrolled-keys" : { + "default" : 0, + "description" : "Use am EFI vars template with distribution-specific and Microsoft Standard keys enrolled, if used with 'efitype=4m'. Note that this will enable Secure Boot by default, though it can still be turned off from within the VM.", + "optional" : 1, + "type" : "boolean" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,efitype=<2m|4m>] [,format=] [,import-from=] [,pre-enrolled-keys=<1|0>] [,size=]" + }, + "force" : { + "description" : "Allow to overwrite existing VM.", + "optional" : 1, + "requires" : "archive", + "type" : "boolean", + "typetext" : "" + }, + "freeze" : { + "description" : "Freeze CPU at startup (use 'c' monitor command to start execution).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "hookscript" : { + "description" : "Script that will be executed during various steps in the vms lifetime.", + "format" : "pve-volume-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "hostpci[n]" : { + "description" : "Map host PCI devices into guest.", + "format" : "pve-qm-hostpci", + "optional" : 1, + "type" : "string", + "typetext" : "[[host=]] [,device-id=] [,legacy-igd=<1|0>] [,mapping=] [,mdev=] [,pcie=<1|0>] [,rombar=<1|0>] [,romfile=] [,sub-device-id=] [,sub-vendor-id=] [,vendor-id=] [,x-vga=<1|0>]", + "verbose_description" : "Map host PCI devices into guest.\n\nNOTE: This option allows direct access to host hardware. So it is no longer\npossible to migrate such machines - use with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "hotplug" : { + "default" : "network,disk,usb", + "description" : "Selectively enable hotplug features. This is a comma separated list of hotplug features: 'network', 'disk', 'cpu', 'memory', 'usb' and 'cloudinit'. Use '0' to disable hotplug completely. Using '1' as value is an alias for the default `network,disk,usb`. USB hotplugging is possible for guests with machine version >= 7.1 and ostype l26 or windows > 7.", + "format" : "pve-hotplug-features", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "hugepages" : { + "description" : "Enable/disable hugepages memory.", + "enum" : [ + "any", + "2", + "1024" + ], + "optional" : 1, + "type" : "string" + }, + "ide[n]" : { + "description" : "Use volume as IDE hard disk or CD-ROM (n is 0 to 3). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "model" : { + "description" : "The drive's reported model name, url-encoded, up to 40 bytes long.", + "format" : "urlencoded", + "format_description" : "model", + "maxLength" : 120, + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,model=] [,replicate=<1|0>] [,rerror=] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,ssd=<1|0>] [,trans=] [,werror=] [,wwn=]" + }, + "ipconfig[n]" : { + "description" : "cloud-init: Specify IP addresses and gateways for the corresponding interface.\n\nIP addresses use CIDR notation, gateways are optional but need an IP of the same type specified.\n\nThe special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit\ngateway should be provided.\nFor IPv6 the special string 'auto' can be used to use stateless autoconfiguration. This requires\ncloud-init 19.4 or newer.\n\nIf cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using\ndhcp on IPv4.\n", + "format" : "pve-qm-ipconfig", + "optional" : 1, + "type" : "string", + "typetext" : "[gw=] [,gw6=] [,ip=] [,ip6=]" + }, + "ivshmem" : { + "description" : "Inter-VM shared memory. Useful for direct communication between VMs, or to the host.", + "format" : { + "name" : { + "description" : "The name of the file. Will be prefixed with 'pve-shm-'. Default is the VMID. Will be deleted when the VM is stopped.", + "format_description" : "string", + "optional" : 1, + "pattern" : "[a-zA-Z0-9\\-]+", + "type" : "string" + }, + "size" : { + "description" : "The size of the file in MB.", + "minimum" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "size= [,name=]" + }, + "keephugepages" : { + "default" : 0, + "description" : "Use together with hugepages. If enabled, hugepages will not not be deleted after VM shutdown and can be used for subsequent starts.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "keyboard" : { + "default" : null, + "description" : "Keyboard layout for VNC server. This option is generally not required and is often better handled from within the guest OS.", + "enum" : [ + "de", + "de-ch", + "da", + "en-gb", + "en-us", + "es", + "fi", + "fr", + "fr-be", + "fr-ca", + "fr-ch", + "hu", + "is", + "it", + "ja", + "lt", + "mk", + "nl", + "no", + "pl", + "pt", + "pt-br", + "sv", + "sl", + "tr" + ], + "optional" : 1, + "type" : "string" + }, + "kvm" : { + "default" : 1, + "description" : "Enable/disable KVM hardware virtualization.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "live-restore" : { + "description" : "Start the VM immediately from the backup and restore in background. PBS only.", + "optional" : 1, + "requires" : "archive", + "type" : "boolean", + "typetext" : "" + }, + "localtime" : { + "description" : "Set the real time clock (RTC) to local time. This is enabled by default if the `ostype` indicates a Microsoft Windows OS.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "lock" : { + "description" : "Lock/unlock the VM.", + "enum" : [ + "backup", + "clone", + "create", + "migrate", + "rollback", + "snapshot", + "snapshot-delete", + "suspending", + "suspended" + ], + "optional" : 1, + "type" : "string" + }, + "machine" : { + "description" : "Specifies the QEMU machine type.", + "maxLength" : 40, + "optional" : 1, + "pattern" : "(pc|pc(-i440fx)?-\\d+(\\.\\d+)+(\\+pve\\d+)?(\\.pxe)?|q35|pc-q35-\\d+(\\.\\d+)+(\\+pve\\d+)?(\\.pxe)?|virt(?:-\\d+(\\.\\d+)+)?(\\+pve\\d+)?)", + "type" : "string" + }, + "memory" : { + "description" : "Memory properties.", + "format" : { + "current" : { + "default" : 512, + "default_key" : 1, + "description" : "Current amount of online RAM for the VM in MiB. This is the maximum available memory when you use the balloon device.", + "minimum" : 16, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[current=]" + }, + "migrate_downtime" : { + "default" : 0.1, + "description" : "Set maximum tolerated downtime (in seconds) for migrations.", + "minimum" : 0, + "optional" : 1, + "type" : "number", + "typetext" : " (0 - N)" + }, + "migrate_speed" : { + "default" : 0, + "description" : "Set maximum speed (in MB/s) for migrations. Value 0 is no limit.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "name" : { + "description" : "Set a name for the VM. Only used on the configuration web interface.", + "format" : "dns-name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "nameserver" : { + "description" : "cloud-init: Sets DNS server IP address for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.", + "format" : "address-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "net[n]" : { + "description" : "Specify network devices.", + "format" : { + "bridge" : { + "description" : "Bridge to attach the network device to. The Proxmox VE standard bridge\nis called 'vmbr0'.\n\nIf you do not specify a bridge, we create a kvm user (NATed) network\ndevice, which provides DHCP and DNS services. The following addresses\nare used:\n\n 10.0.2.2 Gateway\n 10.0.2.3 DNS Server\n 10.0.2.4 SMB Server\n\nThe DHCP server assign addresses to the guest starting from 10.0.2.15.\n", + "format" : "pve-bridge-id", + "format_description" : "bridge", + "optional" : 1, + "type" : "string" + }, + "e1000" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82540em" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82544gc" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000-82545em" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "e1000e" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "firewall" : { + "description" : "Whether this interface should be protected by the firewall.", + "optional" : 1, + "type" : "boolean" + }, + "i82551" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "i82557b" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "i82559er" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "link_down" : { + "description" : "Whether this interface should be disconnected (like pulling the plug).", + "optional" : 1, + "type" : "boolean" + }, + "macaddr" : { + "description" : "MAC address. That address must be unique withing your network. This is automatically generated if not specified.", + "format" : "mac-addr", + "format_description" : "XX:XX:XX:XX:XX:XX", + "optional" : 1, + "type" : "string", + "verbose_description" : "A common MAC address with the I/G (Individual/Group) bit not set." + }, + "model" : { + "default_key" : 1, + "description" : "Network Card Model. The 'virtio' model provides the best performance with very low CPU overhead. If your guest does not support this driver, it is usually best to use 'e1000'.", + "enum" : [ + "e1000", + "e1000-82540em", + "e1000-82544gc", + "e1000-82545em", + "e1000e", + "i82551", + "i82557b", + "i82559er", + "ne2k_isa", + "ne2k_pci", + "pcnet", + "rtl8139", + "virtio", + "vmxnet3" + ], + "type" : "string" + }, + "mtu" : { + "description" : "Force MTU, for VirtIO only. Set to '1' to use the bridge MTU", + "maximum" : 65520, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "ne2k_isa" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "ne2k_pci" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "pcnet" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "queues" : { + "description" : "Number of packet queues to be used on the device.", + "maximum" : 64, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "rate" : { + "description" : "Rate limit in mbps (megabytes per second) as floating point number.", + "minimum" : 0, + "optional" : 1, + "type" : "number" + }, + "rtl8139" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "tag" : { + "description" : "VLAN tag to apply to packets on this interface.", + "maximum" : 4094, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "trunks" : { + "description" : "VLAN trunks to pass through this interface.", + "format_description" : "vlanid[;vlanid...]", + "optional" : 1, + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "virtio" : { + "alias" : "macaddr", + "keyAlias" : "model" + }, + "vmxnet3" : { + "alias" : "macaddr", + "keyAlias" : "model" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[model=] [,bridge=] [,firewall=<1|0>] [,link_down=<1|0>] [,macaddr=] [,mtu=] [,queues=] [,rate=] [,tag=] [,trunks=] [,=]" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "numa" : { + "default" : 0, + "description" : "Enable/disable NUMA.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "numa[n]" : { + "description" : "NUMA topology.", + "format" : { + "cpus" : { + "description" : "CPUs accessing this NUMA node.", + "format_description" : "id[-id];...", + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "hostnodes" : { + "description" : "Host NUMA nodes to use.", + "format_description" : "id[-id];...", + "optional" : 1, + "pattern" : "(?^:\\d+(?:-\\d+)?(?:;\\d+(?:-\\d+)?)*)", + "type" : "string" + }, + "memory" : { + "description" : "Amount of memory this NUMA node provides.", + "optional" : 1, + "type" : "number" + }, + "policy" : { + "description" : "NUMA allocation policy.", + "enum" : [ + "preferred", + "bind", + "interleave" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "cpus= [,hostnodes=] [,memory=] [,policy=]" + }, + "onboot" : { + "default" : 0, + "description" : "Specifies whether a VM will be started during system bootup.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ostype" : { + "description" : "Specify guest operating system.", + "enum" : [ + "other", + "wxp", + "w2k", + "w2k3", + "w2k8", + "wvista", + "win7", + "win8", + "win10", + "win11", + "l24", + "l26", + "solaris" + ], + "optional" : 1, + "type" : "string", + "verbose_description" : "Specify guest operating system. This is used to enable special\noptimization/features for specific operating systems:\n\n[horizontal]\nother;; unspecified OS\nwxp;; Microsoft Windows XP\nw2k;; Microsoft Windows 2000\nw2k3;; Microsoft Windows 2003\nw2k8;; Microsoft Windows 2008\nwvista;; Microsoft Windows Vista\nwin7;; Microsoft Windows 7\nwin8;; Microsoft Windows 8/2012/2012r2\nwin10;; Microsoft Windows 10/2016/2019\nwin11;; Microsoft Windows 11/2022\nl24;; Linux 2.4 Kernel\nl26;; Linux 2.6 - 6.X Kernel\nsolaris;; Solaris/OpenSolaris/OpenIndiania kernel\n" + }, + "parallel[n]" : { + "description" : "Map host parallel devices (n is 0 to 2).", + "optional" : 1, + "pattern" : "/dev/parport\\d+|/dev/usb/lp\\d+", + "type" : "string", + "verbose_description" : "Map host parallel devices (n is 0 to 2).\n\nNOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such\nmachines - use with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "pool" : { + "description" : "Add the VM to the specified pool.", + "format" : "pve-poolid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "protection" : { + "default" : 0, + "description" : "Sets the protection flag of the VM. This will disable the remove VM and remove disk operations.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "reboot" : { + "default" : 1, + "description" : "Allow reboot. If set to '0' the VM exit on reboot.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "rng0" : { + "description" : "Configure a VirtIO-based Random Number Generator.", + "format" : { + "max_bytes" : { + "default" : 1024, + "description" : "Maximum bytes of entropy allowed to get injected into the guest every 'period' milliseconds. Prefer a lower value when using '/dev/random' as source. Use `0` to disable limiting (potentially dangerous!).", + "optional" : 1, + "type" : "integer" + }, + "period" : { + "default" : 1000, + "description" : "Every 'period' milliseconds the entropy-injection quota is reset, allowing the guest to retrieve another 'max_bytes' of entropy.", + "optional" : 1, + "type" : "integer" + }, + "source" : { + "default_key" : 1, + "description" : "The file on the host to gather entropy from. In most cases '/dev/urandom' should be preferred over '/dev/random' to avoid entropy-starvation issues on the host. Using urandom does *not* decrease security in any meaningful way, as it's still seeded from real entropy, and the bytes provided will most likely be mixed with real entropy on the guest as well. '/dev/hwrng' can be used to pass through a hardware RNG from the host.", + "enum" : [ + "/dev/urandom", + "/dev/random", + "/dev/hwrng" + ], + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[source=] [,max_bytes=] [,period=]" + }, + "sata[n]" : { + "description" : "Use volume as SATA hard disk or CD-ROM (n is 0 to 5). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,replicate=<1|0>] [,rerror=] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,ssd=<1|0>] [,trans=] [,werror=] [,wwn=]" + }, + "scsi[n]" : { + "description" : "Use volume as SCSI hard disk or CD-ROM (n is 0 to 30). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iothread" : { + "description" : "Whether to use iothreads for this drive", + "optional" : 1, + "type" : "boolean" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "queues" : { + "description" : "Number of queues.", + "minimum" : 2, + "optional" : 1, + "type" : "integer" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "ro" : { + "description" : "Whether the drive is read-only.", + "optional" : 1, + "type" : "boolean" + }, + "scsiblock" : { + "default" : 0, + "description" : "whether to use scsi-block for full passthrough of host block device\n\nWARNING: can lead to I/O errors in combination with low memory or high memory fragmentation on host", + "optional" : 1, + "type" : "boolean" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "ssd" : { + "description" : "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "description" : "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + "format_description" : "wwn", + "optional" : 1, + "pattern" : "(?^:^(0x)[0-9a-fA-F]{16})", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,iothread=<1|0>] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,queues=] [,replicate=<1|0>] [,rerror=] [,ro=<1|0>] [,scsiblock=<1|0>] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,ssd=<1|0>] [,trans=] [,werror=] [,wwn=]" + }, + "scsihw" : { + "default" : "lsi", + "description" : "SCSI controller model", + "enum" : [ + "lsi", + "lsi53c810", + "virtio-scsi-pci", + "virtio-scsi-single", + "megasas", + "pvscsi" + ], + "optional" : 1, + "type" : "string" + }, + "searchdomain" : { + "description" : "cloud-init: Sets DNS search domains for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "serial[n]" : { + "description" : "Create a serial device inside the VM (n is 0 to 3)", + "optional" : 1, + "pattern" : "(/dev/.+|socket)", + "type" : "string", + "verbose_description" : "Create a serial device inside the VM (n is 0 to 3), and pass through a\nhost serial device (i.e. /dev/ttyS0), or create a unix socket on the\nhost side (use 'qm terminal' to open a terminal connection).\n\nNOTE: If you pass through a host serial device, it is no longer possible to migrate such machines -\nuse with special care.\n\nCAUTION: Experimental! User reported problems with this option.\n" + }, + "shares" : { + "default" : 1000, + "description" : "Amount of memory shares for auto-ballooning. The larger the number is, the more memory this VM gets. Number is relative to weights of all other running VMs. Using zero disables auto-ballooning. Auto-ballooning is done by pvestatd.", + "maximum" : 50000, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 50000)" + }, + "smbios1" : { + "description" : "Specify SMBIOS type 1 fields.", + "format" : "pve-qm-smbios1", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "[base64=<1|0>] [,family=] [,manufacturer=] [,product=] [,serial=] [,sku=] [,uuid=] [,version=]" + }, + "smp" : { + "default" : 1, + "description" : "The number of CPUs. Please use option -sockets instead.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "sockets" : { + "default" : 1, + "description" : "The number of CPU sockets.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "spice_enhancements" : { + "description" : "Configure additional enhancements for SPICE.", + "format" : { + "foldersharing" : { + "default" : "0", + "description" : "Enable folder sharing via SPICE. Needs Spice-WebDAV daemon installed in the VM.", + "optional" : 1, + "type" : "boolean" + }, + "videostreaming" : { + "default" : "off", + "description" : "Enable video streaming. Uses compression for detected video streams.", + "enum" : [ + "off", + "all", + "filter" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[foldersharing=<1|0>] [,videostreaming=]" + }, + "sshkeys" : { + "description" : "cloud-init: Setup public SSH keys (one key per line, OpenSSH format).", + "format" : "urlencoded", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "start" : { + "default" : 0, + "description" : "Start VM after it was created successfully.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "startdate" : { + "default" : "now", + "description" : "Set the initial date of the real time clock. Valid format for date are:'now' or '2006-06-17T16:01:21' or '2006-06-17'.", + "optional" : 1, + "pattern" : "(now|\\d{4}-\\d{1,2}-\\d{1,2}(T\\d{1,2}:\\d{1,2}:\\d{1,2})?)", + "type" : "string", + "typetext" : "(now | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS)" + }, + "startup" : { + "description" : "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped.", + "format" : "pve-startup-order", + "optional" : 1, + "type" : "string", + "typetext" : "[[order=]\\d+] [,up=\\d+] [,down=\\d+] " + }, + "storage" : { + "description" : "Default storage.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tablet" : { + "default" : 1, + "description" : "Enable/disable the USB tablet device.", + "optional" : 1, + "type" : "boolean", + "typetext" : "", + "verbose_description" : "Enable/disable the USB tablet device. This device is usually needed to allow absolute mouse positioning with VNC. Else the mouse runs out of sync with normal VNC clients. If you're running lots of console-only guests on one host, you may consider disabling this to save some context switches. This is turned off by default if you use spice (`qm set --vga qxl`)." + }, + "tags" : { + "description" : "Tags of the VM. This is only meta information.", + "format" : "pve-tag-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tdf" : { + "default" : 0, + "description" : "Enable/disable time drift fix.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "template" : { + "default" : 0, + "description" : "Enable/disable Template.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "tpmstate0" : { + "description" : "Configure a Disk for storing TPM state. The format is fixed to 'raw'. Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Note that SIZE_IN_GiB is ignored here and 4 MiB will be used instead. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "version" : { + "default" : "v2.0", + "description" : "The TPM interface version. v2.0 is newer and should be preferred. Note that this cannot be changed later on.", + "enum" : [ + "v1.2", + "v2.0" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,import-from=] [,size=] [,version=]" + }, + "unique" : { + "description" : "Assign a unique random ethernet address.", + "optional" : 1, + "requires" : "archive", + "type" : "boolean", + "typetext" : "" + }, + "unused[n]" : { + "description" : "Reference to unused volumes. This is used internally, and should not be modified manually.", + "format" : { + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id", + "format_description" : "volume", + "type" : "string" + }, + "volume" : { + "alias" : "file" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=]" + }, + "usb[n]" : { + "description" : "Configure an USB device (n is 0 to 4, for machine version >= 7.1 and ostype l26 or windows > 7, n can be up to 14).", + "format" : { + "host" : { + "default_key" : 1, + "description" : "The Host USB device or port or the value 'spice'. HOSTUSBDEVICE syntax is:\n\n 'bus-port(.port)*' (decimal numbers) or\n 'vendor_id:product_id' (hexadeciaml numbers) or\n 'spice'\n\nYou can use the 'lsusb -t' command to list existing usb devices.\n\nNOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such\nmachines - use with special care.\n\nThe value 'spice' can be used to add a usb redirection devices for spice.\n\nEither this or the 'mapping' key must be set.\n", + "format_description" : "HOSTUSBDEVICE|spice", + "optional" : 1, + "pattern" : "(?^:(?:(?:(?^:(0x)?([0-9A-Fa-f]{4}):(0x)?([0-9A-Fa-f]{4})))|(?:(?^:(\\d+)\\-(\\d+(\\.\\d+)*)))|[Ss][Pp][Ii][Cc][Ee]))", + "type" : "string" + }, + "mapping" : { + "description" : "The ID of a cluster wide mapping. Either this or the default-key 'host' must be set.", + "format" : "pve-configid", + "format_description" : "mapping-id", + "optional" : 1, + "type" : "string" + }, + "usb3" : { + "default" : 0, + "description" : "Specifies whether if given host option is a USB3 device or port. For modern guests (machine version >= 7.1 and ostype l26 and windows > 7), this flag is irrelevant (all devices are plugged into a xhci controller).", + "optional" : 1, + "type" : "boolean" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[[host=]] [,mapping=] [,usb3=<1|0>]" + }, + "vcpus" : { + "default" : 0, + "description" : "Number of hotplugged vcpus.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "vga" : { + "description" : "Configure the VGA hardware.", + "format" : { + "clipboard" : { + "description" : "Enable a specific clipboard. If not set, depending on the display type the SPICE one will be added.", + "enum" : [ + "vnc" + ], + "optional" : 1, + "type" : "string" + }, + "memory" : { + "description" : "Sets the VGA memory (in MiB). Has no effect with serial display.", + "maximum" : 512, + "minimum" : 4, + "optional" : 1, + "type" : "integer" + }, + "type" : { + "default" : "std", + "default_key" : 1, + "description" : "Select the VGA type.", + "enum" : [ + "cirrus", + "qxl", + "qxl2", + "qxl3", + "qxl4", + "none", + "serial0", + "serial1", + "serial2", + "serial3", + "std", + "virtio", + "virtio-gl", + "vmware" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[[type=]] [,clipboard=] [,memory=]", + "verbose_description" : "Configure the VGA Hardware. If you want to use high resolution modes (>= 1280x1024x16) you may need to increase the vga memory option. Since QEMU 2.9 the default VGA display type is 'std' for all OS types besides some Windows versions (XP and older) which use 'cirrus'. The 'qxl' option enables the SPICE display server. For win* OS you can select how many independent displays you want, Linux guests can add displays them self.\nYou can also run without any graphic card, using a serial device as terminal." + }, + "virtio[n]" : { + "description" : "Use volume as VIRTIO hard disk (n is 0 to 15). Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume. Use STORAGE_ID:0 and the 'import-from' parameter to import from an existing volume.", + "format" : { + "aio" : { + "description" : "AIO type to use.", + "enum" : [ + "native", + "threads", + "io_uring" + ], + "optional" : 1, + "type" : "string" + }, + "backup" : { + "description" : "Whether the drive should be included when making backups.", + "optional" : 1, + "type" : "boolean" + }, + "bps" : { + "description" : "Maximum r/w speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_rd" : { + "description" : "Maximum read speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_rd_length" : { + "alias" : "bps_rd_max_length" + }, + "bps_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "bps_wr" : { + "description" : "Maximum write speed in bytes per second.", + "format_description" : "bps", + "optional" : 1, + "type" : "integer" + }, + "bps_wr_length" : { + "alias" : "bps_wr_max_length" + }, + "bps_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cache" : { + "description" : "The drive's cache mode", + "enum" : [ + "none", + "writethrough", + "writeback", + "unsafe", + "directsync" + ], + "optional" : 1, + "type" : "string" + }, + "cyls" : { + "description" : "Force the drive's physical geometry to have a specific cylinder count.", + "optional" : 1, + "type" : "integer" + }, + "detect_zeroes" : { + "description" : "Controls whether to detect and try to optimize writes of zeroes.", + "optional" : 1, + "type" : "boolean" + }, + "discard" : { + "description" : "Controls whether to pass discard/trim requests to the underlying storage.", + "enum" : [ + "ignore", + "on" + ], + "optional" : 1, + "type" : "string" + }, + "file" : { + "default_key" : 1, + "description" : "The drive's backing volume.", + "format" : "pve-volume-id-or-qm-path", + "format_description" : "volume", + "type" : "string" + }, + "format" : { + "description" : "The drive's backing file's data format.", + "enum" : [ + "raw", + "cow", + "qcow", + "qed", + "qcow2", + "vmdk", + "cloop" + ], + "optional" : 1, + "type" : "string" + }, + "heads" : { + "description" : "Force the drive's physical geometry to have a specific head count.", + "optional" : 1, + "type" : "integer" + }, + "import-from" : { + "description" : "Create a new disk, importing from this source (volume ID or absolute path). When an absolute path is specified, it's up to you to ensure that the source is not actively used by another process during the import!", + "format" : "pve-volume-id-or-absolute-path", + "format_description" : "source volume", + "optional" : 1, + "type" : "string" + }, + "iops" : { + "description" : "Maximum r/w I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max" : { + "description" : "Maximum unthrottled r/w I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_max_length" : { + "description" : "Maximum length of I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_rd" : { + "description" : "Maximum read I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_length" : { + "alias" : "iops_rd_max_length" + }, + "iops_rd_max" : { + "description" : "Maximum unthrottled read I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_rd_max_length" : { + "description" : "Maximum length of read I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iops_wr" : { + "description" : "Maximum write I/O in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_length" : { + "alias" : "iops_wr_max_length" + }, + "iops_wr_max" : { + "description" : "Maximum unthrottled write I/O pool in operations per second.", + "format_description" : "iops", + "optional" : 1, + "type" : "integer" + }, + "iops_wr_max_length" : { + "description" : "Maximum length of write I/O bursts in seconds.", + "format_description" : "seconds", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "iothread" : { + "description" : "Whether to use iothreads for this drive", + "optional" : 1, + "type" : "boolean" + }, + "mbps" : { + "description" : "Maximum r/w speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_max" : { + "description" : "Maximum unthrottled r/w pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd" : { + "description" : "Maximum read speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_rd_max" : { + "description" : "Maximum unthrottled read pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr" : { + "description" : "Maximum write speed in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "mbps_wr_max" : { + "description" : "Maximum unthrottled write pool in megabytes per second.", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "media" : { + "default" : "disk", + "description" : "The drive's media type.", + "enum" : [ + "cdrom", + "disk" + ], + "optional" : 1, + "type" : "string" + }, + "replicate" : { + "default" : 1, + "description" : "Whether the drive should considered for replication jobs.", + "optional" : 1, + "type" : "boolean" + }, + "rerror" : { + "description" : "Read error action.", + "enum" : [ + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "ro" : { + "description" : "Whether the drive is read-only.", + "optional" : 1, + "type" : "boolean" + }, + "secs" : { + "description" : "Force the drive's physical geometry to have a specific sector count.", + "optional" : 1, + "type" : "integer" + }, + "serial" : { + "description" : "The drive's reported serial number, url-encoded, up to 20 bytes long.", + "format" : "urlencoded", + "format_description" : "serial", + "maxLength" : 60, + "optional" : 1, + "type" : "string" + }, + "shared" : { + "default" : 0, + "description" : "Mark this locally-managed volume as available on all nodes", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Disk size. This is purely informational and has no effect.", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "snapshot" : { + "description" : "Controls qemu's snapshot mode feature. If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.", + "optional" : 1, + "type" : "boolean" + }, + "trans" : { + "description" : "Force disk geometry bios translation mode.", + "enum" : [ + "none", + "lba", + "auto" + ], + "optional" : 1, + "type" : "string" + }, + "volume" : { + "alias" : "file" + }, + "werror" : { + "description" : "Write error action.", + "enum" : [ + "enospc", + "ignore", + "report", + "stop" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[file=] [,aio=] [,backup=<1|0>] [,bps=] [,bps_max_length=] [,bps_rd=] [,bps_rd_max_length=] [,bps_wr=] [,bps_wr_max_length=] [,cache=] [,cyls=] [,detect_zeroes=<1|0>] [,discard=] [,format=] [,heads=] [,import-from=] [,iops=] [,iops_max=] [,iops_max_length=] [,iops_rd=] [,iops_rd_max=] [,iops_rd_max_length=] [,iops_wr=] [,iops_wr_max=] [,iops_wr_max_length=] [,iothread=<1|0>] [,mbps=] [,mbps_max=] [,mbps_rd=] [,mbps_rd_max=] [,mbps_wr=] [,mbps_wr_max=] [,media=] [,replicate=<1|0>] [,rerror=] [,ro=<1|0>] [,secs=] [,serial=] [,shared=<1|0>] [,size=] [,snapshot=<1|0>] [,trans=] [,werror=]" + }, + "vmgenid" : { + "default" : "1 (autogenerated)", + "description" : "Set VM Generation ID. Use '1' to autogenerate on create or update, pass '0' to disable explicitly.", + "format_description" : "UUID", + "optional" : 1, + "pattern" : "(?:[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}|[01])", + "type" : "string", + "verbose_description" : "The VM generation ID (vmgenid) device exposes a 128-bit integer value identifier to the guest OS. This allows to notify the guest operating system when the virtual machine is executed with a different configuration (e.g. snapshot execution or creation from a template). The guest operating system notices the change, and is then able to react as appropriate by marking its copies of distributed databases as dirty, re-initializing its random number generator, etc.\nNote that auto-creation only works when done through API/CLI create or update methods, but not when manually editing the config file." + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "vmstatestorage" : { + "description" : "Default storage for VM state volumes/files.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "watchdog" : { + "description" : "Create a virtual hardware watchdog device.", + "format" : "pve-qm-watchdog", + "optional" : 1, + "type" : "string", + "typetext" : "[[model=]] [,action=]", + "verbose_description" : "Create a virtual hardware watchdog device. Once enabled (by a guest action), the watchdog must be periodically polled by an agent inside the guest or else the watchdog will reset the guest (or execute the respective action specified)" + } + } + }, + "permissions" : { + "description" : "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. If you create disks you need 'Datastore.AllocateSpace' on any used storage.If you use a bridge/vlan, you need 'SDN.Use' on any used bridge/vlan.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/qemu", + "text" : "qemu" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get container configuration.", + "method" : "GET", + "name" : "vm_config", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "current" : { + "default" : 0, + "description" : "Get current values (instead of pending values).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapshot" : { + "description" : "Fetch config values from given snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "properties" : { + "arch" : { + "default" : "amd64", + "description" : "OS architecture type.", + "enum" : [ + "amd64", + "i386", + "arm64", + "armhf", + "riscv32", + "riscv64" + ], + "optional" : 1, + "type" : "string" + }, + "cmode" : { + "default" : "tty", + "description" : "Console mode. By default, the console command tries to open a connection to one of the available tty devices. By setting cmode to 'console' it tries to attach to /dev/console instead. If you set cmode to 'shell', it simply invokes a shell inside the container (no login).", + "enum" : [ + "shell", + "console", + "tty" + ], + "optional" : 1, + "type" : "string" + }, + "console" : { + "default" : 1, + "description" : "Attach a console device (/dev/console) to the container.", + "optional" : 1, + "type" : "boolean" + }, + "cores" : { + "description" : "The number of cores assigned to the container. A container can use all available cores by default.", + "maximum" : 8192, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "cpulimit" : { + "default" : 0, + "description" : "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has a total of '2' CPU time. Value '0' indicates no CPU limit.", + "maximum" : 8192, + "minimum" : 0, + "optional" : 1, + "type" : "number" + }, + "cpuunits" : { + "default" : "cgroup v1: 1024, cgroup v2: 100", + "description" : "CPU weight for a container, will be clamped to [1, 10000] in cgroup v2.", + "maximum" : 500000, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "verbose_description" : "CPU weight for a container. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this container gets. Number is relative to the weights of all the other running guests." + }, + "debug" : { + "default" : 0, + "description" : "Try to be more verbose. For now this only enables debug log-level on start.", + "optional" : 1, + "type" : "boolean" + }, + "description" : { + "description" : "Description for the Container. Shown in the web-interface CT's summary. This is saved as comment inside the configuration file.", + "maxLength" : 8192, + "optional" : 1, + "type" : "string" + }, + "dev[n]" : { + "description" : "Device to pass through to the container", + "format" : { + "gid" : { + "description" : "Group ID to be assigned to the device node", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "mode" : { + "description" : "Access mode to be set on the device node", + "format_description" : "Octal access mode", + "optional" : 1, + "pattern" : "0[0-7]{3}", + "type" : "string" + }, + "path" : { + "default_key" : 1, + "description" : "Device to pass through to the container", + "format" : "pve-lxc-dev-string", + "format_description" : "Path", + "optional" : 1, + "type" : "string", + "verbose_description" : "Path to the device to pass through to the container" + }, + "uid" : { + "description" : "User ID to be assigned to the device node", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "SHA1 digest of configuration file. This can be used to prevent concurrent modifications.", + "type" : "string" + }, + "features" : { + "description" : "Allow containers access to advanced features.", + "format" : { + "force_rw_sys" : { + "default" : 0, + "description" : "Mount /sys in unprivileged containers as `rw` instead of `mixed`. This can break networking under newer (>= v245) systemd-network use.", + "optional" : 1, + "type" : "boolean" + }, + "fuse" : { + "default" : 0, + "description" : "Allow using 'fuse' file systems in a container. Note that interactions between fuse and the freezer cgroup can potentially cause I/O deadlocks.", + "optional" : 1, + "type" : "boolean" + }, + "keyctl" : { + "default" : 0, + "description" : "For unprivileged containers only: Allow the use of the keyctl() system call. This is required to use docker inside a container. By default unprivileged containers will see this system call as non-existent. This is mostly a workaround for systemd-networkd, as it will treat it as a fatal error when some keyctl() operations are denied by the kernel due to lacking permissions. Essentially, you can choose between running systemd-networkd or docker.", + "optional" : 1, + "type" : "boolean" + }, + "mknod" : { + "default" : 0, + "description" : "Allow unprivileged containers to use mknod() to add certain device nodes. This requires a kernel with seccomp trap to user space support (5.3 or newer). This is experimental.", + "optional" : 1, + "type" : "boolean" + }, + "mount" : { + "description" : "Allow mounting file systems of specific types. This should be a list of file system types as used with the mount command. Note that this can have negative effects on the container's security. With access to a loop device, mounting a file can circumvent the mknod permission of the devices cgroup, mounting an NFS file system can block the host's I/O completely and prevent it from rebooting, etc.", + "format_description" : "fstype;fstype;...", + "optional" : 1, + "pattern" : "(?^:[a-zA-Z0-9_; ]+)", + "type" : "string" + }, + "nesting" : { + "default" : 0, + "description" : "Allow nesting. Best used with unprivileged containers with additional id mapping. Note that this will expose procfs and sysfs contents of the host to the guest.", + "optional" : 1, + "type" : "boolean" + } + }, + "optional" : 1, + "type" : "string" + }, + "hookscript" : { + "description" : "Script that will be exectued during various steps in the containers lifetime.", + "format" : "pve-volume-id", + "optional" : 1, + "type" : "string" + }, + "hostname" : { + "description" : "Set a host name for the container.", + "format" : "dns-name", + "maxLength" : 255, + "optional" : 1, + "type" : "string" + }, + "lock" : { + "description" : "Lock/unlock the container.", + "enum" : [ + "backup", + "create", + "destroyed", + "disk", + "fstrim", + "migrate", + "mounted", + "rollback", + "snapshot", + "snapshot-delete" + ], + "optional" : 1, + "type" : "string" + }, + "lxc" : { + "description" : "Array of lxc low-level configurations ([[key1, value1], [key2, value2] ...]).", + "items" : { + "items" : { + "type" : "string" + }, + "type" : "array" + }, + "optional" : 1, + "type" : "array" + }, + "memory" : { + "default" : 512, + "description" : "Amount of RAM for the container in MB.", + "minimum" : 16, + "optional" : 1, + "type" : "integer" + }, + "mp[n]" : { + "description" : "Use volume as container mount point. Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume.", + "format" : { + "acl" : { + "description" : "Explicitly enable or disable ACL support.", + "optional" : 1, + "type" : "boolean" + }, + "backup" : { + "description" : "Whether to include the mount point in backups.", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Whether to include the mount point in backups (only used for volume mount points)." + }, + "mountoptions" : { + "description" : "Extra mount options for rootfs/mps.", + "format_description" : "opt[;opt...]", + "optional" : 1, + "pattern" : "(?^:(?^:(noatime|lazytime|nodev|nosuid|noexec))(;(?^:(noatime|lazytime|nodev|nosuid|noexec)))*)", + "type" : "string" + }, + "mp" : { + "description" : "Path to the mount point as seen from inside the container (must not contain symlinks).", + "format" : "pve-lxc-mp-string", + "format_description" : "Path", + "type" : "string", + "verbose_description" : "Path to the mount point as seen from inside the container.\n\nNOTE: Must not contain any symlinks for security reasons." + }, + "quota" : { + "description" : "Enable user quotas inside the container (not supported with zfs subvolumes)", + "optional" : 1, + "type" : "boolean" + }, + "replicate" : { + "default" : 1, + "description" : "Will include this volume to a storage replica job.", + "optional" : 1, + "type" : "boolean" + }, + "ro" : { + "description" : "Read-only mount point", + "optional" : 1, + "type" : "boolean" + }, + "shared" : { + "default" : 0, + "description" : "Mark this non-volume mount point as available on multiple nodes (see 'nodes')", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this non-volume mount point as available on all nodes.\n\nWARNING: This option does not share the mount point automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Volume size (read only value).", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "volume" : { + "default_key" : 1, + "description" : "Volume, device or directory to mount into the container.", + "format" : "pve-lxc-mp-string", + "format_description" : "volume", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "nameserver" : { + "description" : "Sets DNS server IP address for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.", + "format" : "lxc-ip-with-ll-iface-list", + "optional" : 1, + "type" : "string" + }, + "net[n]" : { + "description" : "Specifies network interfaces for the container.", + "format" : { + "bridge" : { + "description" : "Bridge to attach the network device to.", + "format_description" : "bridge", + "optional" : 1, + "pattern" : "[-_.\\w\\d]+", + "type" : "string" + }, + "firewall" : { + "description" : "Controls whether this interface's firewall rules should be used.", + "optional" : 1, + "type" : "boolean" + }, + "gw" : { + "description" : "Default gateway for IPv4 traffic.", + "format" : "ipv4", + "format_description" : "GatewayIPv4", + "optional" : 1, + "type" : "string" + }, + "gw6" : { + "description" : "Default gateway for IPv6 traffic.", + "format" : "ipv6", + "format_description" : "GatewayIPv6", + "optional" : 1, + "type" : "string" + }, + "hwaddr" : { + "description" : "The interface MAC address. This is dynamically allocated by default, but you can set that statically if needed, for example to always have the same link-local IPv6 address. (lxc.network.hwaddr)", + "format" : "mac-addr", + "format_description" : "XX:XX:XX:XX:XX:XX", + "optional" : 1, + "type" : "string", + "verbose_description" : "A common MAC address with the I/G (Individual/Group) bit not set." + }, + "ip" : { + "description" : "IPv4 address in CIDR format.", + "format" : "pve-ipv4-config", + "format_description" : "(IPv4/CIDR|dhcp|manual)", + "optional" : 1, + "type" : "string" + }, + "ip6" : { + "description" : "IPv6 address in CIDR format.", + "format" : "pve-ipv6-config", + "format_description" : "(IPv6/CIDR|auto|dhcp|manual)", + "optional" : 1, + "type" : "string" + }, + "link_down" : { + "description" : "Whether this interface should be disconnected (like pulling the plug).", + "optional" : 1, + "type" : "boolean" + }, + "mtu" : { + "description" : "Maximum transfer unit of the interface. (lxc.network.mtu)", + "maximum" : 65535, + "minimum" : 64, + "optional" : 1, + "type" : "integer" + }, + "name" : { + "description" : "Name of the network device as seen from inside the container. (lxc.network.name)", + "format_description" : "string", + "pattern" : "[-_.\\w\\d]+", + "type" : "string" + }, + "rate" : { + "description" : "Apply rate limiting to the interface", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "tag" : { + "description" : "VLAN tag for this interface.", + "maximum" : 4094, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "trunks" : { + "description" : "VLAN ids to pass through the interface", + "format_description" : "vlanid[;vlanid...]", + "optional" : 1, + "pattern" : "(?^:\\d+(?:;\\d+)*)", + "type" : "string" + }, + "type" : { + "description" : "Network interface type.", + "enum" : [ + "veth" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "onboot" : { + "default" : 0, + "description" : "Specifies whether a container will be started during system bootup.", + "optional" : 1, + "type" : "boolean" + }, + "ostype" : { + "description" : "OS type. This is used to setup configuration inside the container, and corresponds to lxc setup scripts in /usr/share/lxc/config/.common.conf. Value 'unmanaged' can be used to skip and OS specific setup.", + "enum" : [ + "debian", + "devuan", + "ubuntu", + "centos", + "fedora", + "opensuse", + "archlinux", + "alpine", + "gentoo", + "nixos", + "unmanaged" + ], + "optional" : 1, + "type" : "string" + }, + "protection" : { + "default" : 0, + "description" : "Sets the protection flag of the container. This will prevent the CT or CT's disk remove/update operation.", + "optional" : 1, + "type" : "boolean" + }, + "rootfs" : { + "description" : "Use volume as container root.", + "format" : { + "acl" : { + "description" : "Explicitly enable or disable ACL support.", + "optional" : 1, + "type" : "boolean" + }, + "mountoptions" : { + "description" : "Extra mount options for rootfs/mps.", + "format_description" : "opt[;opt...]", + "optional" : 1, + "pattern" : "(?^:(?^:(noatime|lazytime|nodev|nosuid|noexec))(;(?^:(noatime|lazytime|nodev|nosuid|noexec)))*)", + "type" : "string" + }, + "quota" : { + "description" : "Enable user quotas inside the container (not supported with zfs subvolumes)", + "optional" : 1, + "type" : "boolean" + }, + "replicate" : { + "default" : 1, + "description" : "Will include this volume to a storage replica job.", + "optional" : 1, + "type" : "boolean" + }, + "ro" : { + "description" : "Read-only mount point", + "optional" : 1, + "type" : "boolean" + }, + "shared" : { + "default" : 0, + "description" : "Mark this non-volume mount point as available on multiple nodes (see 'nodes')", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this non-volume mount point as available on all nodes.\n\nWARNING: This option does not share the mount point automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Volume size (read only value).", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "volume" : { + "default_key" : 1, + "description" : "Volume, device or directory to mount into the container.", + "format" : "pve-lxc-mp-string", + "format_description" : "volume", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "searchdomain" : { + "description" : "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.", + "format" : "dns-name-list", + "optional" : 1, + "type" : "string" + }, + "startup" : { + "description" : "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped.", + "format" : "pve-startup-order", + "optional" : 1, + "type" : "string", + "typetext" : "[[order=]\\d+] [,up=\\d+] [,down=\\d+] " + }, + "swap" : { + "default" : 512, + "description" : "Amount of SWAP for the container in MB.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "tags" : { + "description" : "Tags of the Container. This is only meta information.", + "format" : "pve-tag-list", + "optional" : 1, + "type" : "string" + }, + "template" : { + "default" : 0, + "description" : "Enable/disable Template.", + "optional" : 1, + "type" : "boolean" + }, + "timezone" : { + "description" : "Time zone to use in the container. If option isn't set, then nothing will be done. Can be set to 'host' to match the host time zone, or an arbitrary time zone option from /usr/share/zoneinfo/zone.tab", + "format" : "pve-ct-timezone", + "optional" : 1, + "type" : "string" + }, + "tty" : { + "default" : 2, + "description" : "Specify the number of tty available to the container", + "maximum" : 6, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "unprivileged" : { + "default" : 0, + "description" : "Makes the container run as unprivileged user. (Should not be modified manually.)", + "optional" : 1, + "type" : "boolean" + }, + "unused[n]" : { + "description" : "Reference to unused volumes. This is used internally, and should not be modified manually.", + "format" : { + "volume" : { + "default_key" : 1, + "description" : "The volume that is not used currently.", + "format" : "pve-volume-id", + "format_description" : "volume", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set container options.", + "method" : "PUT", + "name" : "update_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "arch" : { + "default" : "amd64", + "description" : "OS architecture type.", + "enum" : [ + "amd64", + "i386", + "arm64", + "armhf", + "riscv32", + "riscv64" + ], + "optional" : 1, + "type" : "string" + }, + "cmode" : { + "default" : "tty", + "description" : "Console mode. By default, the console command tries to open a connection to one of the available tty devices. By setting cmode to 'console' it tries to attach to /dev/console instead. If you set cmode to 'shell', it simply invokes a shell inside the container (no login).", + "enum" : [ + "shell", + "console", + "tty" + ], + "optional" : 1, + "type" : "string" + }, + "console" : { + "default" : 1, + "description" : "Attach a console device (/dev/console) to the container.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "cores" : { + "description" : "The number of cores assigned to the container. A container can use all available cores by default.", + "maximum" : 8192, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 8192)" + }, + "cpulimit" : { + "default" : 0, + "description" : "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has a total of '2' CPU time. Value '0' indicates no CPU limit.", + "maximum" : 8192, + "minimum" : 0, + "optional" : 1, + "type" : "number", + "typetext" : " (0 - 8192)" + }, + "cpuunits" : { + "default" : "cgroup v1: 1024, cgroup v2: 100", + "description" : "CPU weight for a container, will be clamped to [1, 10000] in cgroup v2.", + "maximum" : 500000, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 500000)", + "verbose_description" : "CPU weight for a container. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this container gets. Number is relative to the weights of all the other running guests." + }, + "debug" : { + "default" : 0, + "description" : "Try to be more verbose. For now this only enables debug log-level on start.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "description" : { + "description" : "Description for the Container. Shown in the web-interface CT's summary. This is saved as comment inside the configuration file.", + "maxLength" : 8192, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dev[n]" : { + "description" : "Device to pass through to the container", + "format" : { + "gid" : { + "description" : "Group ID to be assigned to the device node", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "mode" : { + "description" : "Access mode to be set on the device node", + "format_description" : "Octal access mode", + "optional" : 1, + "pattern" : "0[0-7]{3}", + "type" : "string" + }, + "path" : { + "default_key" : 1, + "description" : "Device to pass through to the container", + "format" : "pve-lxc-dev-string", + "format_description" : "Path", + "optional" : 1, + "type" : "string", + "verbose_description" : "Path to the device to pass through to the container" + }, + "uid" : { + "description" : "User ID to be assigned to the device node", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[[path=]] [,gid=] [,mode=] [,uid=]" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "features" : { + "description" : "Allow containers access to advanced features.", + "format" : { + "force_rw_sys" : { + "default" : 0, + "description" : "Mount /sys in unprivileged containers as `rw` instead of `mixed`. This can break networking under newer (>= v245) systemd-network use.", + "optional" : 1, + "type" : "boolean" + }, + "fuse" : { + "default" : 0, + "description" : "Allow using 'fuse' file systems in a container. Note that interactions between fuse and the freezer cgroup can potentially cause I/O deadlocks.", + "optional" : 1, + "type" : "boolean" + }, + "keyctl" : { + "default" : 0, + "description" : "For unprivileged containers only: Allow the use of the keyctl() system call. This is required to use docker inside a container. By default unprivileged containers will see this system call as non-existent. This is mostly a workaround for systemd-networkd, as it will treat it as a fatal error when some keyctl() operations are denied by the kernel due to lacking permissions. Essentially, you can choose between running systemd-networkd or docker.", + "optional" : 1, + "type" : "boolean" + }, + "mknod" : { + "default" : 0, + "description" : "Allow unprivileged containers to use mknod() to add certain device nodes. This requires a kernel with seccomp trap to user space support (5.3 or newer). This is experimental.", + "optional" : 1, + "type" : "boolean" + }, + "mount" : { + "description" : "Allow mounting file systems of specific types. This should be a list of file system types as used with the mount command. Note that this can have negative effects on the container's security. With access to a loop device, mounting a file can circumvent the mknod permission of the devices cgroup, mounting an NFS file system can block the host's I/O completely and prevent it from rebooting, etc.", + "format_description" : "fstype;fstype;...", + "optional" : 1, + "pattern" : "(?^:[a-zA-Z0-9_; ]+)", + "type" : "string" + }, + "nesting" : { + "default" : 0, + "description" : "Allow nesting. Best used with unprivileged containers with additional id mapping. Note that this will expose procfs and sysfs contents of the host to the guest.", + "optional" : 1, + "type" : "boolean" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[force_rw_sys=<1|0>] [,fuse=<1|0>] [,keyctl=<1|0>] [,mknod=<1|0>] [,mount=] [,nesting=<1|0>]" + }, + "hookscript" : { + "description" : "Script that will be exectued during various steps in the containers lifetime.", + "format" : "pve-volume-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "hostname" : { + "description" : "Set a host name for the container.", + "format" : "dns-name", + "maxLength" : 255, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "lock" : { + "description" : "Lock/unlock the container.", + "enum" : [ + "backup", + "create", + "destroyed", + "disk", + "fstrim", + "migrate", + "mounted", + "rollback", + "snapshot", + "snapshot-delete" + ], + "optional" : 1, + "type" : "string" + }, + "memory" : { + "default" : 512, + "description" : "Amount of RAM for the container in MB.", + "minimum" : 16, + "optional" : 1, + "type" : "integer", + "typetext" : " (16 - N)" + }, + "mp[n]" : { + "description" : "Use volume as container mount point. Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume.", + "format" : { + "acl" : { + "description" : "Explicitly enable or disable ACL support.", + "optional" : 1, + "type" : "boolean" + }, + "backup" : { + "description" : "Whether to include the mount point in backups.", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Whether to include the mount point in backups (only used for volume mount points)." + }, + "mountoptions" : { + "description" : "Extra mount options for rootfs/mps.", + "format_description" : "opt[;opt...]", + "optional" : 1, + "pattern" : "(?^:(?^:(noatime|lazytime|nodev|nosuid|noexec))(;(?^:(noatime|lazytime|nodev|nosuid|noexec)))*)", + "type" : "string" + }, + "mp" : { + "description" : "Path to the mount point as seen from inside the container (must not contain symlinks).", + "format" : "pve-lxc-mp-string", + "format_description" : "Path", + "type" : "string", + "verbose_description" : "Path to the mount point as seen from inside the container.\n\nNOTE: Must not contain any symlinks for security reasons." + }, + "quota" : { + "description" : "Enable user quotas inside the container (not supported with zfs subvolumes)", + "optional" : 1, + "type" : "boolean" + }, + "replicate" : { + "default" : 1, + "description" : "Will include this volume to a storage replica job.", + "optional" : 1, + "type" : "boolean" + }, + "ro" : { + "description" : "Read-only mount point", + "optional" : 1, + "type" : "boolean" + }, + "shared" : { + "default" : 0, + "description" : "Mark this non-volume mount point as available on multiple nodes (see 'nodes')", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this non-volume mount point as available on all nodes.\n\nWARNING: This option does not share the mount point automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Volume size (read only value).", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "volume" : { + "default_key" : 1, + "description" : "Volume, device or directory to mount into the container.", + "format" : "pve-lxc-mp-string", + "format_description" : "volume", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[volume=] ,mp= [,acl=<1|0>] [,backup=<1|0>] [,mountoptions=] [,quota=<1|0>] [,replicate=<1|0>] [,ro=<1|0>] [,shared=<1|0>] [,size=]" + }, + "nameserver" : { + "description" : "Sets DNS server IP address for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.", + "format" : "lxc-ip-with-ll-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "net[n]" : { + "description" : "Specifies network interfaces for the container.", + "format" : { + "bridge" : { + "description" : "Bridge to attach the network device to.", + "format_description" : "bridge", + "optional" : 1, + "pattern" : "[-_.\\w\\d]+", + "type" : "string" + }, + "firewall" : { + "description" : "Controls whether this interface's firewall rules should be used.", + "optional" : 1, + "type" : "boolean" + }, + "gw" : { + "description" : "Default gateway for IPv4 traffic.", + "format" : "ipv4", + "format_description" : "GatewayIPv4", + "optional" : 1, + "type" : "string" + }, + "gw6" : { + "description" : "Default gateway for IPv6 traffic.", + "format" : "ipv6", + "format_description" : "GatewayIPv6", + "optional" : 1, + "type" : "string" + }, + "hwaddr" : { + "description" : "The interface MAC address. This is dynamically allocated by default, but you can set that statically if needed, for example to always have the same link-local IPv6 address. (lxc.network.hwaddr)", + "format" : "mac-addr", + "format_description" : "XX:XX:XX:XX:XX:XX", + "optional" : 1, + "type" : "string", + "verbose_description" : "A common MAC address with the I/G (Individual/Group) bit not set." + }, + "ip" : { + "description" : "IPv4 address in CIDR format.", + "format" : "pve-ipv4-config", + "format_description" : "(IPv4/CIDR|dhcp|manual)", + "optional" : 1, + "type" : "string" + }, + "ip6" : { + "description" : "IPv6 address in CIDR format.", + "format" : "pve-ipv6-config", + "format_description" : "(IPv6/CIDR|auto|dhcp|manual)", + "optional" : 1, + "type" : "string" + }, + "link_down" : { + "description" : "Whether this interface should be disconnected (like pulling the plug).", + "optional" : 1, + "type" : "boolean" + }, + "mtu" : { + "description" : "Maximum transfer unit of the interface. (lxc.network.mtu)", + "maximum" : 65535, + "minimum" : 64, + "optional" : 1, + "type" : "integer" + }, + "name" : { + "description" : "Name of the network device as seen from inside the container. (lxc.network.name)", + "format_description" : "string", + "pattern" : "[-_.\\w\\d]+", + "type" : "string" + }, + "rate" : { + "description" : "Apply rate limiting to the interface", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "tag" : { + "description" : "VLAN tag for this interface.", + "maximum" : 4094, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "trunks" : { + "description" : "VLAN ids to pass through the interface", + "format_description" : "vlanid[;vlanid...]", + "optional" : 1, + "pattern" : "(?^:\\d+(?:;\\d+)*)", + "type" : "string" + }, + "type" : { + "description" : "Network interface type.", + "enum" : [ + "veth" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "name= [,bridge=] [,firewall=<1|0>] [,gw=] [,gw6=] [,hwaddr=] [,ip=<(IPv4/CIDR|dhcp|manual)>] [,ip6=<(IPv6/CIDR|auto|dhcp|manual)>] [,link_down=<1|0>] [,mtu=] [,rate=] [,tag=] [,trunks=] [,type=]" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "onboot" : { + "default" : 0, + "description" : "Specifies whether a container will be started during system bootup.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ostype" : { + "description" : "OS type. This is used to setup configuration inside the container, and corresponds to lxc setup scripts in /usr/share/lxc/config/.common.conf. Value 'unmanaged' can be used to skip and OS specific setup.", + "enum" : [ + "debian", + "devuan", + "ubuntu", + "centos", + "fedora", + "opensuse", + "archlinux", + "alpine", + "gentoo", + "nixos", + "unmanaged" + ], + "optional" : 1, + "type" : "string" + }, + "protection" : { + "default" : 0, + "description" : "Sets the protection flag of the container. This will prevent the CT or CT's disk remove/update operation.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "revert" : { + "description" : "Revert a pending change.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "rootfs" : { + "description" : "Use volume as container root.", + "format" : { + "acl" : { + "description" : "Explicitly enable or disable ACL support.", + "optional" : 1, + "type" : "boolean" + }, + "mountoptions" : { + "description" : "Extra mount options for rootfs/mps.", + "format_description" : "opt[;opt...]", + "optional" : 1, + "pattern" : "(?^:(?^:(noatime|lazytime|nodev|nosuid|noexec))(;(?^:(noatime|lazytime|nodev|nosuid|noexec)))*)", + "type" : "string" + }, + "quota" : { + "description" : "Enable user quotas inside the container (not supported with zfs subvolumes)", + "optional" : 1, + "type" : "boolean" + }, + "replicate" : { + "default" : 1, + "description" : "Will include this volume to a storage replica job.", + "optional" : 1, + "type" : "boolean" + }, + "ro" : { + "description" : "Read-only mount point", + "optional" : 1, + "type" : "boolean" + }, + "shared" : { + "default" : 0, + "description" : "Mark this non-volume mount point as available on multiple nodes (see 'nodes')", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this non-volume mount point as available on all nodes.\n\nWARNING: This option does not share the mount point automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Volume size (read only value).", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "volume" : { + "default_key" : 1, + "description" : "Volume, device or directory to mount into the container.", + "format" : "pve-lxc-mp-string", + "format_description" : "volume", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[volume=] [,acl=<1|0>] [,mountoptions=] [,quota=<1|0>] [,replicate=<1|0>] [,ro=<1|0>] [,shared=<1|0>] [,size=]" + }, + "searchdomain" : { + "description" : "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.", + "format" : "dns-name-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "startup" : { + "description" : "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped.", + "format" : "pve-startup-order", + "optional" : 1, + "type" : "string", + "typetext" : "[[order=]\\d+] [,up=\\d+] [,down=\\d+] " + }, + "swap" : { + "default" : 512, + "description" : "Amount of SWAP for the container in MB.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "tags" : { + "description" : "Tags of the Container. This is only meta information.", + "format" : "pve-tag-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "template" : { + "default" : 0, + "description" : "Enable/disable Template.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "timezone" : { + "description" : "Time zone to use in the container. If option isn't set, then nothing will be done. Can be set to 'host' to match the host time zone, or an arbitrary time zone option from /usr/share/zoneinfo/zone.tab", + "format" : "pve-ct-timezone", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tty" : { + "default" : 2, + "description" : "Specify the number of tty available to the container", + "maximum" : 6, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 6)" + }, + "unprivileged" : { + "default" : 0, + "description" : "Makes the container run as unprivileged user. (Should not be modified manually.)", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "unused[n]" : { + "description" : "Reference to unused volumes. This is used internally, and should not be modified manually.", + "format" : { + "volume" : { + "default_key" : 1, + "description" : "The volume that is not used currently.", + "format" : "pve-volume-id", + "format_description" : "volume", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[volume=]" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Disk", + "VM.Config.CPU", + "VM.Config.Memory", + "VM.Config.Network", + "VM.Config.Options" + ], + "any", + 1 + ], + "description" : "non-volume mount points in rootfs and mp[n] are restricted to root@pam" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/config", + "text" : "config" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get virtual machine status.", + "method" : "GET", + "name" : "vm_status", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "cpus" : { + "description" : "Maximum usable CPUs.", + "optional" : 1, + "type" : "number" + }, + "ha" : { + "description" : "HA manager service status.", + "type" : "object" + }, + "lock" : { + "description" : "The current config lock, if any.", + "optional" : 1, + "type" : "string" + }, + "maxdisk" : { + "description" : "Root disk size in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "maxmem" : { + "description" : "Maximum memory in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "maxswap" : { + "description" : "Maximum SWAP memory in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "name" : { + "description" : "Container name.", + "optional" : 1, + "type" : "string" + }, + "status" : { + "description" : "LXC Container status.", + "enum" : [ + "stopped", + "running" + ], + "type" : "string" + }, + "tags" : { + "description" : "The current configured tags, if any.", + "optional" : 1, + "type" : "string" + }, + "uptime" : { + "description" : "Uptime.", + "optional" : 1, + "renderer" : "duration", + "type" : "integer" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/status/current", + "text" : "current" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Start the container.", + "method" : "POST", + "name" : "vm_start", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "debug" : { + "default" : 0, + "description" : "If set, enables very verbose debug log-level on start.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/status/start", + "text" : "start" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Stop the container. This will abruptly stop all processes running in the container.", + "method" : "POST", + "name" : "vm_stop", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "skiplock" : { + "description" : "Ignore locks - only root is allowed to use this option.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/status/stop", + "text" : "stop" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Shutdown the container. This will trigger a clean shutdown of the container, see lxc-stop(1) for details.", + "method" : "POST", + "name" : "vm_shutdown", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "forceStop" : { + "default" : 0, + "description" : "Make sure the Container stops.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "timeout" : { + "default" : 60, + "description" : "Wait maximal timeout seconds.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/status/shutdown", + "text" : "shutdown" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Suspend the container. This is experimental.", + "method" : "POST", + "name" : "vm_suspend", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/status/suspend", + "text" : "suspend" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Resume the container.", + "method" : "POST", + "name" : "vm_resume", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/status/resume", + "text" : "resume" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Reboot the container by shutting it down, and starting it again. Applies pending changes.", + "method" : "POST", + "name" : "vm_reboot", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "timeout" : { + "description" : "Wait maximal timeout seconds for the shutdown.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/status/reboot", + "text" : "reboot" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index", + "method" : "GET", + "name" : "vmcmdidx", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "subdir" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{subdir}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/lxc/{vmid}/status", + "text" : "status" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Rollback LXC state to specified snapshot.", + "method" : "POST", + "name" : "rollback", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "start" : { + "default" : 0, + "description" : "Whether the container should get started after rolling back successfully", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Snapshot", + "VM.Snapshot.Rollback" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/snapshot/{snapname}/rollback", + "text" : "rollback" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get snapshot configuration", + "method" : "GET", + "name" : "get_snapshot_config", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Snapshot", + "VM.Snapshot.Rollback", + "VM.Audit" + ], + "any", + 1 + ] + }, + "proxyto" : "node", + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update snapshot metadata.", + "method" : "PUT", + "name" : "update_snapshot_config", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "description" : { + "description" : "A textual description or comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Snapshot" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/snapshot/{snapname}/config", + "text" : "config" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete a LXC snapshot.", + "method" : "DELETE", + "name" : "delsnapshot", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "description" : "For removal from config file, even if removing disk snapshots fails.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Snapshot" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "", + "method" : "GET", + "name" : "snapshot_cmd_idx", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{cmd}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/lxc/{vmid}/snapshot/{snapname}", + "text" : "{snapname}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List all snapshots.", + "method" : "GET", + "name" : "list", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "description" : { + "description" : "Snapshot description.", + "type" : "string" + }, + "name" : { + "description" : "Snapshot identifier. Value 'current' identifies the current VM.", + "type" : "string" + }, + "parent" : { + "description" : "Parent snapshot identifier.", + "optional" : 1, + "type" : "string" + }, + "snaptime" : { + "description" : "Snapshot creation time", + "optional" : 1, + "renderer" : "timestamp", + "type" : "integer" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Snapshot a container.", + "method" : "POST", + "name" : "snapshot", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "description" : { + "description" : "A textual description or comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Snapshot" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/lxc/{vmid}/snapshot", + "text" : "snapshot" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete rule.", + "method" : "DELETE", + "name" : "delete_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get single rule data.", + "method" : "GET", + "name" : "get_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : null, + "returns" : { + "properties" : { + "action" : { + "type" : "string" + }, + "comment" : { + "optional" : 1, + "type" : "string" + }, + "dest" : { + "optional" : 1, + "type" : "string" + }, + "dport" : { + "optional" : 1, + "type" : "string" + }, + "enable" : { + "optional" : 1, + "type" : "integer" + }, + "icmp-type" : { + "optional" : 1, + "type" : "string" + }, + "iface" : { + "optional" : 1, + "type" : "string" + }, + "ipversion" : { + "optional" : 1, + "type" : "integer" + }, + "log" : { + "description" : "Log level for firewall rule", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "optional" : 1, + "type" : "string" + }, + "pos" : { + "type" : "integer" + }, + "proto" : { + "optional" : 1, + "type" : "string" + }, + "source" : { + "optional" : 1, + "type" : "string" + }, + "sport" : { + "optional" : 1, + "type" : "string" + }, + "type" : { + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Modify rule data.", + "method" : "PUT", + "name" : "update_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "action" : { + "description" : "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "comment" : { + "description" : "Descriptive comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dest" : { + "description" : "Restrict packet destination address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dport" : { + "description" : "Restrict TCP/UDP destination port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-dport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Flag to enable/disable a rule.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "icmp-type" : { + "description" : "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'.", + "format" : "pve-fw-icmp-type-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name. You have to use network configuration key names for VMs and containers ('net\\d+'). Host related rules can use arbitrary strings.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "log" : { + "description" : "Log level for firewall rule.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "description" : "Use predefined standard macro.", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "moveto" : { + "description" : "Move rule to new position . Other arguments are ignored.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "proto" : { + "description" : "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.", + "format" : "pve-fw-protocol-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "Restrict packet source address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sport" : { + "description" : "Restrict TCP/UDP source port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-sport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Rule type.", + "enum" : [ + "in", + "out", + "group" + ], + "optional" : 1, + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/firewall/rules/{pos}", + "text" : "{pos}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List rules.", + "method" : "GET", + "name" : "get_rules", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : null, + "returns" : { + "items" : { + "properties" : { + "pos" : { + "type" : "integer" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{pos}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new rule.", + "method" : "POST", + "name" : "create_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "action" : { + "description" : "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.", + "maxLength" : 20, + "minLength" : 2, + "optional" : 0, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "comment" : { + "description" : "Descriptive comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dest" : { + "description" : "Restrict packet destination address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dport" : { + "description" : "Restrict TCP/UDP destination port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-dport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Flag to enable/disable a rule.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "icmp-type" : { + "description" : "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'.", + "format" : "pve-fw-icmp-type-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name. You have to use network configuration key names for VMs and containers ('net\\d+'). Host related rules can use arbitrary strings.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "log" : { + "description" : "Log level for firewall rule.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "description" : "Use predefined standard macro.", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "proto" : { + "description" : "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.", + "format" : "pve-fw-protocol-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "Restrict packet source address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sport" : { + "description" : "Restrict TCP/UDP source port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-sport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Rule type.", + "enum" : [ + "in", + "out", + "group" + ], + "optional" : 0, + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "proxyto" : null, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/lxc/{vmid}/firewall/rules", + "text" : "rules" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove IP or Network alias.", + "method" : "DELETE", + "name" : "remove_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read alias.", + "method" : "GET", + "name" : "read_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update IP or Network alias.", + "method" : "PUT", + "name" : "update_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDR", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "rename" : { + "description" : "Rename an existing alias.", + "maxLength" : 64, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/firewall/aliases/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List aliases", + "method" : "GET", + "name" : "get_aliases", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "cidr" : { + "type" : "string" + }, + "comment" : { + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 0, + "type" : "string" + }, + "name" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create IP or Network Alias.", + "method" : "POST", + "name" : "create_alias", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDR", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "Alias name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/lxc/{vmid}/firewall/aliases", + "text" : "aliases" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove IP or Network from IPSet.", + "method" : "DELETE", + "name" : "remove_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read IP or Network settings from IPSet.", + "method" : "GET", + "name" : "read_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update IP or Network settings", + "method" : "PUT", + "name" : "update_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "nomatch" : { + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/firewall/ipset/{name}/{cidr}", + "text" : "{cidr}" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete IPSet", + "method" : "DELETE", + "name" : "delete_ipset", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "description" : "Delete all members of the IPSet, if there are any.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "List IPSet content", + "method" : "GET", + "name" : "get_ipset", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "cidr" : { + "type" : "string" + }, + "comment" : { + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 0, + "type" : "string" + }, + "nomatch" : { + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{cidr}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Add IP or Network to IPSet.", + "method" : "POST", + "name" : "create_ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cidr" : { + "description" : "Network/IP specification in CIDR format.", + "format" : "IPorCIDRorAlias", + "type" : "string", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "nomatch" : { + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/lxc/{vmid}/firewall/ipset/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List IPSets", + "method" : "GET", + "name" : "ipset_index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 0, + "type" : "string" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new IPSet", + "method" : "POST", + "name" : "create_ipset", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "IP set name.", + "maxLength" : 64, + "minLength" : 2, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "rename" : { + "description" : "Rename an existing IPSet. You can set 'rename' to the same value as 'name' to update the 'comment' of an existing IPSet.", + "maxLength" : 64, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/lxc/{vmid}/firewall/ipset", + "text" : "ipset" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get VM firewall options.", + "method" : "GET", + "name" : "get_options", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "properties" : { + "dhcp" : { + "default" : 0, + "description" : "Enable DHCP.", + "optional" : 1, + "type" : "boolean" + }, + "enable" : { + "default" : 0, + "description" : "Enable/disable firewall rules.", + "optional" : 1, + "type" : "boolean" + }, + "ipfilter" : { + "description" : "Enable default IP filters. This is equivalent to adding an empty ipfilter-net ipset for every interface. Such ipsets implicitly contain sane default restrictions such as restricting IPv6 link local addresses to the one derived from the interface's MAC address. For containers the configured IP addresses will be implicitly added.", + "optional" : 1, + "type" : "boolean" + }, + "log_level_in" : { + "description" : "Log level for incoming traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "log_level_out" : { + "description" : "Log level for outgoing traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macfilter" : { + "default" : 1, + "description" : "Enable/disable MAC address filter.", + "optional" : 1, + "type" : "boolean" + }, + "ndp" : { + "default" : 0, + "description" : "Enable NDP (Neighbor Discovery Protocol).", + "optional" : 1, + "type" : "boolean" + }, + "policy_in" : { + "description" : "Input policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + }, + "policy_out" : { + "description" : "Output policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + }, + "radv" : { + "description" : "Allow sending Router Advertisement.", + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set Firewall options.", + "method" : "PUT", + "name" : "set_options", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dhcp" : { + "default" : 0, + "description" : "Enable DHCP.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "default" : 0, + "description" : "Enable/disable firewall rules.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ipfilter" : { + "description" : "Enable default IP filters. This is equivalent to adding an empty ipfilter-net ipset for every interface. Such ipsets implicitly contain sane default restrictions such as restricting IPv6 link local addresses to the one derived from the interface's MAC address. For containers the configured IP addresses will be implicitly added.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "log_level_in" : { + "description" : "Log level for incoming traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "log_level_out" : { + "description" : "Log level for outgoing traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macfilter" : { + "default" : 1, + "description" : "Enable/disable MAC address filter.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ndp" : { + "default" : 0, + "description" : "Enable NDP (Neighbor Discovery Protocol).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "policy_in" : { + "description" : "Input policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + }, + "policy_out" : { + "description" : "Output policy.", + "enum" : [ + "ACCEPT", + "REJECT", + "DROP" + ], + "optional" : 1, + "type" : "string" + }, + "radv" : { + "description" : "Allow sending Router Advertisement.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Network" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/firewall/options", + "text" : "options" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read firewall log", + "method" : "GET", + "name" : "log", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "limit" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "since" : { + "description" : "Display log since this UNIX epoch.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "start" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "until" : { + "description" : "Display log until this UNIX epoch.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Console" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "n" : { + "description" : "Line number", + "type" : "integer" + }, + "t" : { + "description" : "Line text", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/firewall/log", + "text" : "log" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Lists possible IPSet/Alias reference which are allowed in source/dest properties.", + "method" : "GET", + "name" : "refs", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Only list references of specified type.", + "enum" : [ + "alias", + "ipset" + ], + "optional" : 1, + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "name" : { + "type" : "string" + }, + "ref" : { + "type" : "string" + }, + "scope" : { + "type" : "string" + }, + "type" : { + "enum" : [ + "alias", + "ipset" + ], + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/firewall/refs", + "text" : "refs" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/lxc/{vmid}/firewall", + "text" : "firewall" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read VM RRD statistics (returns PNG)", + "method" : "GET", + "name" : "rrd", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cf" : { + "description" : "The RRD consolidation function", + "enum" : [ + "AVERAGE", + "MAX" + ], + "optional" : 1, + "type" : "string" + }, + "ds" : { + "description" : "The list of datasources you want to display.", + "format" : "pve-configid-list", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "timeframe" : { + "description" : "Specify the time frame you are interested in.", + "enum" : [ + "hour", + "day", + "week", + "month", + "year" + ], + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "properties" : { + "filename" : { + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/rrd", + "text" : "rrd" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read VM RRD statistics", + "method" : "GET", + "name" : "rrddata", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cf" : { + "description" : "The RRD consolidation function", + "enum" : [ + "AVERAGE", + "MAX" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "timeframe" : { + "description" : "Specify the time frame you are interested in.", + "enum" : [ + "hour", + "day", + "week", + "month", + "year" + ], + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/rrddata", + "text" : "rrddata" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Creates a TCP VNC proxy connections.", + "method" : "POST", + "name" : "vncproxy", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "height" : { + "description" : "sets the height of the console in pixels.", + "maximum" : 2160, + "minimum" : 16, + "optional" : 1, + "type" : "integer", + "typetext" : " (16 - 2160)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "websocket" : { + "description" : "use websocket instead of standard VNC.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "width" : { + "description" : "sets the width of the console in pixels.", + "maximum" : 4096, + "minimum" : 16, + "optional" : 1, + "type" : "integer", + "typetext" : " (16 - 4096)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "cert" : { + "type" : "string" + }, + "port" : { + "type" : "integer" + }, + "ticket" : { + "type" : "string" + }, + "upid" : { + "type" : "string" + }, + "user" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/vncproxy", + "text" : "vncproxy" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Creates a TCP proxy connection.", + "method" : "POST", + "name" : "termproxy", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "port" : { + "type" : "integer" + }, + "ticket" : { + "type" : "string" + }, + "upid" : { + "type" : "string" + }, + "user" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/termproxy", + "text" : "termproxy" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Opens a weksocket for VNC traffic.", + "method" : "GET", + "name" : "vncwebsocket", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "port" : { + "description" : "Port number returned by previous vncproxy call.", + "maximum" : 5999, + "minimum" : 5900, + "type" : "integer", + "typetext" : " (5900 - 5999)" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "vncticket" : { + "description" : "Ticket from previous call to vncproxy.", + "maxLength" : 512, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Console" + ] + ], + "description" : "You also need to pass a valid ticket (vncticket)." + }, + "returns" : { + "properties" : { + "port" : { + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/vncwebsocket", + "text" : "vncwebsocket" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Returns a SPICE configuration to connect to the CT.", + "method" : "POST", + "name" : "spiceproxy", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "proxy" : { + "description" : "SPICE proxy server. This can be used by the client to specify the proxy server. All nodes in a cluster runs 'spiceproxy', so it is up to the client to choose one. By default, we return the node where the VM is currently running. As reasonable setting is to use same node you use to connect to the API (This is window.location.hostname for the JS GUI).", + "format" : "address", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Console" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "additionalProperties" : 1, + "description" : "Returned values can be directly passed to the 'remote-viewer' application.", + "properties" : { + "host" : { + "type" : "string" + }, + "password" : { + "type" : "string" + }, + "proxy" : { + "type" : "string" + }, + "tls-port" : { + "type" : "integer" + }, + "type" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/spiceproxy", + "text" : "spiceproxy" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Migrate the container to another cluster. Creates a new migration task. EXPERIMENTAL feature!", + "method" : "POST", + "name" : "remote_migrate_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "bwlimit" : { + "default" : "migrate limit from datacenter or storage config", + "description" : "Override I/O bandwidth limit (in KiB/s).", + "minimum" : "0", + "optional" : 1, + "type" : "number", + "typetext" : " (0 - N)" + }, + "delete" : { + "default" : 0, + "description" : "Delete the original CT and related data after successful migration. By default the original CT is kept on the source cluster in a stopped state.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "online" : { + "description" : "Use online/live migration.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "restart" : { + "description" : "Use restart migration", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "target-bridge" : { + "description" : "Mapping from source to target bridges. Providing only a single bridge ID maps all source bridges to that bridge. Providing the special value '1' will map each source bridge to itself.", + "format" : "bridge-pair-list", + "type" : "string", + "typetext" : "" + }, + "target-endpoint" : { + "description" : "Remote target endpoint", + "format" : "proxmox-remote", + "type" : "string", + "typetext" : "apitoken= ,host= [,fingerprint=] [,port=]" + }, + "target-storage" : { + "description" : "Mapping from source to target storages. Providing only a single storage ID maps all source storages to that storage. Providing the special value '1' will map each source storage to itself.", + "format" : "storage-pair-list", + "optional" : 0, + "type" : "string", + "typetext" : "" + }, + "target-vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "timeout" : { + "default" : 180, + "description" : "Timeout in seconds for shutdown for restart migration", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Migrate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/remote_migrate", + "text" : "remote_migrate" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Migrate the container to another node. Creates a new migration task.", + "method" : "POST", + "name" : "migrate_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "bwlimit" : { + "default" : "migrate limit from datacenter or storage config", + "description" : "Override I/O bandwidth limit (in KiB/s).", + "minimum" : "0", + "optional" : 1, + "type" : "number", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "online" : { + "description" : "Use online/live migration.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "restart" : { + "description" : "Use restart migration", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "target" : { + "description" : "Target node.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "target-storage" : { + "description" : "Mapping from source to target storages. Providing only a single storage ID maps all source storages to that storage. Providing the special value '1' will map each source storage to itself.", + "format" : "storage-pair-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "timeout" : { + "default" : 180, + "description" : "Timeout in seconds for shutdown for restart migration", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Migrate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/migrate", + "text" : "migrate" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Check if feature for virtual machine is available.", + "method" : "GET", + "name" : "vm_feature", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "feature" : { + "description" : "Feature to check.", + "enum" : [ + "snapshot", + "clone", + "copy" + ], + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "hasFeature" : { + "type" : "boolean" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/feature", + "text" : "feature" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Create a Template.", + "method" : "POST", + "name" : "template", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Allocate" + ] + ], + "description" : "You need 'VM.Allocate' permissions on /vms/{vmid}" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/template", + "text" : "template" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Create a container clone/copy", + "method" : "POST", + "name" : "clone_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "bwlimit" : { + "default" : "clone limit from datacenter or storage config", + "description" : "Override I/O bandwidth limit (in KiB/s).", + "minimum" : "0", + "optional" : 1, + "type" : "number", + "typetext" : " (0 - N)" + }, + "description" : { + "description" : "Description for the new CT.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "full" : { + "description" : "Create a full copy of all disks. This is always done when you clone a normal CT. For CT templates, we try to create a linked clone by default.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "hostname" : { + "description" : "Set a hostname for the new CT.", + "format" : "dns-name", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "newid" : { + "description" : "VMID for the clone.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pool" : { + "description" : "Add the new CT to the specified pool.", + "format" : "pve-poolid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "snapname" : { + "description" : "The name of the snapshot.", + "format" : "pve-configid", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "Target storage for full clone.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "target" : { + "description" : "Target node. Only allowed if the original VM is on shared storage.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "and", + [ + "perm", + "/vms/{vmid}", + [ + "VM.Clone" + ] + ], + [ + "or", + [ + "perm", + "/vms/{newid}", + [ + "VM.Allocate" + ] + ], + [ + "perm", + "/pool/{pool}", + [ + "VM.Allocate" + ], + "require_param", + "pool" + ] + ] + ], + "description" : "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions on /vms/{newid} (or on the VM pool /pool/{pool}). You also need 'Datastore.AllocateSpace' on any used storage, and 'SDN.Use' on any bridge." + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/clone", + "text" : "clone" + }, + { + "info" : { + "PUT" : { + "allowtoken" : 1, + "description" : "Resize a container mount point.", + "method" : "PUT", + "name" : "resize_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disk" : { + "description" : "The disk you want to resize.", + "enum" : [ + "rootfs", + "mp0", + "mp1", + "mp2", + "mp3", + "mp4", + "mp5", + "mp6", + "mp7", + "mp8", + "mp9", + "mp10", + "mp11", + "mp12", + "mp13", + "mp14", + "mp15", + "mp16", + "mp17", + "mp18", + "mp19", + "mp20", + "mp21", + "mp22", + "mp23", + "mp24", + "mp25", + "mp26", + "mp27", + "mp28", + "mp29", + "mp30", + "mp31", + "mp32", + "mp33", + "mp34", + "mp35", + "mp36", + "mp37", + "mp38", + "mp39", + "mp40", + "mp41", + "mp42", + "mp43", + "mp44", + "mp45", + "mp46", + "mp47", + "mp48", + "mp49", + "mp50", + "mp51", + "mp52", + "mp53", + "mp54", + "mp55", + "mp56", + "mp57", + "mp58", + "mp59", + "mp60", + "mp61", + "mp62", + "mp63", + "mp64", + "mp65", + "mp66", + "mp67", + "mp68", + "mp69", + "mp70", + "mp71", + "mp72", + "mp73", + "mp74", + "mp75", + "mp76", + "mp77", + "mp78", + "mp79", + "mp80", + "mp81", + "mp82", + "mp83", + "mp84", + "mp85", + "mp86", + "mp87", + "mp88", + "mp89", + "mp90", + "mp91", + "mp92", + "mp93", + "mp94", + "mp95", + "mp96", + "mp97", + "mp98", + "mp99", + "mp100", + "mp101", + "mp102", + "mp103", + "mp104", + "mp105", + "mp106", + "mp107", + "mp108", + "mp109", + "mp110", + "mp111", + "mp112", + "mp113", + "mp114", + "mp115", + "mp116", + "mp117", + "mp118", + "mp119", + "mp120", + "mp121", + "mp122", + "mp123", + "mp124", + "mp125", + "mp126", + "mp127", + "mp128", + "mp129", + "mp130", + "mp131", + "mp132", + "mp133", + "mp134", + "mp135", + "mp136", + "mp137", + "mp138", + "mp139", + "mp140", + "mp141", + "mp142", + "mp143", + "mp144", + "mp145", + "mp146", + "mp147", + "mp148", + "mp149", + "mp150", + "mp151", + "mp152", + "mp153", + "mp154", + "mp155", + "mp156", + "mp157", + "mp158", + "mp159", + "mp160", + "mp161", + "mp162", + "mp163", + "mp164", + "mp165", + "mp166", + "mp167", + "mp168", + "mp169", + "mp170", + "mp171", + "mp172", + "mp173", + "mp174", + "mp175", + "mp176", + "mp177", + "mp178", + "mp179", + "mp180", + "mp181", + "mp182", + "mp183", + "mp184", + "mp185", + "mp186", + "mp187", + "mp188", + "mp189", + "mp190", + "mp191", + "mp192", + "mp193", + "mp194", + "mp195", + "mp196", + "mp197", + "mp198", + "mp199", + "mp200", + "mp201", + "mp202", + "mp203", + "mp204", + "mp205", + "mp206", + "mp207", + "mp208", + "mp209", + "mp210", + "mp211", + "mp212", + "mp213", + "mp214", + "mp215", + "mp216", + "mp217", + "mp218", + "mp219", + "mp220", + "mp221", + "mp222", + "mp223", + "mp224", + "mp225", + "mp226", + "mp227", + "mp228", + "mp229", + "mp230", + "mp231", + "mp232", + "mp233", + "mp234", + "mp235", + "mp236", + "mp237", + "mp238", + "mp239", + "mp240", + "mp241", + "mp242", + "mp243", + "mp244", + "mp245", + "mp246", + "mp247", + "mp248", + "mp249", + "mp250", + "mp251", + "mp252", + "mp253", + "mp254", + "mp255" + ], + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "size" : { + "description" : "The new size. With the '+' sign the value is added to the actual size of the volume and without it, the value is taken as an absolute one. Shrinking disk size is not supported.", + "pattern" : "\\+?\\d+(\\.\\d+)?[KMGT]?", + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Disk" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "the task ID.", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/resize", + "text" : "resize" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Move a rootfs-/mp-volume to a different storage or to a different container.", + "method" : "POST", + "name" : "move_volume", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "bwlimit" : { + "default" : "clone limit from datacenter or storage config", + "description" : "Override I/O bandwidth limit (in KiB/s).", + "minimum" : "0", + "optional" : 1, + "type" : "number", + "typetext" : " (0 - N)" + }, + "delete" : { + "default" : 0, + "description" : "Delete the original volume after successful copy. By default the original is kept as an unused volume entry.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has different SHA1 \" .\n\t\t \"digest. This can be used to prevent concurrent modifications.", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "Target Storage.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "target-digest" : { + "description" : "Prevent changes if current configuration file of the target \" .\n\t\t \"container has a different SHA1 digest. This can be used to prevent \" .\n\t\t \"concurrent modifications.", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "target-vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "target-volume" : { + "description" : "The config key the volume will be moved to. Default is the source volume key.", + "enum" : [ + "rootfs", + "mp0", + "mp1", + "mp2", + "mp3", + "mp4", + "mp5", + "mp6", + "mp7", + "mp8", + "mp9", + "mp10", + "mp11", + "mp12", + "mp13", + "mp14", + "mp15", + "mp16", + "mp17", + "mp18", + "mp19", + "mp20", + "mp21", + "mp22", + "mp23", + "mp24", + "mp25", + "mp26", + "mp27", + "mp28", + "mp29", + "mp30", + "mp31", + "mp32", + "mp33", + "mp34", + "mp35", + "mp36", + "mp37", + "mp38", + "mp39", + "mp40", + "mp41", + "mp42", + "mp43", + "mp44", + "mp45", + "mp46", + "mp47", + "mp48", + "mp49", + "mp50", + "mp51", + "mp52", + "mp53", + "mp54", + "mp55", + "mp56", + "mp57", + "mp58", + "mp59", + "mp60", + "mp61", + "mp62", + "mp63", + "mp64", + "mp65", + "mp66", + "mp67", + "mp68", + "mp69", + "mp70", + "mp71", + "mp72", + "mp73", + "mp74", + "mp75", + "mp76", + "mp77", + "mp78", + "mp79", + "mp80", + "mp81", + "mp82", + "mp83", + "mp84", + "mp85", + "mp86", + "mp87", + "mp88", + "mp89", + "mp90", + "mp91", + "mp92", + "mp93", + "mp94", + "mp95", + "mp96", + "mp97", + "mp98", + "mp99", + "mp100", + "mp101", + "mp102", + "mp103", + "mp104", + "mp105", + "mp106", + "mp107", + "mp108", + "mp109", + "mp110", + "mp111", + "mp112", + "mp113", + "mp114", + "mp115", + "mp116", + "mp117", + "mp118", + "mp119", + "mp120", + "mp121", + "mp122", + "mp123", + "mp124", + "mp125", + "mp126", + "mp127", + "mp128", + "mp129", + "mp130", + "mp131", + "mp132", + "mp133", + "mp134", + "mp135", + "mp136", + "mp137", + "mp138", + "mp139", + "mp140", + "mp141", + "mp142", + "mp143", + "mp144", + "mp145", + "mp146", + "mp147", + "mp148", + "mp149", + "mp150", + "mp151", + "mp152", + "mp153", + "mp154", + "mp155", + "mp156", + "mp157", + "mp158", + "mp159", + "mp160", + "mp161", + "mp162", + "mp163", + "mp164", + "mp165", + "mp166", + "mp167", + "mp168", + "mp169", + "mp170", + "mp171", + "mp172", + "mp173", + "mp174", + "mp175", + "mp176", + "mp177", + "mp178", + "mp179", + "mp180", + "mp181", + "mp182", + "mp183", + "mp184", + "mp185", + "mp186", + "mp187", + "mp188", + "mp189", + "mp190", + "mp191", + "mp192", + "mp193", + "mp194", + "mp195", + "mp196", + "mp197", + "mp198", + "mp199", + "mp200", + "mp201", + "mp202", + "mp203", + "mp204", + "mp205", + "mp206", + "mp207", + "mp208", + "mp209", + "mp210", + "mp211", + "mp212", + "mp213", + "mp214", + "mp215", + "mp216", + "mp217", + "mp218", + "mp219", + "mp220", + "mp221", + "mp222", + "mp223", + "mp224", + "mp225", + "mp226", + "mp227", + "mp228", + "mp229", + "mp230", + "mp231", + "mp232", + "mp233", + "mp234", + "mp235", + "mp236", + "mp237", + "mp238", + "mp239", + "mp240", + "mp241", + "mp242", + "mp243", + "mp244", + "mp245", + "mp246", + "mp247", + "mp248", + "mp249", + "mp250", + "mp251", + "mp252", + "mp253", + "mp254", + "mp255", + "unused0", + "unused1", + "unused2", + "unused3", + "unused4", + "unused5", + "unused6", + "unused7", + "unused8", + "unused9", + "unused10", + "unused11", + "unused12", + "unused13", + "unused14", + "unused15", + "unused16", + "unused17", + "unused18", + "unused19", + "unused20", + "unused21", + "unused22", + "unused23", + "unused24", + "unused25", + "unused26", + "unused27", + "unused28", + "unused29", + "unused30", + "unused31", + "unused32", + "unused33", + "unused34", + "unused35", + "unused36", + "unused37", + "unused38", + "unused39", + "unused40", + "unused41", + "unused42", + "unused43", + "unused44", + "unused45", + "unused46", + "unused47", + "unused48", + "unused49", + "unused50", + "unused51", + "unused52", + "unused53", + "unused54", + "unused55", + "unused56", + "unused57", + "unused58", + "unused59", + "unused60", + "unused61", + "unused62", + "unused63", + "unused64", + "unused65", + "unused66", + "unused67", + "unused68", + "unused69", + "unused70", + "unused71", + "unused72", + "unused73", + "unused74", + "unused75", + "unused76", + "unused77", + "unused78", + "unused79", + "unused80", + "unused81", + "unused82", + "unused83", + "unused84", + "unused85", + "unused86", + "unused87", + "unused88", + "unused89", + "unused90", + "unused91", + "unused92", + "unused93", + "unused94", + "unused95", + "unused96", + "unused97", + "unused98", + "unused99", + "unused100", + "unused101", + "unused102", + "unused103", + "unused104", + "unused105", + "unused106", + "unused107", + "unused108", + "unused109", + "unused110", + "unused111", + "unused112", + "unused113", + "unused114", + "unused115", + "unused116", + "unused117", + "unused118", + "unused119", + "unused120", + "unused121", + "unused122", + "unused123", + "unused124", + "unused125", + "unused126", + "unused127", + "unused128", + "unused129", + "unused130", + "unused131", + "unused132", + "unused133", + "unused134", + "unused135", + "unused136", + "unused137", + "unused138", + "unused139", + "unused140", + "unused141", + "unused142", + "unused143", + "unused144", + "unused145", + "unused146", + "unused147", + "unused148", + "unused149", + "unused150", + "unused151", + "unused152", + "unused153", + "unused154", + "unused155", + "unused156", + "unused157", + "unused158", + "unused159", + "unused160", + "unused161", + "unused162", + "unused163", + "unused164", + "unused165", + "unused166", + "unused167", + "unused168", + "unused169", + "unused170", + "unused171", + "unused172", + "unused173", + "unused174", + "unused175", + "unused176", + "unused177", + "unused178", + "unused179", + "unused180", + "unused181", + "unused182", + "unused183", + "unused184", + "unused185", + "unused186", + "unused187", + "unused188", + "unused189", + "unused190", + "unused191", + "unused192", + "unused193", + "unused194", + "unused195", + "unused196", + "unused197", + "unused198", + "unused199", + "unused200", + "unused201", + "unused202", + "unused203", + "unused204", + "unused205", + "unused206", + "unused207", + "unused208", + "unused209", + "unused210", + "unused211", + "unused212", + "unused213", + "unused214", + "unused215", + "unused216", + "unused217", + "unused218", + "unused219", + "unused220", + "unused221", + "unused222", + "unused223", + "unused224", + "unused225", + "unused226", + "unused227", + "unused228", + "unused229", + "unused230", + "unused231", + "unused232", + "unused233", + "unused234", + "unused235", + "unused236", + "unused237", + "unused238", + "unused239", + "unused240", + "unused241", + "unused242", + "unused243", + "unused244", + "unused245", + "unused246", + "unused247", + "unused248", + "unused249", + "unused250", + "unused251", + "unused252", + "unused253", + "unused254", + "unused255" + ], + "optional" : 1, + "type" : "string" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "volume" : { + "description" : "Volume which will be moved.", + "enum" : [ + "rootfs", + "mp0", + "mp1", + "mp2", + "mp3", + "mp4", + "mp5", + "mp6", + "mp7", + "mp8", + "mp9", + "mp10", + "mp11", + "mp12", + "mp13", + "mp14", + "mp15", + "mp16", + "mp17", + "mp18", + "mp19", + "mp20", + "mp21", + "mp22", + "mp23", + "mp24", + "mp25", + "mp26", + "mp27", + "mp28", + "mp29", + "mp30", + "mp31", + "mp32", + "mp33", + "mp34", + "mp35", + "mp36", + "mp37", + "mp38", + "mp39", + "mp40", + "mp41", + "mp42", + "mp43", + "mp44", + "mp45", + "mp46", + "mp47", + "mp48", + "mp49", + "mp50", + "mp51", + "mp52", + "mp53", + "mp54", + "mp55", + "mp56", + "mp57", + "mp58", + "mp59", + "mp60", + "mp61", + "mp62", + "mp63", + "mp64", + "mp65", + "mp66", + "mp67", + "mp68", + "mp69", + "mp70", + "mp71", + "mp72", + "mp73", + "mp74", + "mp75", + "mp76", + "mp77", + "mp78", + "mp79", + "mp80", + "mp81", + "mp82", + "mp83", + "mp84", + "mp85", + "mp86", + "mp87", + "mp88", + "mp89", + "mp90", + "mp91", + "mp92", + "mp93", + "mp94", + "mp95", + "mp96", + "mp97", + "mp98", + "mp99", + "mp100", + "mp101", + "mp102", + "mp103", + "mp104", + "mp105", + "mp106", + "mp107", + "mp108", + "mp109", + "mp110", + "mp111", + "mp112", + "mp113", + "mp114", + "mp115", + "mp116", + "mp117", + "mp118", + "mp119", + "mp120", + "mp121", + "mp122", + "mp123", + "mp124", + "mp125", + "mp126", + "mp127", + "mp128", + "mp129", + "mp130", + "mp131", + "mp132", + "mp133", + "mp134", + "mp135", + "mp136", + "mp137", + "mp138", + "mp139", + "mp140", + "mp141", + "mp142", + "mp143", + "mp144", + "mp145", + "mp146", + "mp147", + "mp148", + "mp149", + "mp150", + "mp151", + "mp152", + "mp153", + "mp154", + "mp155", + "mp156", + "mp157", + "mp158", + "mp159", + "mp160", + "mp161", + "mp162", + "mp163", + "mp164", + "mp165", + "mp166", + "mp167", + "mp168", + "mp169", + "mp170", + "mp171", + "mp172", + "mp173", + "mp174", + "mp175", + "mp176", + "mp177", + "mp178", + "mp179", + "mp180", + "mp181", + "mp182", + "mp183", + "mp184", + "mp185", + "mp186", + "mp187", + "mp188", + "mp189", + "mp190", + "mp191", + "mp192", + "mp193", + "mp194", + "mp195", + "mp196", + "mp197", + "mp198", + "mp199", + "mp200", + "mp201", + "mp202", + "mp203", + "mp204", + "mp205", + "mp206", + "mp207", + "mp208", + "mp209", + "mp210", + "mp211", + "mp212", + "mp213", + "mp214", + "mp215", + "mp216", + "mp217", + "mp218", + "mp219", + "mp220", + "mp221", + "mp222", + "mp223", + "mp224", + "mp225", + "mp226", + "mp227", + "mp228", + "mp229", + "mp230", + "mp231", + "mp232", + "mp233", + "mp234", + "mp235", + "mp236", + "mp237", + "mp238", + "mp239", + "mp240", + "mp241", + "mp242", + "mp243", + "mp244", + "mp245", + "mp246", + "mp247", + "mp248", + "mp249", + "mp250", + "mp251", + "mp252", + "mp253", + "mp254", + "mp255", + "unused0", + "unused1", + "unused2", + "unused3", + "unused4", + "unused5", + "unused6", + "unused7", + "unused8", + "unused9", + "unused10", + "unused11", + "unused12", + "unused13", + "unused14", + "unused15", + "unused16", + "unused17", + "unused18", + "unused19", + "unused20", + "unused21", + "unused22", + "unused23", + "unused24", + "unused25", + "unused26", + "unused27", + "unused28", + "unused29", + "unused30", + "unused31", + "unused32", + "unused33", + "unused34", + "unused35", + "unused36", + "unused37", + "unused38", + "unused39", + "unused40", + "unused41", + "unused42", + "unused43", + "unused44", + "unused45", + "unused46", + "unused47", + "unused48", + "unused49", + "unused50", + "unused51", + "unused52", + "unused53", + "unused54", + "unused55", + "unused56", + "unused57", + "unused58", + "unused59", + "unused60", + "unused61", + "unused62", + "unused63", + "unused64", + "unused65", + "unused66", + "unused67", + "unused68", + "unused69", + "unused70", + "unused71", + "unused72", + "unused73", + "unused74", + "unused75", + "unused76", + "unused77", + "unused78", + "unused79", + "unused80", + "unused81", + "unused82", + "unused83", + "unused84", + "unused85", + "unused86", + "unused87", + "unused88", + "unused89", + "unused90", + "unused91", + "unused92", + "unused93", + "unused94", + "unused95", + "unused96", + "unused97", + "unused98", + "unused99", + "unused100", + "unused101", + "unused102", + "unused103", + "unused104", + "unused105", + "unused106", + "unused107", + "unused108", + "unused109", + "unused110", + "unused111", + "unused112", + "unused113", + "unused114", + "unused115", + "unused116", + "unused117", + "unused118", + "unused119", + "unused120", + "unused121", + "unused122", + "unused123", + "unused124", + "unused125", + "unused126", + "unused127", + "unused128", + "unused129", + "unused130", + "unused131", + "unused132", + "unused133", + "unused134", + "unused135", + "unused136", + "unused137", + "unused138", + "unused139", + "unused140", + "unused141", + "unused142", + "unused143", + "unused144", + "unused145", + "unused146", + "unused147", + "unused148", + "unused149", + "unused150", + "unused151", + "unused152", + "unused153", + "unused154", + "unused155", + "unused156", + "unused157", + "unused158", + "unused159", + "unused160", + "unused161", + "unused162", + "unused163", + "unused164", + "unused165", + "unused166", + "unused167", + "unused168", + "unused169", + "unused170", + "unused171", + "unused172", + "unused173", + "unused174", + "unused175", + "unused176", + "unused177", + "unused178", + "unused179", + "unused180", + "unused181", + "unused182", + "unused183", + "unused184", + "unused185", + "unused186", + "unused187", + "unused188", + "unused189", + "unused190", + "unused191", + "unused192", + "unused193", + "unused194", + "unused195", + "unused196", + "unused197", + "unused198", + "unused199", + "unused200", + "unused201", + "unused202", + "unused203", + "unused204", + "unused205", + "unused206", + "unused207", + "unused208", + "unused209", + "unused210", + "unused211", + "unused212", + "unused213", + "unused214", + "unused215", + "unused216", + "unused217", + "unused218", + "unused219", + "unused220", + "unused221", + "unused222", + "unused223", + "unused224", + "unused225", + "unused226", + "unused227", + "unused228", + "unused229", + "unused230", + "unused231", + "unused232", + "unused233", + "unused234", + "unused235", + "unused236", + "unused237", + "unused238", + "unused239", + "unused240", + "unused241", + "unused242", + "unused243", + "unused244", + "unused245", + "unused246", + "unused247", + "unused248", + "unused249", + "unused250", + "unused251", + "unused252", + "unused253", + "unused254", + "unused255" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Config.Disk" + ] + ], + "description" : "You need 'VM.Config.Disk' permissions on /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage. To move a volume to another container, you need the permissions on the target container as well." + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/move_volume", + "text" : "move_volume" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get container configuration, including pending changes.", + "method" : "GET", + "name" : "vm_pending", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "delete" : { + "description" : "Indicates a pending delete request if present and not 0.", + "maximum" : 2, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "key" : { + "description" : "Configuration option name.", + "type" : "string" + }, + "pending" : { + "description" : "Pending value.", + "optional" : 1, + "type" : "string" + }, + "value" : { + "description" : "Current value.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/pending", + "text" : "pending" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get IP addresses of the specified container interface.", + "method" : "GET", + "name" : "ip", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : { + "hwaddr" : { + "description" : "The MAC address of the interface", + "optional" : 0, + "type" : "string" + }, + "inet" : { + "description" : "The IPv4 address of the interface", + "optional" : 1, + "type" : "string" + }, + "inet6" : { + "description" : "The IPv6 address of the interface", + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "The name of the interface", + "optional" : 0, + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/interfaces", + "text" : "interfaces" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Migration tunnel endpoint - only for internal use by CT migration.", + "method" : "POST", + "name" : "mtunnel", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "bridges" : { + "description" : "List of network bridges to check availability. Will be checked again for actually used bridges during migration.", + "format" : "pve-bridge-id-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storages" : { + "description" : "List of storages to check permission and availability. Will be checked again for all actually used storages during migration.", + "format" : "pve-storage-id-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "and", + [ + "perm", + "/vms/{vmid}", + [ + "VM.Allocate" + ] + ], + [ + "perm", + "/", + [ + "Sys.Incoming" + ] + ] + ], + "description" : "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming on '/'. Further permission checks happen during the actual migration." + }, + "protected" : 1, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "socket" : { + "type" : "string" + }, + "ticket" : { + "type" : "string" + }, + "upid" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/mtunnel", + "text" : "mtunnel" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.", + "method" : "GET", + "name" : "mtunnelwebsocket", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "socket" : { + "description" : "unix socket to forward to", + "type" : "string", + "typetext" : "" + }, + "ticket" : { + "description" : "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "description" : "You need to pass a ticket valid for the selected socket. Tickets can be created via the mtunnel API call, which will check permissions accordingly.", + "user" : "all" + }, + "returns" : { + "properties" : { + "port" : { + "optional" : 1, + "type" : "string" + }, + "socket" : { + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/lxc/{vmid}/mtunnelwebsocket", + "text" : "mtunnelwebsocket" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Destroy the container (also delete all uses files).", + "method" : "DELETE", + "name" : "destroy_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "destroy-unreferenced-disks" : { + "description" : "If set, destroy additionally all disks with the VMID from all enabled storages which are not referenced in the config.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "force" : { + "default" : 0, + "description" : "Force destroy, even if running.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "purge" : { + "default" : 0, + "description" : "Remove container from all related configurations. For example, backup jobs, replication jobs or HA. Related ACLs and Firewall entries will *always* be removed.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/vms/{vmid}", + [ + "VM.Allocate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Directory index", + "method" : "GET", + "name" : "vmdiridx", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "subdir" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{subdir}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/lxc/{vmid}", + "text" : "{vmid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "LXC container index (per node).", + "method" : "GET", + "name" : "vmlist", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Only list CTs where you have VM.Audit permissons on /vms/.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "cpus" : { + "description" : "Maximum usable CPUs.", + "optional" : 1, + "type" : "number" + }, + "lock" : { + "description" : "The current config lock, if any.", + "optional" : 1, + "type" : "string" + }, + "maxdisk" : { + "description" : "Root disk size in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "maxmem" : { + "description" : "Maximum memory in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "maxswap" : { + "description" : "Maximum SWAP memory in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "name" : { + "description" : "Container name.", + "optional" : 1, + "type" : "string" + }, + "status" : { + "description" : "LXC Container status.", + "enum" : [ + "stopped", + "running" + ], + "type" : "string" + }, + "tags" : { + "description" : "The current configured tags, if any.", + "optional" : 1, + "type" : "string" + }, + "uptime" : { + "description" : "Uptime.", + "optional" : 1, + "renderer" : "duration", + "type" : "integer" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{vmid}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create or restore a container.", + "method" : "POST", + "name" : "create_vm", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "arch" : { + "default" : "amd64", + "description" : "OS architecture type.", + "enum" : [ + "amd64", + "i386", + "arm64", + "armhf", + "riscv32", + "riscv64" + ], + "optional" : 1, + "type" : "string" + }, + "bwlimit" : { + "default" : "restore limit from datacenter or storage config", + "description" : "Override I/O bandwidth limit (in KiB/s).", + "minimum" : "0", + "optional" : 1, + "type" : "number", + "typetext" : " (0 - N)" + }, + "cmode" : { + "default" : "tty", + "description" : "Console mode. By default, the console command tries to open a connection to one of the available tty devices. By setting cmode to 'console' it tries to attach to /dev/console instead. If you set cmode to 'shell', it simply invokes a shell inside the container (no login).", + "enum" : [ + "shell", + "console", + "tty" + ], + "optional" : 1, + "type" : "string" + }, + "console" : { + "default" : 1, + "description" : "Attach a console device (/dev/console) to the container.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "cores" : { + "description" : "The number of cores assigned to the container. A container can use all available cores by default.", + "maximum" : 8192, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 8192)" + }, + "cpulimit" : { + "default" : 0, + "description" : "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has a total of '2' CPU time. Value '0' indicates no CPU limit.", + "maximum" : 8192, + "minimum" : 0, + "optional" : 1, + "type" : "number", + "typetext" : " (0 - 8192)" + }, + "cpuunits" : { + "default" : "cgroup v1: 1024, cgroup v2: 100", + "description" : "CPU weight for a container, will be clamped to [1, 10000] in cgroup v2.", + "maximum" : 500000, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 500000)", + "verbose_description" : "CPU weight for a container. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this container gets. Number is relative to the weights of all the other running guests." + }, + "debug" : { + "default" : 0, + "description" : "Try to be more verbose. For now this only enables debug log-level on start.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "description" : { + "description" : "Description for the Container. Shown in the web-interface CT's summary. This is saved as comment inside the configuration file.", + "maxLength" : 8192, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dev[n]" : { + "description" : "Device to pass through to the container", + "format" : { + "gid" : { + "description" : "Group ID to be assigned to the device node", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "mode" : { + "description" : "Access mode to be set on the device node", + "format_description" : "Octal access mode", + "optional" : 1, + "pattern" : "0[0-7]{3}", + "type" : "string" + }, + "path" : { + "default_key" : 1, + "description" : "Device to pass through to the container", + "format" : "pve-lxc-dev-string", + "format_description" : "Path", + "optional" : 1, + "type" : "string", + "verbose_description" : "Path to the device to pass through to the container" + }, + "uid" : { + "description" : "User ID to be assigned to the device node", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[[path=]] [,gid=] [,mode=] [,uid=]" + }, + "features" : { + "description" : "Allow containers access to advanced features.", + "format" : { + "force_rw_sys" : { + "default" : 0, + "description" : "Mount /sys in unprivileged containers as `rw` instead of `mixed`. This can break networking under newer (>= v245) systemd-network use.", + "optional" : 1, + "type" : "boolean" + }, + "fuse" : { + "default" : 0, + "description" : "Allow using 'fuse' file systems in a container. Note that interactions between fuse and the freezer cgroup can potentially cause I/O deadlocks.", + "optional" : 1, + "type" : "boolean" + }, + "keyctl" : { + "default" : 0, + "description" : "For unprivileged containers only: Allow the use of the keyctl() system call. This is required to use docker inside a container. By default unprivileged containers will see this system call as non-existent. This is mostly a workaround for systemd-networkd, as it will treat it as a fatal error when some keyctl() operations are denied by the kernel due to lacking permissions. Essentially, you can choose between running systemd-networkd or docker.", + "optional" : 1, + "type" : "boolean" + }, + "mknod" : { + "default" : 0, + "description" : "Allow unprivileged containers to use mknod() to add certain device nodes. This requires a kernel with seccomp trap to user space support (5.3 or newer). This is experimental.", + "optional" : 1, + "type" : "boolean" + }, + "mount" : { + "description" : "Allow mounting file systems of specific types. This should be a list of file system types as used with the mount command. Note that this can have negative effects on the container's security. With access to a loop device, mounting a file can circumvent the mknod permission of the devices cgroup, mounting an NFS file system can block the host's I/O completely and prevent it from rebooting, etc.", + "format_description" : "fstype;fstype;...", + "optional" : 1, + "pattern" : "(?^:[a-zA-Z0-9_; ]+)", + "type" : "string" + }, + "nesting" : { + "default" : 0, + "description" : "Allow nesting. Best used with unprivileged containers with additional id mapping. Note that this will expose procfs and sysfs contents of the host to the guest.", + "optional" : 1, + "type" : "boolean" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[force_rw_sys=<1|0>] [,fuse=<1|0>] [,keyctl=<1|0>] [,mknod=<1|0>] [,mount=] [,nesting=<1|0>]" + }, + "force" : { + "description" : "Allow to overwrite existing container.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "hookscript" : { + "description" : "Script that will be exectued during various steps in the containers lifetime.", + "format" : "pve-volume-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "hostname" : { + "description" : "Set a host name for the container.", + "format" : "dns-name", + "maxLength" : 255, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ignore-unpack-errors" : { + "description" : "Ignore errors when extracting the template.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "lock" : { + "description" : "Lock/unlock the container.", + "enum" : [ + "backup", + "create", + "destroyed", + "disk", + "fstrim", + "migrate", + "mounted", + "rollback", + "snapshot", + "snapshot-delete" + ], + "optional" : 1, + "type" : "string" + }, + "memory" : { + "default" : 512, + "description" : "Amount of RAM for the container in MB.", + "minimum" : 16, + "optional" : 1, + "type" : "integer", + "typetext" : " (16 - N)" + }, + "mp[n]" : { + "description" : "Use volume as container mount point. Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume.", + "format" : { + "acl" : { + "description" : "Explicitly enable or disable ACL support.", + "optional" : 1, + "type" : "boolean" + }, + "backup" : { + "description" : "Whether to include the mount point in backups.", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Whether to include the mount point in backups (only used for volume mount points)." + }, + "mountoptions" : { + "description" : "Extra mount options for rootfs/mps.", + "format_description" : "opt[;opt...]", + "optional" : 1, + "pattern" : "(?^:(?^:(noatime|lazytime|nodev|nosuid|noexec))(;(?^:(noatime|lazytime|nodev|nosuid|noexec)))*)", + "type" : "string" + }, + "mp" : { + "description" : "Path to the mount point as seen from inside the container (must not contain symlinks).", + "format" : "pve-lxc-mp-string", + "format_description" : "Path", + "type" : "string", + "verbose_description" : "Path to the mount point as seen from inside the container.\n\nNOTE: Must not contain any symlinks for security reasons." + }, + "quota" : { + "description" : "Enable user quotas inside the container (not supported with zfs subvolumes)", + "optional" : 1, + "type" : "boolean" + }, + "replicate" : { + "default" : 1, + "description" : "Will include this volume to a storage replica job.", + "optional" : 1, + "type" : "boolean" + }, + "ro" : { + "description" : "Read-only mount point", + "optional" : 1, + "type" : "boolean" + }, + "shared" : { + "default" : 0, + "description" : "Mark this non-volume mount point as available on multiple nodes (see 'nodes')", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this non-volume mount point as available on all nodes.\n\nWARNING: This option does not share the mount point automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Volume size (read only value).", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "volume" : { + "default_key" : 1, + "description" : "Volume, device or directory to mount into the container.", + "format" : "pve-lxc-mp-string", + "format_description" : "volume", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[volume=] ,mp= [,acl=<1|0>] [,backup=<1|0>] [,mountoptions=] [,quota=<1|0>] [,replicate=<1|0>] [,ro=<1|0>] [,shared=<1|0>] [,size=]" + }, + "nameserver" : { + "description" : "Sets DNS server IP address for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.", + "format" : "lxc-ip-with-ll-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "net[n]" : { + "description" : "Specifies network interfaces for the container.", + "format" : { + "bridge" : { + "description" : "Bridge to attach the network device to.", + "format_description" : "bridge", + "optional" : 1, + "pattern" : "[-_.\\w\\d]+", + "type" : "string" + }, + "firewall" : { + "description" : "Controls whether this interface's firewall rules should be used.", + "optional" : 1, + "type" : "boolean" + }, + "gw" : { + "description" : "Default gateway for IPv4 traffic.", + "format" : "ipv4", + "format_description" : "GatewayIPv4", + "optional" : 1, + "type" : "string" + }, + "gw6" : { + "description" : "Default gateway for IPv6 traffic.", + "format" : "ipv6", + "format_description" : "GatewayIPv6", + "optional" : 1, + "type" : "string" + }, + "hwaddr" : { + "description" : "The interface MAC address. This is dynamically allocated by default, but you can set that statically if needed, for example to always have the same link-local IPv6 address. (lxc.network.hwaddr)", + "format" : "mac-addr", + "format_description" : "XX:XX:XX:XX:XX:XX", + "optional" : 1, + "type" : "string", + "verbose_description" : "A common MAC address with the I/G (Individual/Group) bit not set." + }, + "ip" : { + "description" : "IPv4 address in CIDR format.", + "format" : "pve-ipv4-config", + "format_description" : "(IPv4/CIDR|dhcp|manual)", + "optional" : 1, + "type" : "string" + }, + "ip6" : { + "description" : "IPv6 address in CIDR format.", + "format" : "pve-ipv6-config", + "format_description" : "(IPv6/CIDR|auto|dhcp|manual)", + "optional" : 1, + "type" : "string" + }, + "link_down" : { + "description" : "Whether this interface should be disconnected (like pulling the plug).", + "optional" : 1, + "type" : "boolean" + }, + "mtu" : { + "description" : "Maximum transfer unit of the interface. (lxc.network.mtu)", + "maximum" : 65535, + "minimum" : 64, + "optional" : 1, + "type" : "integer" + }, + "name" : { + "description" : "Name of the network device as seen from inside the container. (lxc.network.name)", + "format_description" : "string", + "pattern" : "[-_.\\w\\d]+", + "type" : "string" + }, + "rate" : { + "description" : "Apply rate limiting to the interface", + "format_description" : "mbps", + "optional" : 1, + "type" : "number" + }, + "tag" : { + "description" : "VLAN tag for this interface.", + "maximum" : 4094, + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "trunks" : { + "description" : "VLAN ids to pass through the interface", + "format_description" : "vlanid[;vlanid...]", + "optional" : 1, + "pattern" : "(?^:\\d+(?:;\\d+)*)", + "type" : "string" + }, + "type" : { + "description" : "Network interface type.", + "enum" : [ + "veth" + ], + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "name= [,bridge=] [,firewall=<1|0>] [,gw=] [,gw6=] [,hwaddr=] [,ip=<(IPv4/CIDR|dhcp|manual)>] [,ip6=<(IPv6/CIDR|auto|dhcp|manual)>] [,link_down=<1|0>] [,mtu=] [,rate=] [,tag=] [,trunks=] [,type=]" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "onboot" : { + "default" : 0, + "description" : "Specifies whether a container will be started during system bootup.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ostemplate" : { + "description" : "The OS template or backup file.", + "maxLength" : 255, + "type" : "string", + "typetext" : "" + }, + "ostype" : { + "description" : "OS type. This is used to setup configuration inside the container, and corresponds to lxc setup scripts in /usr/share/lxc/config/.common.conf. Value 'unmanaged' can be used to skip and OS specific setup.", + "enum" : [ + "debian", + "devuan", + "ubuntu", + "centos", + "fedora", + "opensuse", + "archlinux", + "alpine", + "gentoo", + "nixos", + "unmanaged" + ], + "optional" : 1, + "type" : "string" + }, + "password" : { + "description" : "Sets root password inside container.", + "minLength" : 5, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "pool" : { + "description" : "Add the VM to the specified pool.", + "format" : "pve-poolid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "protection" : { + "default" : 0, + "description" : "Sets the protection flag of the container. This will prevent the CT or CT's disk remove/update operation.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "restore" : { + "description" : "Mark this as restore task.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "rootfs" : { + "description" : "Use volume as container root.", + "format" : { + "acl" : { + "description" : "Explicitly enable or disable ACL support.", + "optional" : 1, + "type" : "boolean" + }, + "mountoptions" : { + "description" : "Extra mount options for rootfs/mps.", + "format_description" : "opt[;opt...]", + "optional" : 1, + "pattern" : "(?^:(?^:(noatime|lazytime|nodev|nosuid|noexec))(;(?^:(noatime|lazytime|nodev|nosuid|noexec)))*)", + "type" : "string" + }, + "quota" : { + "description" : "Enable user quotas inside the container (not supported with zfs subvolumes)", + "optional" : 1, + "type" : "boolean" + }, + "replicate" : { + "default" : 1, + "description" : "Will include this volume to a storage replica job.", + "optional" : 1, + "type" : "boolean" + }, + "ro" : { + "description" : "Read-only mount point", + "optional" : 1, + "type" : "boolean" + }, + "shared" : { + "default" : 0, + "description" : "Mark this non-volume mount point as available on multiple nodes (see 'nodes')", + "optional" : 1, + "type" : "boolean", + "verbose_description" : "Mark this non-volume mount point as available on all nodes.\n\nWARNING: This option does not share the mount point automatically, it assumes it is shared already!" + }, + "size" : { + "description" : "Volume size (read only value).", + "format" : "disk-size", + "format_description" : "DiskSize", + "optional" : 1, + "type" : "string" + }, + "volume" : { + "default_key" : 1, + "description" : "Volume, device or directory to mount into the container.", + "format" : "pve-lxc-mp-string", + "format_description" : "volume", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[volume=] [,acl=<1|0>] [,mountoptions=] [,quota=<1|0>] [,replicate=<1|0>] [,ro=<1|0>] [,shared=<1|0>] [,size=]" + }, + "searchdomain" : { + "description" : "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.", + "format" : "dns-name-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ssh-public-keys" : { + "description" : "Setup public SSH keys (one key per line, OpenSSH format).", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "start" : { + "default" : 0, + "description" : "Start the CT after its creation finished successfully.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "startup" : { + "description" : "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped.", + "format" : "pve-startup-order", + "optional" : 1, + "type" : "string", + "typetext" : "[[order=]\\d+] [,up=\\d+] [,down=\\d+] " + }, + "storage" : { + "default" : "local", + "description" : "Default Storage.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "swap" : { + "default" : 512, + "description" : "Amount of SWAP for the container in MB.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "tags" : { + "description" : "Tags of the Container. This is only meta information.", + "format" : "pve-tag-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "template" : { + "default" : 0, + "description" : "Enable/disable Template.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "timezone" : { + "description" : "Time zone to use in the container. If option isn't set, then nothing will be done. Can be set to 'host' to match the host time zone, or an arbitrary time zone option from /usr/share/zoneinfo/zone.tab", + "format" : "pve-ct-timezone", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tty" : { + "default" : 2, + "description" : "Specify the number of tty available to the container", + "maximum" : 6, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 6)" + }, + "unique" : { + "description" : "Assign a unique random ethernet address.", + "optional" : 1, + "requires" : "restore", + "type" : "boolean", + "typetext" : "" + }, + "unprivileged" : { + "default" : 0, + "description" : "Makes the container run as unprivileged user. (Should not be modified manually.)", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "unused[n]" : { + "description" : "Reference to unused volumes. This is used internally, and should not be modified manually.", + "format" : { + "volume" : { + "default_key" : 1, + "description" : "The volume that is not used currently.", + "format" : "pve-volume-id", + "format_description" : "volume", + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[volume=]" + }, + "vmid" : { + "description" : "The (unique) ID of the VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "description" : "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. For restore, it is enough if the user has 'VM.Backup' permission and the VM already exists. You also need 'Datastore.AllocateSpace' permissions on the storage.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/lxc", + "text" : "lxc" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get the Ceph configuration file.", + "method" : "GET", + "name" : "raw", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/cfg/raw", + "text" : "raw" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get the Ceph configuration database.", + "method" : "GET", + "name" : "db", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "can_update_at_runtime" : { + "type" : "boolean" + }, + "level" : { + "type" : "string" + }, + "mask" : { + "type" : "string" + }, + "name" : { + "type" : "string" + }, + "section" : { + "type" : "string" + }, + "value" : { + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/cfg/db", + "text" : "db" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get configured values from either the config file or config DB.", + "method" : "GET", + "name" : "value", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "config-keys" : { + "description" : "List of
: items.", + "pattern" : "(?^:^(:?(?^i:[0-9a-z\\-_\\.]+:[0-9a-zA-Z\\-_]+))(:?[;, ](?^i:[0-9a-z\\-_\\.]+:[0-9a-zA-Z\\-_]+))*$)", + "type" : "string", + "typetext" : "
:[;
:]" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Contains {section}->{key} children with the values", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/cfg/value", + "text" : "value" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/ceph/cfg", + "text" : "cfg" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get OSD details", + "method" : "GET", + "name" : "osddetails", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "osdid" : { + "description" : "OSD ID", + "type" : "integer", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "devices" : { + "description" : "Array containing data about devices", + "items" : { + "properties" : { + "dev_node" : { + "description" : "Device node", + "type" : "string" + }, + "device" : { + "description" : "Kind of OSD device", + "enum" : [ + "block", + "db", + "wal" + ], + "type" : "string" + }, + "devices" : { + "description" : "Physical disks used", + "type" : "string" + }, + "size" : { + "description" : "Size in bytes", + "type" : "integer" + }, + "support_discard" : { + "description" : "Discard support of the physical device", + "type" : "boolean" + }, + "type" : { + "description" : "Type of device. For example, hdd or ssd", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + }, + "osd" : { + "description" : "General information about the OSD", + "properties" : { + "back_addr" : { + "description" : "Address and port used to talk to other OSDs.", + "type" : "string" + }, + "front_addr" : { + "description" : "Address and port used to talk to clients and monitors.", + "type" : "string" + }, + "hb_back_addr" : { + "description" : "Heartbeat address and port for other OSDs.", + "type" : "string" + }, + "hb_front_addr" : { + "description" : "Heartbeat address and port for clients and monitors.", + "type" : "string" + }, + "hostname" : { + "description" : "Name of the host containing the OSD.", + "type" : "string" + }, + "id" : { + "description" : "ID of the OSD.", + "type" : "integer" + }, + "mem_usage" : { + "description" : "Memory usage of the OSD service.", + "type" : "integer" + }, + "osd_data" : { + "description" : "Path to the OSD's data directory.", + "type" : "string" + }, + "osd_objectstore" : { + "description" : "The type of object store used.", + "type" : "string" + }, + "pid" : { + "description" : "OSD process ID.", + "type" : "integer" + }, + "version" : { + "description" : "Ceph version of the OSD service.", + "type" : "string" + } + }, + "type" : "object" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/osd/{osdid}/metadata", + "text" : "metadata" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get OSD volume details", + "method" : "GET", + "name" : "osdvolume", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "osdid" : { + "description" : "OSD ID", + "type" : "integer", + "typetext" : "" + }, + "type" : { + "default" : "block", + "description" : "OSD device type", + "enum" : [ + "block", + "db", + "wal" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "creation_time" : { + "description" : "Creation time as reported by `lvs`.", + "type" : "string" + }, + "lv_name" : { + "description" : "Name of the logical volume (LV).", + "type" : "string" + }, + "lv_path" : { + "description" : "Path to the logical volume (LV).", + "type" : "string" + }, + "lv_size" : { + "description" : "Size of the logical volume (LV).", + "type" : "integer" + }, + "lv_uuid" : { + "description" : "UUID of the logical volume (LV).", + "type" : "string" + }, + "vg_name" : { + "description" : "Name of the volume group (VG).", + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/osd/{osdid}/lv-info", + "text" : "lv-info" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "ceph osd in", + "method" : "POST", + "name" : "in", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "osdid" : { + "description" : "OSD ID", + "type" : "integer", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/osd/{osdid}/in", + "text" : "in" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "ceph osd out", + "method" : "POST", + "name" : "out", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "osdid" : { + "description" : "OSD ID", + "type" : "integer", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/osd/{osdid}/out", + "text" : "out" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Instruct the OSD to scrub.", + "method" : "POST", + "name" : "scrub", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "deep" : { + "default" : 0, + "description" : "If set, instructs a deep scrub instead of a normal one.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "osdid" : { + "description" : "OSD ID", + "type" : "integer", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/osd/{osdid}/scrub", + "text" : "scrub" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Destroy OSD", + "method" : "DELETE", + "name" : "destroyosd", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cleanup" : { + "default" : 0, + "description" : "If set, we remove partition table entries.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "osdid" : { + "description" : "OSD ID", + "type" : "integer", + "typetext" : "" + } + } + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "OSD index.", + "method" : "GET", + "name" : "osdindex", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "osdid" : { + "description" : "OSD ID", + "type" : "integer", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/ceph/osd/{osdid}", + "text" : "{osdid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get Ceph osd list/tree.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "flags" : { + "type" : "string" + }, + "root" : { + "description" : "Tree with OSDs in the CRUSH map structure.", + "type" : "object" + } + }, + "type" : "object" + }, + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create OSD", + "method" : "POST", + "name" : "createosd", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "crush-device-class" : { + "description" : "Set the device class of the OSD in crush.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "db_dev" : { + "description" : "Block device name for block.db.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "db_dev_size" : { + "default" : "bluestore_block_db_size or 10% of OSD size", + "description" : "Size in GiB for block.db.", + "minimum" : 1, + "optional" : 1, + "requires" : "db_dev", + "type" : "number", + "typetext" : " (1 - N)", + "verbose_description" : "If a block.db is requested but the size is not given, will be automatically selected by: bluestore_block_db_size from the ceph database (osd or global section) or config (osd or global section) in that order. If this is not available, it will be sized 10% of the size of the OSD device. Fails if the available size is not enough." + }, + "dev" : { + "description" : "Block device name.", + "type" : "string", + "typetext" : "" + }, + "encrypted" : { + "default" : 0, + "description" : "Enables encryption of the OSD.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "osds-per-device" : { + "description" : "OSD services per physical device. Only useful for fast NVMe devices\"\n\t\t .\" to utilize their performance better.", + "minimum" : "1", + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "wal_dev" : { + "description" : "Block device name for block.wal.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "wal_dev_size" : { + "default" : "bluestore_block_wal_size or 1% of OSD size", + "description" : "Size in GiB for block.wal.", + "minimum" : 0.5, + "optional" : 1, + "requires" : "wal_dev", + "type" : "number", + "typetext" : " (0.5 - N)", + "verbose_description" : "If a block.wal is requested but the size is not given, will be automatically selected by: bluestore_block_wal_size from the ceph database (osd or global section) or config (osd or global section) in that order. If this is not available, it will be sized 1% of the size of the OSD device. Fails if the available size is not enough." + } + } + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/ceph/osd", + "text" : "osd" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Destroy Ceph Metadata Server", + "method" : "DELETE", + "name" : "destroymds", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "The name (ID) of the mds", + "pattern" : "[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create Ceph Metadata Server (MDS)", + "method" : "POST", + "name" : "createmds", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "hotstandby" : { + "default" : "0", + "description" : "Determines whether a ceph-mds daemon should poll and replay the log of an active MDS. Faster switch on MDS failure, but needs more idle resources.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "default" : "nodename", + "description" : "The ID for the mds, when omitted the same as the nodename", + "maxLength" : 200, + "optional" : 1, + "pattern" : "[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/mds/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "MDS directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "addr" : { + "optional" : 1, + "type" : "string" + }, + "host" : { + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "The name (ID) for the MDS" + }, + "rank" : { + "optional" : 1, + "type" : "integer" + }, + "standby_replay" : { + "description" : "If true, the standby MDS is polling the active MDS for faster recovery (hot standby).", + "optional" : 1, + "type" : "boolean" + }, + "state" : { + "description" : "State of the MDS", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/ceph/mds", + "text" : "mds" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Destroy Ceph Manager.", + "method" : "DELETE", + "name" : "destroymgr", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "The ID of the manager", + "pattern" : "[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create Ceph Manager", + "method" : "POST", + "name" : "createmgr", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "The ID for the manager, when omitted the same as the nodename", + "maxLength" : 200, + "optional" : 1, + "pattern" : "[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/mgr/{id}", + "text" : "{id}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "MGR directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "addr" : { + "optional" : 1, + "type" : "string" + }, + "host" : { + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "The name (ID) for the MGR" + }, + "state" : { + "description" : "State of the MGR", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/ceph/mgr", + "text" : "mgr" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Destroy Ceph Monitor and Manager.", + "method" : "DELETE", + "name" : "destroymon", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "monid" : { + "description" : "Monitor ID", + "pattern" : "[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create Ceph Monitor and Manager", + "method" : "POST", + "name" : "createmon", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "mon-address" : { + "description" : "Overwrites autodetected monitor IP address(es). Must be in the public network(s) of Ceph.", + "format" : "ip-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "monid" : { + "description" : "The ID for the monitor, when omitted the same as the nodename", + "maxLength" : 200, + "optional" : 1, + "pattern" : "[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/mon/{monid}", + "text" : "{monid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get Ceph monitor list.", + "method" : "GET", + "name" : "listmon", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "addr" : { + "optional" : 1, + "type" : "string" + }, + "ceph_version" : { + "optional" : 1, + "type" : "string" + }, + "ceph_version_short" : { + "optional" : 1, + "type" : "string" + }, + "direxists" : { + "optional" : 1, + "type" : "string" + }, + "host" : { + "optional" : 1, + "type" : "boolean" + }, + "name" : { + "type" : "string" + }, + "quorum" : { + "optional" : 1, + "type" : "boolean" + }, + "rank" : { + "optional" : 1, + "type" : "integer" + }, + "service" : { + "optional" : 1, + "type" : "integer" + }, + "state" : { + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/ceph/mon", + "text" : "mon" + }, + { + "children" : [ + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Create a Ceph filesystem", + "method" : "POST", + "name" : "createfs", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "add-storage" : { + "default" : 0, + "description" : "Configure the created CephFS as storage for this cluster.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "default" : "cephfs", + "description" : "The ceph filesystem name.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pg_num" : { + "default" : 128, + "description" : "Number of placement groups for the backing data pool. The metadata pool will use a quarter of this.", + "maximum" : 32768, + "minimum" : 8, + "optional" : 1, + "type" : "integer", + "typetext" : " (8 - 32768)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/fs/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "data_pool" : { + "description" : "The name of the data pool.", + "type" : "string" + }, + "metadata_pool" : { + "description" : "The name of the metadata pool.", + "type" : "string" + }, + "name" : { + "description" : "The ceph filesystem name.", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/ceph/fs", + "text" : "fs" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Show the current pool status.", + "method" : "GET", + "name" : "getpool", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "The name of the pool. It must be unique.", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "verbose" : { + "default" : 0, + "description" : "If enabled, will display additional data(eg. statistics).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "application" : { + "default" : "rbd", + "description" : "The application of the pool.", + "enum" : [ + "rbd", + "cephfs", + "rgw" + ], + "optional" : 1, + "title" : "Application", + "type" : "string" + }, + "application_list" : { + "optional" : 1, + "title" : "Application", + "type" : "array" + }, + "autoscale_status" : { + "optional" : 1, + "title" : "Autoscale Status", + "type" : "object" + }, + "crush_rule" : { + "description" : "The rule to use for mapping object placement in the cluster.", + "optional" : 1, + "title" : "Crush Rule Name", + "type" : "string" + }, + "fast_read" : { + "title" : "Fast Read", + "type" : "boolean" + }, + "hashpspool" : { + "title" : "hashpspool", + "type" : "boolean" + }, + "id" : { + "title" : "ID", + "type" : "integer" + }, + "min_size" : { + "default" : 2, + "description" : "Minimum number of replicas per object", + "maximum" : 7, + "minimum" : 1, + "optional" : 1, + "title" : "Min Size", + "type" : "integer" + }, + "name" : { + "description" : "The name of the pool. It must be unique.", + "title" : "Name", + "type" : "string" + }, + "nodeep-scrub" : { + "title" : "nodeep-scrub", + "type" : "boolean" + }, + "nodelete" : { + "title" : "nodelete", + "type" : "boolean" + }, + "nopgchange" : { + "title" : "nopgchange", + "type" : "boolean" + }, + "noscrub" : { + "title" : "noscrub", + "type" : "boolean" + }, + "nosizechange" : { + "title" : "nosizechange", + "type" : "boolean" + }, + "pg_autoscale_mode" : { + "default" : "warn", + "description" : "The automatic PG scaling mode of the pool.", + "enum" : [ + "on", + "off", + "warn" + ], + "optional" : 1, + "title" : "PG Autoscale Mode", + "type" : "string" + }, + "pg_num" : { + "default" : 128, + "description" : "Number of placement groups.", + "maximum" : 32768, + "minimum" : 1, + "optional" : 1, + "title" : "PG Num", + "type" : "integer" + }, + "pg_num_min" : { + "description" : "Minimal number of placement groups.", + "maximum" : 32768, + "optional" : 1, + "title" : "min. PG Num", + "type" : "integer" + }, + "pgp_num" : { + "title" : "PGP num", + "type" : "integer" + }, + "size" : { + "default" : 3, + "description" : "Number of replicas per object", + "maximum" : 7, + "minimum" : 1, + "optional" : 1, + "title" : "Size", + "type" : "integer" + }, + "statistics" : { + "optional" : 1, + "title" : "Statistics", + "type" : "object" + }, + "target_size" : { + "description" : "The estimated target size of the pool for the PG autoscaler.", + "optional" : 1, + "pattern" : "^(\\d+(\\.\\d+)?)([KMGT])?$", + "title" : "PG Autoscale Target Size", + "type" : "string" + }, + "target_size_ratio" : { + "description" : "The estimated target ratio of the pool for the PG autoscaler.", + "optional" : 1, + "title" : "PG Autoscale Target Ratio", + "type" : "number" + }, + "use_gmt_hitset" : { + "title" : "use_gmt_hitset", + "type" : "boolean" + }, + "write_fadvise_dontneed" : { + "title" : "write_fadvise_dontneed", + "type" : "boolean" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/pool/{name}/status", + "text" : "status" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Destroy pool", + "method" : "DELETE", + "name" : "destroypool", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "default" : 0, + "description" : "If true, destroys pool even if in use", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "description" : "The name of the pool. It must be unique.", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "remove_ecprofile" : { + "default" : 1, + "description" : "Remove the erasure code profile. Defaults to true, if applicable.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "remove_storages" : { + "default" : 0, + "description" : "Remove all pveceph-managed storages configured for this pool", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Pool index.", + "method" : "GET", + "name" : "poolindex", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "The name of the pool.", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Change POOL settings", + "method" : "PUT", + "name" : "setpool", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "application" : { + "description" : "The application of the pool.", + "enum" : [ + "rbd", + "cephfs", + "rgw" + ], + "optional" : 1, + "title" : "Application", + "type" : "string" + }, + "crush_rule" : { + "description" : "The rule to use for mapping object placement in the cluster.", + "optional" : 1, + "title" : "Crush Rule Name", + "type" : "string", + "typetext" : "" + }, + "min_size" : { + "description" : "Minimum number of replicas per object", + "maximum" : 7, + "minimum" : 1, + "optional" : 1, + "title" : "Min Size", + "type" : "integer", + "typetext" : " (1 - 7)" + }, + "name" : { + "description" : "The name of the pool. It must be unique.", + "title" : "Name", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pg_autoscale_mode" : { + "description" : "The automatic PG scaling mode of the pool.", + "enum" : [ + "on", + "off", + "warn" + ], + "optional" : 1, + "title" : "PG Autoscale Mode", + "type" : "string" + }, + "pg_num" : { + "description" : "Number of placement groups.", + "maximum" : 32768, + "minimum" : 1, + "optional" : 1, + "title" : "PG Num", + "type" : "integer", + "typetext" : " (1 - 32768)" + }, + "pg_num_min" : { + "description" : "Minimal number of placement groups.", + "maximum" : 32768, + "optional" : 1, + "title" : "min. PG Num", + "type" : "integer", + "typetext" : " (-N - 32768)" + }, + "size" : { + "description" : "Number of replicas per object", + "maximum" : 7, + "minimum" : 1, + "optional" : 1, + "title" : "Size", + "type" : "integer", + "typetext" : " (1 - 7)" + }, + "target_size" : { + "description" : "The estimated target size of the pool for the PG autoscaler.", + "optional" : 1, + "pattern" : "^(\\d+(\\.\\d+)?)([KMGT])?$", + "title" : "PG Autoscale Target Size", + "type" : "string" + }, + "target_size_ratio" : { + "description" : "The estimated target ratio of the pool for the PG autoscaler.", + "optional" : 1, + "title" : "PG Autoscale Target Ratio", + "type" : "number", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/ceph/pool/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List all pools and their settings (which are settable by the POST/PUT endpoints).", + "method" : "GET", + "name" : "lspools", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "application_metadata" : { + "optional" : 1, + "title" : "Associated Applications", + "type" : "object" + }, + "autoscale_status" : { + "optional" : 1, + "title" : "Autoscale Status", + "type" : "object" + }, + "bytes_used" : { + "title" : "Used", + "type" : "integer" + }, + "crush_rule" : { + "title" : "Crush Rule", + "type" : "integer" + }, + "crush_rule_name" : { + "title" : "Crush Rule Name", + "type" : "string" + }, + "min_size" : { + "title" : "Min Size", + "type" : "integer" + }, + "percent_used" : { + "title" : "%-Used", + "type" : "number" + }, + "pg_autoscale_mode" : { + "optional" : 1, + "title" : "PG Autoscale Mode", + "type" : "string" + }, + "pg_num" : { + "title" : "PG Num", + "type" : "integer" + }, + "pg_num_final" : { + "optional" : 1, + "title" : "Optimal PG Num", + "type" : "integer" + }, + "pg_num_min" : { + "optional" : 1, + "title" : "min. PG Num", + "type" : "integer" + }, + "pool" : { + "title" : "ID", + "type" : "integer" + }, + "pool_name" : { + "title" : "Name", + "type" : "string" + }, + "size" : { + "title" : "Size", + "type" : "integer" + }, + "target_size" : { + "optional" : 1, + "title" : "PG Autoscale Target Size", + "type" : "integer" + }, + "target_size_ratio" : { + "optional" : 1, + "title" : "PG Autoscale Target Ratio", + "type" : "number" + }, + "type" : { + "enum" : [ + "replicated", + "erasure", + "unknown" + ], + "title" : "Type", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{pool_name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create Ceph pool", + "method" : "POST", + "name" : "createpool", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "add_storages" : { + "default" : "0; for erasure coded pools: 1", + "description" : "Configure VM and CT storage using the new pool.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "application" : { + "default" : "rbd", + "description" : "The application of the pool.", + "enum" : [ + "rbd", + "cephfs", + "rgw" + ], + "optional" : 1, + "title" : "Application", + "type" : "string" + }, + "crush_rule" : { + "description" : "The rule to use for mapping object placement in the cluster.", + "optional" : 1, + "title" : "Crush Rule Name", + "type" : "string", + "typetext" : "" + }, + "erasure-coding" : { + "description" : "Create an erasure coded pool for RBD with an accompaning replicated pool for metadata storage. With EC, the common ceph options 'size', 'min_size' and 'crush_rule' parameters will be applied to the metadata pool.", + "format" : { + "device-class" : { + "description" : "CRUSH device class. Will create an erasure coded pool plus a replicated pool for metadata.", + "format_description" : "class", + "optional" : 1, + "type" : "string" + }, + "failure-domain" : { + "default" : "host", + "description" : "CRUSH failure domain. Default is 'host'. Will create an erasure coded pool plus a replicated pool for metadata.", + "format_description" : "domain", + "optional" : 1, + "type" : "string" + }, + "k" : { + "description" : "Number of data chunks. Will create an erasure coded pool plus a replicated pool for metadata.", + "minimum" : 2, + "type" : "integer" + }, + "m" : { + "description" : "Number of coding chunks. Will create an erasure coded pool plus a replicated pool for metadata.", + "minimum" : 1, + "type" : "integer" + }, + "profile" : { + "description" : "Override the erasure code (EC) profile to use. Will create an erasure coded pool plus a replicated pool for metadata.", + "format_description" : "profile", + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "k= ,m= [,device-class=] [,failure-domain=] [,profile=]" + }, + "min_size" : { + "default" : 2, + "description" : "Minimum number of replicas per object", + "maximum" : 7, + "minimum" : 1, + "optional" : 1, + "title" : "Min Size", + "type" : "integer", + "typetext" : " (1 - 7)" + }, + "name" : { + "description" : "The name of the pool. It must be unique.", + "title" : "Name", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pg_autoscale_mode" : { + "default" : "warn", + "description" : "The automatic PG scaling mode of the pool.", + "enum" : [ + "on", + "off", + "warn" + ], + "optional" : 1, + "title" : "PG Autoscale Mode", + "type" : "string" + }, + "pg_num" : { + "default" : 128, + "description" : "Number of placement groups.", + "maximum" : 32768, + "minimum" : 1, + "optional" : 1, + "title" : "PG Num", + "type" : "integer", + "typetext" : " (1 - 32768)" + }, + "pg_num_min" : { + "description" : "Minimal number of placement groups.", + "maximum" : 32768, + "optional" : 1, + "title" : "min. PG Num", + "type" : "integer", + "typetext" : " (-N - 32768)" + }, + "size" : { + "default" : 3, + "description" : "Number of replicas per object", + "maximum" : 7, + "minimum" : 1, + "optional" : 1, + "title" : "Size", + "type" : "integer", + "typetext" : " (1 - 7)" + }, + "target_size" : { + "description" : "The estimated target size of the pool for the PG autoscaler.", + "optional" : 1, + "pattern" : "^(\\d+(\\.\\d+)?)([KMGT])?$", + "title" : "PG Autoscale Target Size", + "type" : "string" + }, + "target_size_ratio" : { + "description" : "The estimated target ratio of the pool for the PG autoscaler.", + "optional" : 1, + "title" : "PG Autoscale Target Ratio", + "type" : "number", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/ceph/pool", + "text" : "pool" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Create initial ceph default configuration and setup symlinks.", + "method" : "POST", + "name" : "init", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cluster-network" : { + "description" : "Declare a separate cluster network, OSDs will routeheartbeat, object replication and recovery traffic over it", + "format" : "CIDR", + "maxLength" : 128, + "optional" : 1, + "requires" : "network", + "type" : "string", + "typetext" : "" + }, + "disable_cephx" : { + "default" : 0, + "description" : "Disable cephx authentication.\n\nWARNING: cephx is a security feature protecting against man-in-the-middle attacks. Only consider disabling cephx if your network is private!", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "min_size" : { + "default" : 2, + "description" : "Minimum number of available replicas per object to allow I/O", + "maximum" : 7, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 7)" + }, + "network" : { + "description" : "Use specific network for all ceph related traffic", + "format" : "CIDR", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pg_bits" : { + "default" : 6, + "description" : "Placement group bits, used to specify the default number of placement groups.\n\nDepreacted. This setting was deprecated in recent Ceph versions.", + "maximum" : 14, + "minimum" : 6, + "optional" : 1, + "type" : "integer", + "typetext" : " (6 - 14)" + }, + "size" : { + "default" : 3, + "description" : "Targeted number of replicas per object", + "maximum" : 7, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 7)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/init", + "text" : "init" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Stop ceph services.", + "method" : "POST", + "name" : "stop", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "service" : { + "default" : "ceph.target", + "description" : "Ceph service name.", + "optional" : 1, + "pattern" : "(ceph|mon|mds|osd|mgr)(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)?", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/stop", + "text" : "stop" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Start ceph services.", + "method" : "POST", + "name" : "start", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "service" : { + "default" : "ceph.target", + "description" : "Ceph service name.", + "optional" : 1, + "pattern" : "(ceph|mon|mds|osd|mgr)(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)?", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/start", + "text" : "start" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Restart ceph services.", + "method" : "POST", + "name" : "restart", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "service" : { + "default" : "ceph.target", + "description" : "Ceph service name.", + "optional" : 1, + "pattern" : "(mon|mds|osd|mgr)(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)?", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/restart", + "text" : "restart" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get ceph status.", + "method" : "GET", + "name" : "status", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/status", + "text" : "status" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get OSD crush map", + "method" : "GET", + "name" : "crush", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/crush", + "text" : "crush" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read ceph log", + "method" : "GET", + "name" : "log", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "limit" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "start" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Syslog" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "n" : { + "description" : "Line number", + "type" : "integer" + }, + "t" : { + "description" : "Line text", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/log", + "text" : "log" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List ceph rules.", + "method" : "GET", + "name" : "rules", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "name" : { + "description" : "Name of the CRUSH rule.", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/rules", + "text" : "rules" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Heuristical check if it is safe to perform an action.", + "method" : "GET", + "name" : "cmd_safety", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "action" : { + "description" : "Action to check", + "enum" : [ + "stop", + "destroy" + ], + "type" : "string" + }, + "id" : { + "description" : "ID of the service", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "service" : { + "description" : "Service type", + "enum" : [ + "osd", + "mon", + "mds" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "safe" : { + "description" : "If it is safe to run the command.", + "type" : "boolean" + }, + "status" : { + "description" : "Status message given by Ceph.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/ceph/cmd-safety", + "text" : "cmd-safety" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Datastore.Audit" + ], + "any", + 1 + ] + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/ceph", + "text" : "ceph" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get the currently configured vzdump defaults.", + "method" : "GET", + "name" : "defaults", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "The user needs 'Datastore.Audit' or 'Datastore.AllocateSpace' permissions for the specified storage (or default storage if none specified). Some properties are only returned when the user has 'Sys.Audit' permissions for the node.", + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "additionalProperties" : 0, + "properties" : { + "all" : { + "default" : 0, + "description" : "Backup all known guest systems on this host.", + "optional" : 1, + "type" : "boolean" + }, + "bwlimit" : { + "default" : 0, + "description" : "Limit I/O bandwidth (in KiB/s).", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "compress" : { + "default" : "0", + "description" : "Compress dump file.", + "enum" : [ + "0", + "1", + "gzip", + "lzo", + "zstd" + ], + "optional" : 1, + "type" : "string" + }, + "dumpdir" : { + "description" : "Store resulting files to specified directory.", + "optional" : 1, + "type" : "string" + }, + "exclude" : { + "description" : "Exclude specified guest systems (assumes --all)", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string" + }, + "exclude-path" : { + "description" : "Exclude certain files/directories (shell globs). Paths starting with '/' are anchored to the container's root, other paths match relative to each subdirectory.", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "ionice" : { + "default" : 7, + "description" : "Set IO priority when using the BFQ scheduler. For snapshot and suspend mode backups of VMs, this only affects the compressor. A value of 8 means the idle priority is used, otherwise the best-effort priority is used with the specified value.", + "maximum" : 8, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "lockwait" : { + "default" : 180, + "description" : "Maximal time to wait for the global lock (minutes).", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "mailnotification" : { + "default" : "always", + "description" : "Deprecated: use 'notification-policy' instead.", + "enum" : [ + "always", + "failure" + ], + "optional" : 1, + "type" : "string" + }, + "mailto" : { + "description" : "Comma-separated list of email addresses or users that should receive email notifications. Has no effect if the 'notification-target' option is set at the same time.", + "format" : "email-or-username-list", + "optional" : 1, + "type" : "string" + }, + "maxfiles" : { + "description" : "Deprecated: use 'prune-backups' instead. Maximal number of backup files per guest system.", + "minimum" : 1, + "optional" : 1, + "type" : "integer" + }, + "mode" : { + "default" : "snapshot", + "description" : "Backup mode.", + "enum" : [ + "snapshot", + "suspend", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "Only run if executed on this node.", + "format" : "pve-node", + "optional" : 1, + "type" : "string" + }, + "notes-template" : { + "description" : "Template string for generating notes for the backup(s). It can contain variables which will be replaced by their values. Currently supported are {{cluster}}, {{guestname}}, {{node}}, and {{vmid}}, but more might be added in the future. Needs to be a single line, newline and backslash need to be escaped as '\\n' and '\\\\' respectively.", + "maxLength" : 1024, + "optional" : 1, + "requires" : "storage", + "type" : "string" + }, + "notification-policy" : { + "default" : "always", + "description" : "Specify when to send a notification", + "enum" : [ + "always", + "failure", + "never" + ], + "optional" : 1, + "type" : "string" + }, + "notification-target" : { + "description" : "Determine the target to which notifications should be sent. Can either be a notification endpoint or a notification group. This option takes precedence over 'mailto', meaning that if both are set, the 'mailto' option will be ignored.", + "format" : "pve-configid", + "optional" : 1, + "type" : "string" + }, + "performance" : { + "description" : "Other performance-related settings.", + "format" : "backup-performance", + "optional" : 1, + "type" : "string" + }, + "pigz" : { + "default" : 0, + "description" : "Use pigz instead of gzip when N>0. N=1 uses half of cores, N>1 uses N as thread count.", + "optional" : 1, + "type" : "integer" + }, + "pool" : { + "description" : "Backup all known guest systems included in the specified pool.", + "optional" : 1, + "type" : "string" + }, + "protected" : { + "description" : "If true, mark backup(s) as protected.", + "optional" : 1, + "requires" : "storage", + "type" : "boolean" + }, + "prune-backups" : { + "default" : "keep-all=1", + "description" : "Use these retention options instead of those from the storage configuration.", + "format" : "prune-backups", + "optional" : 1, + "type" : "string" + }, + "quiet" : { + "default" : 0, + "description" : "Be quiet.", + "optional" : 1, + "type" : "boolean" + }, + "remove" : { + "default" : 1, + "description" : "Prune older backups according to 'prune-backups'.", + "optional" : 1, + "type" : "boolean" + }, + "script" : { + "description" : "Use specified hook script.", + "optional" : 1, + "type" : "string" + }, + "stdexcludes" : { + "default" : 1, + "description" : "Exclude temporary files and logs.", + "optional" : 1, + "type" : "boolean" + }, + "stop" : { + "default" : 0, + "description" : "Stop running backup jobs on this host.", + "optional" : 1, + "type" : "boolean" + }, + "stopwait" : { + "default" : 10, + "description" : "Maximal time to wait until a guest system is stopped (minutes).", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "storage" : { + "description" : "Store resulting file to this storage.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string" + }, + "tmpdir" : { + "description" : "Store temporary files to specified directory.", + "optional" : 1, + "type" : "string" + }, + "vmid" : { + "description" : "The ID of the guest system you want to backup.", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string" + }, + "zstd" : { + "default" : 1, + "description" : "Zstd threads. N=0 uses half of the available cores, N>0 uses N as thread count.", + "optional" : 1, + "type" : "integer" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/vzdump/defaults", + "text" : "defaults" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Extract configuration from vzdump backup archive.", + "method" : "GET", + "name" : "extractconfig", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "volume" : { + "description" : "Volume identifier", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "The user needs 'VM.Backup' permissions on the backed up guest ID, and 'Datastore.AllocateSpace' on the backup storage.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/vzdump/extractconfig", + "text" : "extractconfig" + } + ], + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Create backup.", + "method" : "POST", + "name" : "vzdump", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "all" : { + "default" : 0, + "description" : "Backup all known guest systems on this host.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "bwlimit" : { + "default" : 0, + "description" : "Limit I/O bandwidth (in KiB/s).", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "compress" : { + "default" : "0", + "description" : "Compress dump file.", + "enum" : [ + "0", + "1", + "gzip", + "lzo", + "zstd" + ], + "optional" : 1, + "type" : "string" + }, + "dumpdir" : { + "description" : "Store resulting files to specified directory.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "exclude" : { + "description" : "Exclude specified guest systems (assumes --all)", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "exclude-path" : { + "description" : "Exclude certain files/directories (shell globs). Paths starting with '/' are anchored to the container's root, other paths match relative to each subdirectory.", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array", + "typetext" : "" + }, + "ionice" : { + "default" : 7, + "description" : "Set IO priority when using the BFQ scheduler. For snapshot and suspend mode backups of VMs, this only affects the compressor. A value of 8 means the idle priority is used, otherwise the best-effort priority is used with the specified value.", + "maximum" : 8, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 8)" + }, + "lockwait" : { + "default" : 180, + "description" : "Maximal time to wait for the global lock (minutes).", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "mailnotification" : { + "default" : "always", + "description" : "Deprecated: use 'notification-policy' instead.", + "enum" : [ + "always", + "failure" + ], + "optional" : 1, + "type" : "string" + }, + "mailto" : { + "description" : "Comma-separated list of email addresses or users that should receive email notifications. Has no effect if the 'notification-target' option is set at the same time.", + "format" : "email-or-username-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "maxfiles" : { + "description" : "Deprecated: use 'prune-backups' instead. Maximal number of backup files per guest system.", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "mode" : { + "default" : "snapshot", + "description" : "Backup mode.", + "enum" : [ + "snapshot", + "suspend", + "stop" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "Only run if executed on this node.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "notes-template" : { + "description" : "Template string for generating notes for the backup(s). It can contain variables which will be replaced by their values. Currently supported are {{cluster}}, {{guestname}}, {{node}}, and {{vmid}}, but more might be added in the future. Needs to be a single line, newline and backslash need to be escaped as '\\n' and '\\\\' respectively.", + "maxLength" : 1024, + "optional" : 1, + "requires" : "storage", + "type" : "string", + "typetext" : "" + }, + "notification-policy" : { + "default" : "always", + "description" : "Specify when to send a notification", + "enum" : [ + "always", + "failure", + "never" + ], + "optional" : 1, + "type" : "string" + }, + "notification-target" : { + "description" : "Determine the target to which notifications should be sent. Can either be a notification endpoint or a notification group. This option takes precedence over 'mailto', meaning that if both are set, the 'mailto' option will be ignored.", + "format" : "pve-configid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "performance" : { + "description" : "Other performance-related settings.", + "format" : "backup-performance", + "optional" : 1, + "type" : "string", + "typetext" : "[max-workers=] [,pbs-entries-max=]" + }, + "pigz" : { + "default" : 0, + "description" : "Use pigz instead of gzip when N>0. N=1 uses half of cores, N>1 uses N as thread count.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "pool" : { + "description" : "Backup all known guest systems included in the specified pool.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "protected" : { + "description" : "If true, mark backup(s) as protected.", + "optional" : 1, + "requires" : "storage", + "type" : "boolean", + "typetext" : "" + }, + "prune-backups" : { + "default" : "keep-all=1", + "description" : "Use these retention options instead of those from the storage configuration.", + "format" : "prune-backups", + "optional" : 1, + "type" : "string", + "typetext" : "[keep-all=<1|0>] [,keep-daily=] [,keep-hourly=] [,keep-last=] [,keep-monthly=] [,keep-weekly=] [,keep-yearly=]" + }, + "quiet" : { + "default" : 0, + "description" : "Be quiet.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "remove" : { + "default" : 1, + "description" : "Prune older backups according to 'prune-backups'.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "script" : { + "description" : "Use specified hook script.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "stdexcludes" : { + "default" : 1, + "description" : "Exclude temporary files and logs.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "stdout" : { + "description" : "Write tar to stdout, not to a file.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "stop" : { + "default" : 0, + "description" : "Stop running backup jobs on this host.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "stopwait" : { + "default" : 10, + "description" : "Maximal time to wait until a guest system is stopped (minutes).", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "storage" : { + "description" : "Store resulting file to this storage.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tmpdir" : { + "description" : "Store temporary files to specified directory.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "The ID of the guest system you want to backup.", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "zstd" : { + "default" : 1, + "description" : "Zstd threads. N=0 uses half of the available cores, N>0 uses N as thread count.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "The user needs 'VM.Backup' permissions on any VM, and 'Datastore.AllocateSpace' on the backup storage. The 'tmpdir', 'dumpdir' and 'script' parameters are restricted to the 'root@pam' user. The 'maxfiles' and 'prune-backups' settings require 'Datastore.Allocate' on the backup storage. The 'bwlimit', 'performance' and 'ionice' parameters require 'Sys.Modify' on '/'. ", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/vzdump", + "text" : "vzdump" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read service properties", + "method" : "GET", + "name" : "service_state", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "service" : { + "description" : "Service ID", + "enum" : [ + "chrony", + "corosync", + "cron", + "ksmtuned", + "postfix", + "pve-cluster", + "pve-firewall", + "pve-ha-crm", + "pve-ha-lrm", + "pvedaemon", + "pvefw-logger", + "pveproxy", + "pvescheduler", + "pvestatd", + "spiceproxy", + "sshd", + "syslog", + "systemd-journald", + "systemd-timesyncd" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/services/{service}/state", + "text" : "state" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Start service.", + "method" : "POST", + "name" : "service_start", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "service" : { + "description" : "Service ID", + "enum" : [ + "chrony", + "corosync", + "cron", + "ksmtuned", + "postfix", + "pve-cluster", + "pve-firewall", + "pve-ha-crm", + "pve-ha-lrm", + "pvedaemon", + "pvefw-logger", + "pveproxy", + "pvescheduler", + "pvestatd", + "spiceproxy", + "sshd", + "syslog", + "systemd-journald", + "systemd-timesyncd" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/services/{service}/start", + "text" : "start" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Stop service.", + "method" : "POST", + "name" : "service_stop", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "service" : { + "description" : "Service ID", + "enum" : [ + "chrony", + "corosync", + "cron", + "ksmtuned", + "postfix", + "pve-cluster", + "pve-firewall", + "pve-ha-crm", + "pve-ha-lrm", + "pvedaemon", + "pvefw-logger", + "pveproxy", + "pvescheduler", + "pvestatd", + "spiceproxy", + "sshd", + "syslog", + "systemd-journald", + "systemd-timesyncd" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/services/{service}/stop", + "text" : "stop" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Hard restart service. Use reload if you want to reduce interruptions.", + "method" : "POST", + "name" : "service_restart", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "service" : { + "description" : "Service ID", + "enum" : [ + "chrony", + "corosync", + "cron", + "ksmtuned", + "postfix", + "pve-cluster", + "pve-firewall", + "pve-ha-crm", + "pve-ha-lrm", + "pvedaemon", + "pvefw-logger", + "pveproxy", + "pvescheduler", + "pvestatd", + "spiceproxy", + "sshd", + "syslog", + "systemd-journald", + "systemd-timesyncd" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/services/{service}/restart", + "text" : "restart" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Reload service. Falls back to restart if service cannot be reloaded.", + "method" : "POST", + "name" : "service_reload", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "service" : { + "description" : "Service ID", + "enum" : [ + "chrony", + "corosync", + "cron", + "ksmtuned", + "postfix", + "pve-cluster", + "pve-firewall", + "pve-ha-crm", + "pve-ha-lrm", + "pvedaemon", + "pvefw-logger", + "pveproxy", + "pvescheduler", + "pvestatd", + "spiceproxy", + "sshd", + "syslog", + "systemd-journald", + "systemd-timesyncd" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/services/{service}/reload", + "text" : "reload" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index", + "method" : "GET", + "name" : "srvcmdidx", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "service" : { + "description" : "Service ID", + "enum" : [ + "chrony", + "corosync", + "cron", + "ksmtuned", + "postfix", + "pve-cluster", + "pve-firewall", + "pve-ha-crm", + "pve-ha-lrm", + "pvedaemon", + "pvefw-logger", + "pveproxy", + "pvescheduler", + "pvestatd", + "spiceproxy", + "sshd", + "syslog", + "systemd-journald", + "systemd-timesyncd" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "subdir" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{subdir}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/services/{service}", + "text" : "{service}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Service list.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{service}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/services", + "text" : "services" + }, + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete subscription key of this node.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read subscription info.", + "method" : "GET", + "name" : "get", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Update subscription info.", + "method" : "POST", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "default" : 0, + "description" : "Always connect to server, even if local cache is still valid.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set subscription key.", + "method" : "PUT", + "name" : "set", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "key" : { + "description" : "Proxmox VE subscription key", + "maxLength" : 32, + "pattern" : "\\s*pve([1248])([cbsp])-[0-9a-f]{10}\\s*", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/subscription", + "text" : "subscription" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete network device configuration", + "method" : "DELETE", + "name" : "delete_network", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "iface" : { + "description" : "Network interface name.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read network device configuration", + "method" : "GET", + "name" : "network_config", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "iface" : { + "description" : "Network interface name.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "properties" : { + "method" : { + "type" : "string" + }, + "type" : { + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update network device configuration", + "method" : "PUT", + "name" : "update_network", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "address" : { + "description" : "IP address.", + "format" : "ipv4", + "optional" : 1, + "requires" : "netmask", + "type" : "string", + "typetext" : "" + }, + "address6" : { + "description" : "IP address.", + "format" : "ipv6", + "optional" : 1, + "requires" : "netmask6", + "type" : "string", + "typetext" : "" + }, + "autostart" : { + "description" : "Automatically start interface on boot.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "bond-primary" : { + "description" : "Specify the primary interface for active-backup bond.", + "format" : "pve-iface", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bond_mode" : { + "description" : "Bonding mode.", + "enum" : [ + "balance-rr", + "active-backup", + "balance-xor", + "broadcast", + "802.3ad", + "balance-tlb", + "balance-alb", + "balance-slb", + "lacp-balance-slb", + "lacp-balance-tcp" + ], + "optional" : 1, + "type" : "string" + }, + "bond_xmit_hash_policy" : { + "description" : "Selects the transmit hash policy to use for slave selection in balance-xor and 802.3ad modes.", + "enum" : [ + "layer2", + "layer2+3", + "layer3+4" + ], + "optional" : 1, + "type" : "string" + }, + "bridge_ports" : { + "description" : "Specify the interfaces you want to add to your bridge.", + "format" : "pve-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bridge_vlan_aware" : { + "description" : "Enable bridge vlan support.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "cidr" : { + "description" : "IPv4 CIDR.", + "format" : "CIDRv4", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "cidr6" : { + "description" : "IPv6 CIDR.", + "format" : "CIDRv6", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comments" : { + "description" : "Comments", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comments6" : { + "description" : "Comments", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "gateway" : { + "description" : "Default gateway address.", + "format" : "ipv4", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "gateway6" : { + "description" : "Default ipv6 gateway address.", + "format" : "ipv6", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "type" : "string", + "typetext" : "" + }, + "mtu" : { + "description" : "MTU.", + "maximum" : 65520, + "minimum" : 1280, + "optional" : 1, + "type" : "integer", + "typetext" : " (1280 - 65520)" + }, + "netmask" : { + "description" : "Network mask.", + "format" : "ipv4mask", + "optional" : 1, + "requires" : "address", + "type" : "string", + "typetext" : "" + }, + "netmask6" : { + "description" : "Network mask.", + "maximum" : 128, + "minimum" : 0, + "optional" : 1, + "requires" : "address6", + "type" : "integer", + "typetext" : " (0 - 128)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "ovs_bonds" : { + "description" : "Specify the interfaces used by the bonding device.", + "format" : "pve-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ovs_bridge" : { + "description" : "The OVS bridge associated with a OVS port. This is required when you create an OVS port.", + "format" : "pve-iface", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ovs_options" : { + "description" : "OVS interface options.", + "maxLength" : 1024, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ovs_ports" : { + "description" : "Specify the interfaces you want to add to your bridge.", + "format" : "pve-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ovs_tag" : { + "description" : "Specify a VLan tag (used by OVSPort, OVSIntPort, OVSBond)", + "maximum" : 4094, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 4094)" + }, + "slaves" : { + "description" : "Specify the interfaces used by the bonding device.", + "format" : "pve-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Network interface type", + "enum" : [ + "bridge", + "bond", + "eth", + "alias", + "vlan", + "OVSBridge", + "OVSBond", + "OVSPort", + "OVSIntPort", + "unknown" + ], + "type" : "string" + }, + "vlan-id" : { + "description" : "vlan-id for a custom named vlan interface (ifupdown2 only).", + "maximum" : 4094, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 4094)" + }, + "vlan-raw-device" : { + "description" : "Specify the raw interface for the vlan interface.", + "format" : "pve-iface", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/network/{iface}", + "text" : "{iface}" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Revert network configuration changes.", + "method" : "DELETE", + "name" : "revert_network_changes", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "List available networks", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Only list specific interface types.", + "enum" : [ + "bridge", + "bond", + "eth", + "alias", + "vlan", + "OVSBridge", + "OVSBond", + "OVSPort", + "OVSIntPort", + "any_bridge", + "any_local_bridge" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{iface}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create network device configuration", + "method" : "POST", + "name" : "create_network", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "address" : { + "description" : "IP address.", + "format" : "ipv4", + "optional" : 1, + "requires" : "netmask", + "type" : "string", + "typetext" : "" + }, + "address6" : { + "description" : "IP address.", + "format" : "ipv6", + "optional" : 1, + "requires" : "netmask6", + "type" : "string", + "typetext" : "" + }, + "autostart" : { + "description" : "Automatically start interface on boot.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "bond-primary" : { + "description" : "Specify the primary interface for active-backup bond.", + "format" : "pve-iface", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bond_mode" : { + "description" : "Bonding mode.", + "enum" : [ + "balance-rr", + "active-backup", + "balance-xor", + "broadcast", + "802.3ad", + "balance-tlb", + "balance-alb", + "balance-slb", + "lacp-balance-slb", + "lacp-balance-tcp" + ], + "optional" : 1, + "type" : "string" + }, + "bond_xmit_hash_policy" : { + "description" : "Selects the transmit hash policy to use for slave selection in balance-xor and 802.3ad modes.", + "enum" : [ + "layer2", + "layer2+3", + "layer3+4" + ], + "optional" : 1, + "type" : "string" + }, + "bridge_ports" : { + "description" : "Specify the interfaces you want to add to your bridge.", + "format" : "pve-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bridge_vlan_aware" : { + "description" : "Enable bridge vlan support.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "cidr" : { + "description" : "IPv4 CIDR.", + "format" : "CIDRv4", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "cidr6" : { + "description" : "IPv6 CIDR.", + "format" : "CIDRv6", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comments" : { + "description" : "Comments", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comments6" : { + "description" : "Comments", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "gateway" : { + "description" : "Default gateway address.", + "format" : "ipv4", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "gateway6" : { + "description" : "Default ipv6 gateway address.", + "format" : "ipv6", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "type" : "string", + "typetext" : "" + }, + "mtu" : { + "description" : "MTU.", + "maximum" : 65520, + "minimum" : 1280, + "optional" : 1, + "type" : "integer", + "typetext" : " (1280 - 65520)" + }, + "netmask" : { + "description" : "Network mask.", + "format" : "ipv4mask", + "optional" : 1, + "requires" : "address", + "type" : "string", + "typetext" : "" + }, + "netmask6" : { + "description" : "Network mask.", + "maximum" : 128, + "minimum" : 0, + "optional" : 1, + "requires" : "address6", + "type" : "integer", + "typetext" : " (0 - 128)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "ovs_bonds" : { + "description" : "Specify the interfaces used by the bonding device.", + "format" : "pve-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ovs_bridge" : { + "description" : "The OVS bridge associated with a OVS port. This is required when you create an OVS port.", + "format" : "pve-iface", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ovs_options" : { + "description" : "OVS interface options.", + "maxLength" : 1024, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ovs_ports" : { + "description" : "Specify the interfaces you want to add to your bridge.", + "format" : "pve-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "ovs_tag" : { + "description" : "Specify a VLan tag (used by OVSPort, OVSIntPort, OVSBond)", + "maximum" : 4094, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 4094)" + }, + "slaves" : { + "description" : "Specify the interfaces used by the bonding device.", + "format" : "pve-iface-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Network interface type", + "enum" : [ + "bridge", + "bond", + "eth", + "alias", + "vlan", + "OVSBridge", + "OVSBond", + "OVSPort", + "OVSIntPort", + "unknown" + ], + "type" : "string" + }, + "vlan-id" : { + "description" : "vlan-id for a custom named vlan interface (ifupdown2 only).", + "maximum" : 4094, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 4094)" + }, + "vlan-raw-device" : { + "description" : "Specify the raw interface for the vlan interface.", + "format" : "pve-iface", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Reload network configuration", + "method" : "PUT", + "name" : "reload_network_config", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/network", + "text" : "network" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read task log.", + "method" : "GET", + "name" : "read_task_log", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "download" : { + "description" : "Whether the tasklog file should be downloaded. This parameter can't be used in conjunction with other parameters", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "limit" : { + "default" : 50, + "description" : "The amount of lines to read from the tasklog.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "start" : { + "default" : 0, + "description" : "Start at this line when reading the tasklog", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "upid" : { + "description" : "The task's unique ID.", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "The user needs 'Sys.Audit' permissions on '/nodes/' if they aren't the owner of the task.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "n" : { + "description" : "Line number", + "type" : "integer" + }, + "t" : { + "description" : "Line text", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/tasks/{upid}/log", + "text" : "log" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read task status.", + "method" : "GET", + "name" : "read_task_status", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "upid" : { + "description" : "The task's unique ID.", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "The user needs 'Sys.Audit' permissions on '/nodes/' if they are not the owner of the task.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "exitstatus" : { + "optional" : 1, + "type" : "string" + }, + "id" : { + "type" : "string" + }, + "node" : { + "type" : "string" + }, + "pid" : { + "type" : "integer" + }, + "starttime" : { + "type" : "number" + }, + "status" : { + "enum" : [ + "running", + "stopped" + ], + "type" : "string" + }, + "type" : { + "type" : "string" + }, + "upid" : { + "type" : "string" + }, + "user" : { + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/tasks/{upid}/status", + "text" : "status" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Stop a task.", + "method" : "DELETE", + "name" : "stop_task", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "upid" : { + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "The user needs 'Sys.Modify' permissions on '/nodes/' if they aren't the owner of the task.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "", + "method" : "GET", + "name" : "upid_index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "upid" : { + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/tasks/{upid}", + "text" : "{upid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read task list for one node (finished tasks).", + "method" : "GET", + "name" : "node_tasks", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "errors" : { + "default" : 0, + "description" : "Only list tasks with a status of ERROR.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "limit" : { + "default" : 50, + "description" : "Only list this amount of tasks.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "since" : { + "description" : "Only list tasks since this UNIX epoch.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "source" : { + "default" : "archive", + "description" : "List archived, active or all tasks.", + "enum" : [ + "archive", + "active", + "all" + ], + "optional" : 1, + "type" : "string" + }, + "start" : { + "default" : 0, + "description" : "List tasks beginning from this offset.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "statusfilter" : { + "description" : "List of Task States that should be returned.", + "format" : "pve-task-status-type-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "typefilter" : { + "description" : "Only list tasks of this type (e.g., vzstart, vzdump).", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "until" : { + "description" : "Only list tasks until this UNIX epoch.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "userfilter" : { + "description" : "Only list tasks from this user.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "Only list tasks for this VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "description" : "List task associated with the current user, or all task the user has 'Sys.Audit' permissions on /nodes/ (the the task runs on).", + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "endtime" : { + "optional" : 1, + "title" : "Endtime", + "type" : "integer" + }, + "id" : { + "title" : "ID", + "type" : "string" + }, + "node" : { + "title" : "Node", + "type" : "string" + }, + "pid" : { + "title" : "PID", + "type" : "integer" + }, + "pstart" : { + "type" : "integer" + }, + "starttime" : { + "title" : "Starttime", + "type" : "integer" + }, + "status" : { + "optional" : 1, + "title" : "Status", + "type" : "string" + }, + "type" : { + "title" : "Type", + "type" : "string" + }, + "upid" : { + "title" : "UPID", + "type" : "string" + }, + "user" : { + "title" : "User", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{upid}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/tasks", + "text" : "tasks" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Scan remote NFS server.", + "method" : "GET", + "name" : "nfsscan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "server" : { + "description" : "The server address (name or IP).", + "format" : "pve-storage-server", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "options" : { + "description" : "NFS export options.", + "type" : "string" + }, + "path" : { + "description" : "The exported path.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/scan/nfs", + "text" : "nfs" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Scan remote CIFS server.", + "method" : "GET", + "name" : "cifsscan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "domain" : { + "description" : "SMB domain (Workgroup).", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "User password.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "server" : { + "description" : "The server address (name or IP).", + "format" : "pve-storage-server", + "type" : "string", + "typetext" : "" + }, + "username" : { + "description" : "User name.", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "description" : { + "description" : "Descriptive text from server.", + "type" : "string" + }, + "share" : { + "description" : "The cifs share name.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/scan/cifs", + "text" : "cifs" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Scan remote Proxmox Backup Server.", + "method" : "GET", + "name" : "pbsscan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "fingerprint" : { + "description" : "Certificate SHA 256 fingerprint.", + "optional" : 1, + "pattern" : "([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "User password or API token secret.", + "type" : "string", + "typetext" : "" + }, + "port" : { + "default" : 8007, + "description" : "Optional port.", + "maximum" : 65535, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 65535)" + }, + "server" : { + "description" : "The server address (name or IP).", + "format" : "pve-storage-server", + "type" : "string", + "typetext" : "" + }, + "username" : { + "description" : "User-name or API token-ID.", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "comment" : { + "description" : "Comment from server.", + "optional" : 1, + "type" : "string" + }, + "store" : { + "description" : "The datastore name.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/scan/pbs", + "text" : "pbs" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Scan remote GlusterFS server.", + "method" : "GET", + "name" : "glusterfsscan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "server" : { + "description" : "The server address (name or IP).", + "format" : "pve-storage-server", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "volname" : { + "description" : "The volume name.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/scan/glusterfs", + "text" : "glusterfs" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Scan remote iSCSI server.", + "method" : "GET", + "name" : "iscsiscan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "portal" : { + "description" : "The iSCSI portal (IP or DNS name with optional port).", + "format" : "pve-storage-portal-dns", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "portal" : { + "description" : "The iSCSI portal name.", + "type" : "string" + }, + "target" : { + "description" : "The iSCSI target name.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/scan/iscsi", + "text" : "iscsi" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List local LVM volume groups.", + "method" : "GET", + "name" : "lvmscan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "vg" : { + "description" : "The LVM logical volume group name.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/scan/lvm", + "text" : "lvm" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List local LVM Thin Pools.", + "method" : "GET", + "name" : "lvmthinscan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vg" : { + "maxLength" : 100, + "pattern" : "[a-zA-Z0-9\\.\\+\\_][a-zA-Z0-9\\.\\+\\_\\-]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "lv" : { + "description" : "The LVM Thin Pool name (LVM logical volume).", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/scan/lvmthin", + "text" : "lvmthin" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Scan zfs pool list on local node.", + "method" : "GET", + "name" : "zfsscan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "pool" : { + "description" : "ZFS pool name.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/scan/zfs", + "text" : "zfs" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Index of available scan methods", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "method" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{method}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/scan", + "text" : "scan" + }, + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List mediated device types for given PCI device.", + "method" : "GET", + "name" : "mdevscan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pciid" : { + "description" : "The PCI ID to list the mdev types for.", + "pattern" : "(?:[0-9a-fA-F]{4}:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\\.[0-9a-fA-F]", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Sys.Modify" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "available" : { + "description" : "The number of still available instances of this type.", + "type" : "integer" + }, + "description" : { + "type" : "string" + }, + "type" : { + "description" : "The name of the mdev type.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/hardware/pci/{pciid}/mdev", + "text" : "mdev" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Index of available pci methods", + "method" : "GET", + "name" : "pciindex", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pciid" : { + "pattern" : "(?:[0-9a-fA-F]{4}:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\\.[0-9a-fA-F]", + "type" : "string" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "method" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{method}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/hardware/pci/{pciid}", + "text" : "{pciid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List local PCI devices.", + "method" : "GET", + "name" : "pciscan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pci-class-blacklist" : { + "default" : "05;06;0b", + "description" : "A list of blacklisted PCI classes, which will not be returned. Following are filtered by default: Memory Controller (05), Bridge (06) and Processor (0b).", + "format" : "string-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "verbose" : { + "default" : 1, + "description" : "If disabled, does only print the PCI IDs. Otherwise, additional information like vendor and device will be returned.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Sys.Modify" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "class" : { + "description" : "The PCI Class of the device.", + "type" : "string" + }, + "device" : { + "description" : "The Device ID.", + "type" : "string" + }, + "device_name" : { + "optional" : 1, + "type" : "string" + }, + "id" : { + "description" : "The PCI ID.", + "type" : "string" + }, + "iommugroup" : { + "description" : "The IOMMU group in which the device is in. If no IOMMU group is detected, it is set to -1.", + "type" : "integer" + }, + "mdev" : { + "description" : "If set, marks that the device is capable of creating mediated devices.", + "optional" : 1, + "type" : "boolean" + }, + "subsystem_device" : { + "description" : "The Subsystem Device ID.", + "optional" : 1, + "type" : "string" + }, + "subsystem_device_name" : { + "optional" : 1, + "type" : "string" + }, + "subsystem_vendor" : { + "description" : "The Subsystem Vendor ID.", + "optional" : 1, + "type" : "string" + }, + "subsystem_vendor_name" : { + "optional" : 1, + "type" : "string" + }, + "vendor" : { + "description" : "The Vendor ID.", + "type" : "string" + }, + "vendor_name" : { + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/hardware/pci", + "text" : "pci" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List local USB devices.", + "method" : "GET", + "name" : "usbscan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "busnum" : { + "type" : "integer" + }, + "class" : { + "type" : "integer" + }, + "devnum" : { + "type" : "integer" + }, + "level" : { + "type" : "integer" + }, + "manufacturer" : { + "optional" : 1, + "type" : "string" + }, + "port" : { + "type" : "integer" + }, + "prodid" : { + "type" : "string" + }, + "product" : { + "optional" : 1, + "type" : "string" + }, + "serial" : { + "optional" : 1, + "type" : "string" + }, + "speed" : { + "type" : "string" + }, + "usbpath" : { + "optional" : 1, + "type" : "string" + }, + "vendid" : { + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/hardware/usb", + "text" : "usb" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Index of hardware types", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "type" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{type}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/hardware", + "text" : "hardware" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List all custom and default CPU models.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Only returns custom models when the current user has Sys.Audit on /nodes.", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "custom" : { + "description" : "True if this is a custom CPU model.", + "type" : "boolean" + }, + "name" : { + "description" : "Name of the CPU model. Identifies it for subsequent API calls. Prefixed with 'custom-' for custom models.", + "type" : "string" + }, + "vendor" : { + "description" : "CPU vendor visible to the guest when this model is selected. Vendor of 'reported-model' in case of custom models.", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/capabilities/qemu/cpu", + "text" : "cpu" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get available QEMU/KVM machine types.", + "method" : "GET", + "name" : "types", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "items" : { + "additionalProperties" : 1, + "properties" : { + "id" : { + "description" : "Full name of machine type and version.", + "type" : "string" + }, + "type" : { + "description" : "The machine type.", + "enum" : [ + "q35", + "i440fx" + ], + "type" : "string" + }, + "version" : { + "description" : "The machine version.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/capabilities/qemu/machines", + "text" : "machines" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "QEMU capabilities index.", + "method" : "GET", + "name" : "qemu_caps_index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/capabilities/qemu", + "text" : "qemu" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Node capabilities index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/capabilities", + "text" : "capabilities" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Prune backups. Only those using the standard naming scheme are considered.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "prune-backups" : { + "description" : "Use these retention options instead of those from the storage configuration.", + "format" : "prune-backups", + "optional" : 1, + "type" : "string", + "typetext" : "[keep-all=<1|0>] [,keep-daily=] [,keep-hourly=] [,keep-last=] [,keep-monthly=] [,keep-weekly=] [,keep-yearly=]" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.", + "enum" : [ + "qemu", + "lxc" + ], + "optional" : 1, + "type" : "string" + }, + "vmid" : { + "description" : "Only prune backups for this VM.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "description" : "You need the 'Datastore.Allocate' privilege on the storage (or if a VM ID is specified, 'Datastore.AllocateSpace' and 'VM.Backup' for the VM).", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get prune information for backups. NOTE: this is only a preview and might not be what a subsequent prune call does if backups are removed/added in the meantime.", + "method" : "GET", + "name" : "dryrun", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "prune-backups" : { + "description" : "Use these retention options instead of those from the storage configuration.", + "format" : "prune-backups", + "optional" : 1, + "type" : "string", + "typetext" : "[keep-all=<1|0>] [,keep-daily=] [,keep-hourly=] [,keep-last=] [,keep-monthly=] [,keep-weekly=] [,keep-yearly=]" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.", + "enum" : [ + "qemu", + "lxc" + ], + "optional" : 1, + "type" : "string" + }, + "vmid" : { + "description" : "Only consider backups for this guest.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage/{storage}", + [ + "Datastore.Audit", + "Datastore.AllocateSpace" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "ctime" : { + "description" : "Creation time of the backup (seconds since the UNIX epoch).", + "type" : "integer" + }, + "mark" : { + "description" : "Whether the backup would be kept or removed. Backups that are protected or don't use the standard naming scheme are not removed.", + "enum" : [ + "keep", + "remove", + "protected", + "renamed" + ], + "type" : "string" + }, + "type" : { + "description" : "One of 'qemu', 'lxc', 'openvz' or 'unknown'.", + "type" : "string" + }, + "vmid" : { + "description" : "The VM the backup belongs to.", + "optional" : 1, + "type" : "integer" + }, + "volid" : { + "description" : "Backup volume ID.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/storage/{storage}/prunebackups", + "text" : "prunebackups" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete volume", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "delay" : { + "description" : "Time to wait for the task to finish. We return 'null' if the task finish within that time.", + "maximum" : 30, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 30)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "volume" : { + "description" : "Volume identifier", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "You need 'Datastore.Allocate' privilege on the storage (or 'Datastore.AllocateSpace' for backup volumes if you have VM.Backup privilege on the VM).", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "optional" : 1, + "type" : "string" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get volume attributes", + "method" : "GET", + "name" : "info", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "volume" : { + "description" : "Volume identifier", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "You need read access for the volume.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "format" : { + "description" : "Format identifier ('raw', 'qcow2', 'subvol', 'iso', 'tgz' ...)", + "type" : "string" + }, + "notes" : { + "description" : "Optional notes.", + "optional" : 1, + "type" : "string" + }, + "path" : { + "description" : "The Path", + "type" : "string" + }, + "protected" : { + "description" : "Protection status. Currently only supported for backups.", + "optional" : 1, + "type" : "boolean" + }, + "size" : { + "description" : "Volume size in bytes.", + "renderer" : "bytes", + "type" : "integer" + }, + "used" : { + "description" : "Used space. Please note that most storage plugins do not report anything useful here.", + "renderer" : "bytes", + "type" : "integer" + } + }, + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Copy a volume. This is experimental code - do not use.", + "method" : "POST", + "name" : "copy", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "target" : { + "description" : "Target volume identifier", + "type" : "string", + "typetext" : "" + }, + "target_node" : { + "description" : "Target node. Default is local node.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "volume" : { + "description" : "Source volume identifier", + "type" : "string", + "typetext" : "" + } + } + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update volume attributes", + "method" : "PUT", + "name" : "updateattributes", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "notes" : { + "description" : "The new notes.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "protected" : { + "description" : "Protection status. Currently only supported for backups.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "volume" : { + "description" : "Volume identifier", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "You need read access for the volume.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/storage/{storage}/content/{volume}", + "text" : "{volume}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List storage content.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "content" : { + "description" : "Only list content of this type.", + "format" : "pve-storage-content", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "Only list images for this VM", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage/{storage}", + [ + "Datastore.Audit", + "Datastore.AllocateSpace" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "ctime" : { + "description" : "Creation time (seconds since the UNIX Epoch).", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "encrypted" : { + "description" : "If whole backup is encrypted, value is the fingerprint or '1' if encrypted. Only useful for the Proxmox Backup Server storage type.", + "optional" : 1, + "type" : "string" + }, + "format" : { + "description" : "Format identifier ('raw', 'qcow2', 'subvol', 'iso', 'tgz' ...)", + "type" : "string" + }, + "notes" : { + "description" : "Optional notes. If they contain multiple lines, only the first one is returned here.", + "optional" : 1, + "type" : "string" + }, + "parent" : { + "description" : "Volume identifier of parent (for linked cloned).", + "optional" : 1, + "type" : "string" + }, + "protected" : { + "description" : "Protection status. Currently only supported for backups.", + "optional" : 1, + "type" : "boolean" + }, + "size" : { + "description" : "Volume size in bytes.", + "renderer" : "bytes", + "type" : "integer" + }, + "used" : { + "description" : "Used space. Please note that most storage plugins do not report anything useful here.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "verification" : { + "description" : "Last backup verification result, only useful for PBS storages.", + "optional" : 1, + "properties" : { + "state" : { + "description" : "Last backup verification state.", + "type" : "string" + }, + "upid" : { + "description" : "Last backup verification UPID.", + "type" : "string" + } + }, + "type" : "object" + }, + "vmid" : { + "description" : "Associated Owner VMID.", + "optional" : 1, + "type" : "integer" + }, + "volid" : { + "description" : "Volume identifier.", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{volid}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Allocate disk images.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "filename" : { + "description" : "The name of the file to create.", + "type" : "string", + "typetext" : "" + }, + "format" : { + "enum" : [ + "raw", + "qcow2", + "subvol" + ], + "optional" : 1, + "requires" : "size", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "size" : { + "description" : "Size in kilobyte (1024 bytes). Optional suffixes 'M' (megabyte, 1024K) and 'G' (gigabyte, 1024M)", + "pattern" : "\\d+[MG]?", + "type" : "string" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "vmid" : { + "description" : "Specify owner VM", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "type" : "integer", + "typetext" : " (100 - 999999999)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage/{storage}", + [ + "Datastore.AllocateSpace" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "description" : "Volume identifier", + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/storage/{storage}/content", + "text" : "content" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List files and directories for single file restore under the given path.", + "method" : "GET", + "name" : "list", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "filepath" : { + "description" : "base64-path to the directory or file being listed, or \"/\".", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "volume" : { + "description" : "Backup volume ID or name. Currently only PBS snapshots are supported.", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "You need read access for the volume.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "filepath" : { + "description" : "base64 path of the current entry", + "type" : "string" + }, + "leaf" : { + "description" : "If this entry is a leaf in the directory graph.", + "type" : "boolean" + }, + "mtime" : { + "description" : "Entry last-modified time (unix timestamp).", + "optional" : 1, + "type" : "integer" + }, + "size" : { + "description" : "Entry file size.", + "optional" : 1, + "type" : "integer" + }, + "text" : { + "description" : "Entry display text.", + "type" : "string" + }, + "type" : { + "description" : "Entry type.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/storage/{storage}/file-restore/list", + "text" : "list" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Extract a file or directory (as zip archive) from a PBS backup.", + "method" : "GET", + "name" : "download", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "filepath" : { + "description" : "base64-path to the directory or file to download.", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "tar" : { + "default" : 0, + "description" : "Download dirs as 'tar.zst' instead of 'zip'.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "volume" : { + "description" : "Backup volume ID or name. Currently only PBS snapshots are supported.", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "You need read access for the volume.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "any" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/storage/{storage}/file-restore/download", + "text" : "download" + } + ], + "leaf" : 0, + "path" : "/nodes/{node}/storage/{storage}/file-restore", + "text" : "file-restore" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read storage status.", + "method" : "GET", + "name" : "read_status", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage/{storage}", + [ + "Datastore.Audit", + "Datastore.AllocateSpace" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/storage/{storage}/status", + "text" : "status" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read storage RRD statistics (returns PNG).", + "method" : "GET", + "name" : "rrd", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cf" : { + "description" : "The RRD consolidation function", + "enum" : [ + "AVERAGE", + "MAX" + ], + "optional" : 1, + "type" : "string" + }, + "ds" : { + "description" : "The list of datasources you want to display.", + "format" : "pve-configid-list", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "timeframe" : { + "description" : "Specify the time frame you are interested in.", + "enum" : [ + "hour", + "day", + "week", + "month", + "year" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage/{storage}", + [ + "Datastore.Audit", + "Datastore.AllocateSpace" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "filename" : { + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/storage/{storage}/rrd", + "text" : "rrd" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read storage RRD statistics.", + "method" : "GET", + "name" : "rrddata", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cf" : { + "description" : "The RRD consolidation function", + "enum" : [ + "AVERAGE", + "MAX" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "timeframe" : { + "description" : "Specify the time frame you are interested in.", + "enum" : [ + "hour", + "day", + "week", + "month", + "year" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage/{storage}", + [ + "Datastore.Audit", + "Datastore.AllocateSpace" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/storage/{storage}/rrddata", + "text" : "rrddata" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Upload templates and ISO images.", + "method" : "POST", + "name" : "upload", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "checksum" : { + "description" : "The expected checksum of the file.", + "optional" : 1, + "requires" : "checksum-algorithm", + "type" : "string", + "typetext" : "" + }, + "checksum-algorithm" : { + "description" : "The algorithm to calculate the checksum of the file.", + "enum" : [ + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512" + ], + "optional" : 1, + "requires" : "checksum", + "type" : "string" + }, + "content" : { + "description" : "Content type.", + "enum" : [ + "iso", + "vztmpl" + ], + "format" : "pve-storage-content", + "type" : "string" + }, + "filename" : { + "description" : "The name of the file to create. Caution: This will be normalized!", + "maxLength" : 255, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "tmpfilename" : { + "description" : "The source file name. This parameter is usually set by the REST handler. You can only overwrite it when connecting to the trusted port on localhost.", + "optional" : 1, + "pattern" : "/var/tmp/pveupload-[0-9a-f]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage/{storage}", + [ + "Datastore.AllocateTemplate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/storage/{storage}/upload", + "text" : "upload" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Download templates and ISO images by using an URL.", + "method" : "POST", + "name" : "download_url", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "checksum" : { + "description" : "The expected checksum of the file.", + "optional" : 1, + "requires" : "checksum-algorithm", + "type" : "string", + "typetext" : "" + }, + "checksum-algorithm" : { + "description" : "The algorithm to calculate the checksum of the file.", + "enum" : [ + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512" + ], + "optional" : 1, + "requires" : "checksum", + "type" : "string" + }, + "compression" : { + "description" : "Decompress the downloaded file using the specified compression algorithm.", + "enum" : null, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "content" : { + "description" : "Content type.", + "enum" : [ + "iso", + "vztmpl" + ], + "format" : "pve-storage-content", + "type" : "string" + }, + "filename" : { + "description" : "The name of the file to create. Caution: This will be normalized!", + "maxLength" : 255, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "url" : { + "description" : "The URL to download the file from.", + "pattern" : "https?://.*", + "type" : "string" + }, + "verify-certificates" : { + "default" : 1, + "description" : "If false, no SSL/TLS certificates will be verified.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "and", + [ + "perm", + "/storage/{storage}", + [ + "Datastore.AllocateTemplate" + ] + ], + [ + "perm", + "/", + [ + "Sys.Audit", + "Sys.Modify" + ] + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/storage/{storage}/download-url", + "text" : "download-url" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "", + "method" : "GET", + "name" : "diridx", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage/{storage}", + [ + "Datastore.Audit", + "Datastore.AllocateSpace" + ], + "any", + 1 + ] + }, + "returns" : { + "items" : { + "properties" : { + "subdir" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{subdir}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/storage/{storage}", + "text" : "{storage}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get status for all datastores.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "content" : { + "description" : "Only list stores which support this content type.", + "format" : "pve-storage-content-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enabled" : { + "default" : 0, + "description" : "Only list stores which are enabled (not disabled in config).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "format" : { + "default" : 0, + "description" : "Include information about formats", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "Only list status for specified storage", + "format" : "pve-storage-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "target" : { + "description" : "If target is different to 'node', we only lists shared storages which content is accessible on this 'node' and the specified 'target' node.", + "format" : "pve-node", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Only list entries where you have 'Datastore.Audit' or 'Datastore.AllocateSpace' permissions on '/storage/'", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "active" : { + "description" : "Set when storage is accessible.", + "optional" : 1, + "type" : "boolean" + }, + "avail" : { + "description" : "Available storage space in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "content" : { + "description" : "Allowed storage content types.", + "format" : "pve-storage-content-list", + "type" : "string" + }, + "enabled" : { + "description" : "Set when storage is enabled (not disabled).", + "optional" : 1, + "type" : "boolean" + }, + "shared" : { + "description" : "Shared flag from storage configuration.", + "optional" : 1, + "type" : "boolean" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string" + }, + "total" : { + "description" : "Total storage space in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "type" : { + "description" : "Storage type.", + "type" : "string" + }, + "used" : { + "description" : "Used storage space in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "used_fraction" : { + "description" : "Used fraction (used/total).", + "optional" : 1, + "renderer" : "fraction_as_percentage", + "type" : "number" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{storage}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/storage", + "text" : "storage" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove an LVM Volume Group.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cleanup-config" : { + "default" : 0, + "description" : "Marks associated storage(s) as not available on this node anymore or removes them from the configuration (if configured for this node only).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "cleanup-disks" : { + "default" : 0, + "description" : "Also wipe disks so they can be repurposed afterwards.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ], + "description" : "Requires additionally 'Datastore.Allocate' on /storage when setting 'cleanup-config'" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/disks/lvm/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List LVM Volume Groups", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "children" : { + "items" : { + "properties" : { + "children" : { + "description" : "The underlying physical volumes", + "items" : { + "properties" : { + "free" : { + "description" : "The free bytes in the physical volume", + "type" : "integer" + }, + "leaf" : { + "type" : "boolean" + }, + "name" : { + "description" : "The name of the physical volume", + "type" : "string" + }, + "size" : { + "description" : "The size of the physical volume in bytes", + "type" : "integer" + } + }, + "type" : "object" + }, + "optional" : 1, + "type" : "array" + }, + "free" : { + "description" : "The free bytes in the volume group", + "type" : "integer" + }, + "leaf" : { + "type" : "boolean" + }, + "name" : { + "description" : "The name of the volume group", + "type" : "string" + }, + "size" : { + "description" : "The size of the volume group in bytes", + "type" : "integer" + } + }, + "type" : "object" + }, + "type" : "array" + }, + "leaf" : { + "type" : "boolean" + } + }, + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create an LVM Volume Group", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "add_storage" : { + "default" : 0, + "description" : "Configure storage using the Volume Group", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "device" : { + "description" : "The block device you want to create the volume group on", + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ], + "description" : "Requires additionally 'Datastore.Allocate' on /storage when setting 'add_storage'" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/disks/lvm", + "text" : "lvm" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove an LVM thin pool.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cleanup-config" : { + "default" : 0, + "description" : "Marks associated storage(s) as not available on this node anymore or removes them from the configuration (if configured for this node only).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "cleanup-disks" : { + "default" : 0, + "description" : "Also wipe disks so they can be repurposed afterwards.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "volume-group" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ], + "description" : "Requires additionally 'Datastore.Allocate' on /storage when setting 'cleanup-config'" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/disks/lvmthin/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List LVM thinpools", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "lv" : { + "description" : "The name of the thinpool.", + "type" : "string" + }, + "lv_size" : { + "description" : "The size of the thinpool in bytes.", + "type" : "integer" + }, + "metadata_size" : { + "description" : "The size of the metadata lv in bytes.", + "type" : "integer" + }, + "metadata_used" : { + "description" : "The used bytes of the metadata lv.", + "type" : "integer" + }, + "used" : { + "description" : "The used bytes of the thinpool.", + "type" : "integer" + }, + "vg" : { + "description" : "The associated volume group.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create an LVM thinpool", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "add_storage" : { + "default" : 0, + "description" : "Configure storage using the thinpool.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "device" : { + "description" : "The block device you want to create the thinpool on.", + "type" : "string", + "typetext" : "" + }, + "name" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ], + "description" : "Requires additionally 'Datastore.Allocate' on /storage when setting 'add_storage'" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/disks/lvmthin", + "text" : "lvmthin" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Unmounts the storage and removes the mount unit.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cleanup-config" : { + "default" : 0, + "description" : "Marks associated storage(s) as not available on this node anymore or removes them from the configuration (if configured for this node only).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "cleanup-disks" : { + "default" : 0, + "description" : "Also wipe disk so it can be repurposed afterwards.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ], + "description" : "Requires additionally 'Datastore.Allocate' on /storage when setting 'cleanup-config'" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/disks/directory/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "PVE Managed Directory storages.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "device" : { + "description" : "The mounted device.", + "type" : "string" + }, + "options" : { + "description" : "The mount options.", + "type" : "string" + }, + "path" : { + "description" : "The mount path.", + "type" : "string" + }, + "type" : { + "description" : "The filesystem type.", + "type" : "string" + }, + "unitfile" : { + "description" : "The path of the mount unit.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a Filesystem on an unused disk. Will be mounted under '/mnt/pve/NAME'.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "add_storage" : { + "default" : 0, + "description" : "Configure storage using the directory.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "device" : { + "description" : "The block device you want to create the filesystem on.", + "type" : "string", + "typetext" : "" + }, + "filesystem" : { + "default" : "ext4", + "description" : "The desired filesystem.", + "enum" : [ + "ext4", + "xfs" + ], + "optional" : 1, + "type" : "string" + }, + "name" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ], + "description" : "Requires additionally 'Datastore.Allocate' on /storage when setting 'add_storage'" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/disks/directory", + "text" : "directory" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Destroy a ZFS pool.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cleanup-config" : { + "default" : 0, + "description" : "Marks associated storage(s) as not available on this node anymore or removes them from the configuration (if configured for this node only).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "cleanup-disks" : { + "default" : 0, + "description" : "Also wipe disks so they can be repurposed afterwards.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "name" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ], + "description" : "Requires additionally 'Datastore.Allocate' on /storage when setting 'cleanup-config'" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get details about a zpool.", + "method" : "GET", + "name" : "detail", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "action" : { + "description" : "Information about the recommended action to fix the state.", + "optional" : 1, + "type" : "string" + }, + "children" : { + "description" : "The pool configuration information, including the vdevs for each section (e.g. spares, cache), may be nested.", + "items" : { + "properties" : { + "cksum" : { + "optional" : 1, + "type" : "number" + }, + "msg" : { + "description" : "An optional message about the vdev.", + "type" : "string" + }, + "name" : { + "description" : "The name of the vdev or section.", + "type" : "string" + }, + "read" : { + "optional" : 1, + "type" : "number" + }, + "state" : { + "description" : "The state of the vdev.", + "optional" : 1, + "type" : "string" + }, + "write" : { + "optional" : 1, + "type" : "number" + } + }, + "type" : "object" + }, + "type" : "array" + }, + "errors" : { + "description" : "Information about the errors on the zpool.", + "type" : "string" + }, + "name" : { + "description" : "The name of the zpool.", + "type" : "string" + }, + "scan" : { + "description" : "Information about the last/current scrub.", + "optional" : 1, + "type" : "string" + }, + "state" : { + "description" : "The state of the zpool.", + "type" : "string" + }, + "status" : { + "description" : "Information about the state of the zpool.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/disks/zfs/{name}", + "text" : "{name}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List Zpools.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "alloc" : { + "description" : "", + "type" : "integer" + }, + "dedup" : { + "description" : "", + "type" : "number" + }, + "frag" : { + "description" : "", + "type" : "integer" + }, + "free" : { + "description" : "", + "type" : "integer" + }, + "health" : { + "description" : "", + "type" : "string" + }, + "name" : { + "description" : "", + "type" : "string" + }, + "size" : { + "description" : "", + "type" : "integer" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a ZFS pool.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "add_storage" : { + "default" : 0, + "description" : "Configure storage using the zpool.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ashift" : { + "default" : 12, + "description" : "Pool sector size exponent.", + "maximum" : 16, + "minimum" : 9, + "optional" : 1, + "type" : "integer", + "typetext" : " (9 - 16)" + }, + "compression" : { + "default" : "on", + "description" : "The compression algorithm to use.", + "enum" : [ + "on", + "off", + "gzip", + "lz4", + "lzjb", + "zle", + "zstd" + ], + "optional" : 1, + "type" : "string" + }, + "devices" : { + "description" : "The block devices you want to create the zpool on.", + "format" : "string-list", + "type" : "string", + "typetext" : "" + }, + "draid-config" : { + "format" : { + "data" : { + "description" : "The number of data devices per redundancy group. (dRAID)", + "minimum" : 1, + "type" : "integer" + }, + "spares" : { + "description" : "Number of dRAID spares.", + "minimum" : 0, + "type" : "integer" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "data= ,spares=" + }, + "name" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "raidlevel" : { + "description" : "The RAID level to use.", + "enum" : [ + "single", + "mirror", + "raid10", + "raidz", + "raidz2", + "raidz3", + "draid", + "draid2", + "draid3" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ], + "description" : "Requires additionally 'Datastore.Allocate' on /storage when setting 'add_storage'" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/disks/zfs", + "text" : "zfs" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List local disks.", + "method" : "GET", + "name" : "list", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "include-partitions" : { + "default" : 0, + "description" : "Also include partitions.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "skipsmart" : { + "default" : 0, + "description" : "Skip smart checks.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "type" : { + "description" : "Only list specific types of disks.", + "enum" : [ + "unused", + "journal_disks" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "perm", + "/", + [ + "Sys.Audit" + ] + ], + [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "devpath" : { + "description" : "The device path", + "type" : "string" + }, + "gpt" : { + "type" : "boolean" + }, + "health" : { + "optional" : 1, + "type" : "string" + }, + "model" : { + "optional" : 1, + "type" : "string" + }, + "mounted" : { + "type" : "boolean" + }, + "osdid" : { + "type" : "integer" + }, + "osdid-list" : { + "items" : { + "type" : "integer" + }, + "type" : "array" + }, + "parent" : { + "description" : "For partitions only. The device path of the disk the partition resides on.", + "optional" : 1, + "type" : "string" + }, + "serial" : { + "optional" : 1, + "type" : "string" + }, + "size" : { + "type" : "integer" + }, + "used" : { + "optional" : 1, + "type" : "string" + }, + "vendor" : { + "optional" : 1, + "type" : "string" + }, + "wwn" : { + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/disks/list", + "text" : "list" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get SMART Health of a disk.", + "method" : "GET", + "name" : "smart", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "disk" : { + "description" : "Block device name", + "pattern" : "^/dev/[a-zA-Z0-9\\/]+$", + "type" : "string" + }, + "healthonly" : { + "description" : "If true returns only the health status", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "attributes" : { + "optional" : 1, + "type" : "array" + }, + "health" : { + "type" : "string" + }, + "text" : { + "optional" : 1, + "type" : "string" + }, + "type" : { + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/disks/smart", + "text" : "smart" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Initialize Disk with GPT", + "method" : "POST", + "name" : "initgpt", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "disk" : { + "description" : "Block device name", + "pattern" : "^/dev/[a-zA-Z0-9\\/]+$", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "uuid" : { + "description" : "UUID for the GPT table", + "maxLength" : 36, + "optional" : 1, + "pattern" : "[a-fA-F0-9\\-]+", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/disks/initgpt", + "text" : "initgpt" + }, + { + "info" : { + "PUT" : { + "allowtoken" : 1, + "description" : "Wipe a disk or partition.", + "method" : "PUT", + "name" : "wipe_disk", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "disk" : { + "description" : "Block device name", + "pattern" : "^/dev/[a-zA-Z0-9\\/]+$", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/disks/wipedisk", + "text" : "wipedisk" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Node index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/disks", + "text" : "disks" + }, + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List available updates.", + "method" : "GET", + "name" : "list_updates", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "This is used to resynchronize the package index files from their sources (apt-get update).", + "method" : "POST", + "name" : "update_database", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "notify" : { + "default" : 0, + "description" : "Send notification about new packages.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "quiet" : { + "default" : 0, + "description" : "Only produces output suitable for logging, omitting progress indicators.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/apt/update", + "text" : "update" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get package changelogs.", + "method" : "GET", + "name" : "changelog", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "name" : { + "description" : "Package name.", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "version" : { + "description" : "Package version.", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/apt/changelog", + "text" : "changelog" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get APT repository information.", + "method" : "GET", + "name" : "repositories", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "description" : "Result from parsing the APT repository files in /etc/apt/.", + "properties" : { + "digest" : { + "description" : "Common digest of all files.", + "type" : "string" + }, + "errors" : { + "description" : "List of problematic repository files.", + "items" : { + "properties" : { + "error" : { + "description" : "The error message", + "type" : "string" + }, + "path" : { + "description" : "Path to the problematic file.", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + }, + "files" : { + "description" : "List of parsed repository files.", + "items" : { + "properties" : { + "digest" : { + "description" : "Digest of the file as bytes.", + "items" : { + "type" : "integer" + }, + "type" : "array" + }, + "file-type" : { + "description" : "Format of the file.", + "enum" : [ + "list", + "sources" + ], + "type" : "string" + }, + "path" : { + "description" : "Path to the problematic file.", + "type" : "string" + }, + "repositories" : { + "description" : "The parsed repositories.", + "items" : { + "properties" : { + "Comment" : { + "description" : "Associated comment", + "optional" : 1, + "type" : "string" + }, + "Components" : { + "description" : "List of repository components", + "items" : { + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "Enabled" : { + "description" : "Whether the repository is enabled or not", + "type" : "boolean" + }, + "FileType" : { + "description" : "Format of the defining file.", + "enum" : [ + "list", + "sources" + ], + "type" : "string" + }, + "Options" : { + "description" : "Additional options", + "items" : { + "properties" : { + "Key" : { + "type" : "string" + }, + "Values" : { + "items" : { + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "optional" : 1, + "type" : "array" + }, + "Suites" : { + "description" : "List of package distribuitions", + "items" : { + "type" : "string" + }, + "type" : "array" + }, + "Types" : { + "description" : "List of package types.", + "items" : { + "enum" : [ + "deb", + "deb-src" + ], + "type" : "string" + }, + "type" : "array" + }, + "URIs" : { + "description" : "List of repository URIs.", + "items" : { + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "type" : "array" + }, + "infos" : { + "description" : "Additional information/warnings for APT repositories.", + "items" : { + "properties" : { + "index" : { + "description" : "Index of the associated repository within the file.", + "type" : "string" + }, + "kind" : { + "description" : "Kind of the information (e.g. warning).", + "type" : "string" + }, + "message" : { + "description" : "Information message.", + "type" : "string" + }, + "path" : { + "description" : "Path to the associated file.", + "type" : "string" + }, + "property" : { + "description" : "Property from which the info originates.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + }, + "standard-repos" : { + "description" : "List of standard repositories and their configuration status", + "items" : { + "properties" : { + "handle" : { + "description" : "Handle to identify the repository.", + "type" : "string" + }, + "name" : { + "description" : "Full name of the repository.", + "type" : "string" + }, + "status" : { + "description" : "Indicating enabled/disabled status, if the repository is configured.", + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + }, + "type" : "array" + } + }, + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Change the properties of a repository. Currently only allows enabling/disabling.", + "method" : "POST", + "name" : "change_repository", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Digest to detect modifications.", + "maxLength" : 80, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enabled" : { + "description" : "Whether the repository should be enabled or not.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "index" : { + "description" : "Index within the file (starting from 0).", + "type" : "integer", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "path" : { + "description" : "Path to the containing file.", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Add a standard repository to the configuration", + "method" : "PUT", + "name" : "add_repository", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Digest to detect modifications.", + "maxLength" : 80, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "handle" : { + "description" : "Handle that identifies a repository.", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/apt/repositories", + "text" : "repositories" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get package information for important Proxmox packages.", + "method" : "GET", + "name" : "versions", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/apt/versions", + "text" : "versions" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index for apt (Advanced Package Tool).", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "id" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/apt", + "text" : "apt" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete rule.", + "method" : "DELETE", + "name" : "delete_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get single rule data.", + "method" : "GET", + "name" : "get_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "properties" : { + "action" : { + "type" : "string" + }, + "comment" : { + "optional" : 1, + "type" : "string" + }, + "dest" : { + "optional" : 1, + "type" : "string" + }, + "dport" : { + "optional" : 1, + "type" : "string" + }, + "enable" : { + "optional" : 1, + "type" : "integer" + }, + "icmp-type" : { + "optional" : 1, + "type" : "string" + }, + "iface" : { + "optional" : 1, + "type" : "string" + }, + "ipversion" : { + "optional" : 1, + "type" : "integer" + }, + "log" : { + "description" : "Log level for firewall rule", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "optional" : 1, + "type" : "string" + }, + "pos" : { + "type" : "integer" + }, + "proto" : { + "optional" : 1, + "type" : "string" + }, + "source" : { + "optional" : 1, + "type" : "string" + }, + "sport" : { + "optional" : 1, + "type" : "string" + }, + "type" : { + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Modify rule data.", + "method" : "PUT", + "name" : "update_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "action" : { + "description" : "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "comment" : { + "description" : "Descriptive comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dest" : { + "description" : "Restrict packet destination address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dport" : { + "description" : "Restrict TCP/UDP destination port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-dport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Flag to enable/disable a rule.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "icmp-type" : { + "description" : "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'.", + "format" : "pve-fw-icmp-type-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name. You have to use network configuration key names for VMs and containers ('net\\d+'). Host related rules can use arbitrary strings.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "log" : { + "description" : "Log level for firewall rule.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "description" : "Use predefined standard macro.", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "moveto" : { + "description" : "Move rule to new position . Other arguments are ignored.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "proto" : { + "description" : "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.", + "format" : "pve-fw-protocol-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "Restrict packet source address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sport" : { + "description" : "Restrict TCP/UDP source port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-sport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Rule type.", + "enum" : [ + "in", + "out", + "group" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/firewall/rules/{pos}", + "text" : "{pos}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List rules.", + "method" : "GET", + "name" : "get_rules", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "pos" : { + "type" : "integer" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{pos}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new rule.", + "method" : "POST", + "name" : "create_rule", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "action" : { + "description" : "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.", + "maxLength" : 20, + "minLength" : 2, + "optional" : 0, + "pattern" : "[A-Za-z][A-Za-z0-9\\-\\_]+", + "type" : "string" + }, + "comment" : { + "description" : "Descriptive comment.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dest" : { + "description" : "Restrict packet destination address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dport" : { + "description" : "Restrict TCP/UDP destination port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-dport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Flag to enable/disable a rule.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "icmp-type" : { + "description" : "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'.", + "format" : "pve-fw-icmp-type-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iface" : { + "description" : "Network interface name. You have to use network configuration key names for VMs and containers ('net\\d+'). Host related rules can use arbitrary strings.", + "format" : "pve-iface", + "maxLength" : 20, + "minLength" : 2, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "log" : { + "description" : "Log level for firewall rule.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "macro" : { + "description" : "Use predefined standard macro.", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "pos" : { + "description" : "Update rule at position .", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "proto" : { + "description" : "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.", + "format" : "pve-fw-protocol-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "source" : { + "description" : "Restrict packet source address. This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.", + "format" : "pve-fw-addr-spec", + "maxLength" : 512, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sport" : { + "description" : "Restrict TCP/UDP source port. You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.", + "format" : "pve-fw-sport-spec", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "Rule type.", + "enum" : [ + "in", + "out", + "group" + ], + "optional" : 0, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/firewall/rules", + "text" : "rules" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get host firewall options.", + "method" : "GET", + "name" : "get_options", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "properties" : { + "enable" : { + "description" : "Enable host firewall rules.", + "optional" : 1, + "type" : "boolean" + }, + "log_level_in" : { + "description" : "Log level for incoming traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "log_level_out" : { + "description" : "Log level for outgoing traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "log_nf_conntrack" : { + "default" : 0, + "description" : "Enable logging of conntrack information.", + "optional" : 1, + "type" : "boolean" + }, + "ndp" : { + "default" : 0, + "description" : "Enable NDP (Neighbor Discovery Protocol).", + "optional" : 1, + "type" : "boolean" + }, + "nf_conntrack_allow_invalid" : { + "default" : 0, + "description" : "Allow invalid packets on connection tracking.", + "optional" : 1, + "type" : "boolean" + }, + "nf_conntrack_helpers" : { + "default" : "", + "description" : "Enable conntrack helpers for specific protocols. Supported protocols: amanda, ftp, irc, netbios-ns, pptp, sane, sip, snmp, tftp", + "format" : "pve-fw-conntrack-helper", + "optional" : 1, + "type" : "string" + }, + "nf_conntrack_max" : { + "default" : 262144, + "description" : "Maximum number of tracked connections.", + "minimum" : 32768, + "optional" : 1, + "type" : "integer" + }, + "nf_conntrack_tcp_timeout_established" : { + "default" : 432000, + "description" : "Conntrack established timeout.", + "minimum" : 7875, + "optional" : 1, + "type" : "integer" + }, + "nf_conntrack_tcp_timeout_syn_recv" : { + "default" : 60, + "description" : "Conntrack syn recv timeout.", + "maximum" : 60, + "minimum" : 30, + "optional" : 1, + "type" : "integer" + }, + "nosmurfs" : { + "description" : "Enable SMURFS filter.", + "optional" : 1, + "type" : "boolean" + }, + "protection_synflood" : { + "default" : 0, + "description" : "Enable synflood protection", + "optional" : 1, + "type" : "boolean" + }, + "protection_synflood_burst" : { + "default" : 1000, + "description" : "Synflood protection rate burst by ip src.", + "optional" : 1, + "type" : "integer" + }, + "protection_synflood_rate" : { + "default" : 200, + "description" : "Synflood protection rate syn/sec by ip src.", + "optional" : 1, + "type" : "integer" + }, + "smurf_log_level" : { + "description" : "Log level for SMURFS filter.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "tcp_flags_log_level" : { + "description" : "Log level for illegal tcp flags filter.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "tcpflags" : { + "default" : 0, + "description" : "Filter illegal combinations of TCP flags.", + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set Firewall options.", + "method" : "PUT", + "name" : "set_options", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Enable host firewall rules.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "log_level_in" : { + "description" : "Log level for incoming traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "log_level_out" : { + "description" : "Log level for outgoing traffic.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "log_nf_conntrack" : { + "default" : 0, + "description" : "Enable logging of conntrack information.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "ndp" : { + "default" : 0, + "description" : "Enable NDP (Neighbor Discovery Protocol).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "nf_conntrack_allow_invalid" : { + "default" : 0, + "description" : "Allow invalid packets on connection tracking.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "nf_conntrack_helpers" : { + "default" : "", + "description" : "Enable conntrack helpers for specific protocols. Supported protocols: amanda, ftp, irc, netbios-ns, pptp, sane, sip, snmp, tftp", + "format" : "pve-fw-conntrack-helper", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "nf_conntrack_max" : { + "default" : 262144, + "description" : "Maximum number of tracked connections.", + "minimum" : 32768, + "optional" : 1, + "type" : "integer", + "typetext" : " (32768 - N)" + }, + "nf_conntrack_tcp_timeout_established" : { + "default" : 432000, + "description" : "Conntrack established timeout.", + "minimum" : 7875, + "optional" : 1, + "type" : "integer", + "typetext" : " (7875 - N)" + }, + "nf_conntrack_tcp_timeout_syn_recv" : { + "default" : 60, + "description" : "Conntrack syn recv timeout.", + "maximum" : 60, + "minimum" : 30, + "optional" : 1, + "type" : "integer", + "typetext" : " (30 - 60)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "nosmurfs" : { + "description" : "Enable SMURFS filter.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "protection_synflood" : { + "default" : 0, + "description" : "Enable synflood protection", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "protection_synflood_burst" : { + "default" : 1000, + "description" : "Synflood protection rate burst by ip src.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "protection_synflood_rate" : { + "default" : 200, + "description" : "Synflood protection rate syn/sec by ip src.", + "optional" : 1, + "type" : "integer", + "typetext" : "" + }, + "smurf_log_level" : { + "description" : "Log level for SMURFS filter.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "tcp_flags_log_level" : { + "description" : "Log level for illegal tcp flags filter.", + "enum" : [ + "emerg", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "nolog" + ], + "optional" : 1, + "type" : "string" + }, + "tcpflags" : { + "default" : 0, + "description" : "Filter illegal combinations of TCP flags.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/firewall/options", + "text" : "options" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read firewall log", + "method" : "GET", + "name" : "log", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "limit" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "since" : { + "description" : "Display log since this UNIX epoch.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "start" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "until" : { + "description" : "Display log until this UNIX epoch.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Syslog" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "n" : { + "description" : "Line number", + "type" : "integer" + }, + "t" : { + "description" : "Line text", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/firewall/log", + "text" : "log" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/firewall", + "text" : "firewall" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get replication job status.", + "method" : "GET", + "name" : "job_status", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '-'.", + "format" : "pve-replication-job-id", + "pattern" : "[1-9][0-9]{2,8}-\\d{1,9}", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Requires the VM.Audit permission on /vms/.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/replication/{id}/status", + "text" : "status" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read replication job log.", + "method" : "GET", + "name" : "read_job_log", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '-'.", + "format" : "pve-replication-job-id", + "pattern" : "[1-9][0-9]{2,8}-\\d{1,9}", + "type" : "string" + }, + "limit" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "start" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "permissions" : { + "description" : "Requires the VM.Audit permission on /vms/, or 'Sys.Audit' on '/nodes/'", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "n" : { + "description" : "Line number", + "type" : "integer" + }, + "t" : { + "description" : "Line text", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/replication/{id}/log", + "text" : "log" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Schedule replication job to start as soon as possible.", + "method" : "POST", + "name" : "schedule_now", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '-'.", + "format" : "pve-replication-job-id", + "pattern" : "[1-9][0-9]{2,8}-\\d{1,9}", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/replication/{id}/schedule_now", + "text" : "schedule_now" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '-'.", + "format" : "pve-replication-job-id", + "pattern" : "[1-9][0-9]{2,8}-\\d{1,9}", + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/replication/{id}", + "text" : "{id}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List status of all replication jobs on this node.", + "method" : "GET", + "name" : "status", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "guest" : { + "description" : "Only list replication jobs for this guest.", + "format" : "pve-vmid", + "maximum" : 999999999, + "minimum" : 100, + "optional" : 1, + "type" : "integer", + "typetext" : " (100 - 999999999)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Requires the VM.Audit permission on /vms/.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "id" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/replication", + "text" : "replication" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Revoke existing certificate from CA.", + "method" : "DELETE", + "name" : "revoke_certificate", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Order a new certificate from ACME-compatible CA.", + "method" : "POST", + "name" : "new_certificate", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "default" : 0, + "description" : "Overwrite existing custom certificate.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Renew existing certificate from CA.", + "method" : "PUT", + "name" : "renew_certificate", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "default" : 0, + "description" : "Force renewal even if expiry is more than 30 days away.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/certificates/acme/certificate", + "text" : "certificate" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "ACME index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/certificates/acme", + "text" : "acme" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get information about node's certificates.", + "method" : "GET", + "name" : "info", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "filename" : { + "optional" : 1, + "type" : "string" + }, + "fingerprint" : { + "description" : "Certificate SHA 256 fingerprint.", + "optional" : 1, + "pattern" : "([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}", + "type" : "string" + }, + "issuer" : { + "description" : "Certificate issuer name.", + "optional" : 1, + "type" : "string" + }, + "notafter" : { + "description" : "Certificate's notAfter timestamp (UNIX epoch).", + "optional" : 1, + "renderer" : "timestamp", + "type" : "integer" + }, + "notbefore" : { + "description" : "Certificate's notBefore timestamp (UNIX epoch).", + "optional" : 1, + "renderer" : "timestamp", + "type" : "integer" + }, + "pem" : { + "description" : "Certificate in PEM format", + "format" : "pem-certificate", + "optional" : 1, + "type" : "string" + }, + "public-key-bits" : { + "description" : "Certificate's public key size", + "optional" : 1, + "type" : "integer" + }, + "public-key-type" : { + "description" : "Certificate's public key algorithm", + "optional" : 1, + "type" : "string" + }, + "san" : { + "description" : "List of Certificate's SubjectAlternativeName entries.", + "items" : { + "type" : "string" + }, + "optional" : 1, + "renderer" : "yaml", + "type" : "array" + }, + "subject" : { + "description" : "Certificate subject name.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/certificates/info", + "text" : "info" + }, + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "DELETE custom certificate chain and key.", + "method" : "DELETE", + "name" : "remove_custom_cert", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "restart" : { + "default" : 0, + "description" : "Restart pveproxy.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Upload or update custom certificate chain and key.", + "method" : "POST", + "name" : "upload_custom_cert", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "certificates" : { + "description" : "PEM encoded certificate (chain).", + "format" : "pem-certificate-chain", + "type" : "string", + "typetext" : "" + }, + "force" : { + "default" : 0, + "description" : "Overwrite existing custom or ACME certificate files.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "key" : { + "description" : "PEM encoded private key.", + "format" : "pem-string", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "restart" : { + "default" : 0, + "description" : "Restart pveproxy.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "filename" : { + "optional" : 1, + "type" : "string" + }, + "fingerprint" : { + "description" : "Certificate SHA 256 fingerprint.", + "optional" : 1, + "pattern" : "([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}", + "type" : "string" + }, + "issuer" : { + "description" : "Certificate issuer name.", + "optional" : 1, + "type" : "string" + }, + "notafter" : { + "description" : "Certificate's notAfter timestamp (UNIX epoch).", + "optional" : 1, + "renderer" : "timestamp", + "type" : "integer" + }, + "notbefore" : { + "description" : "Certificate's notBefore timestamp (UNIX epoch).", + "optional" : 1, + "renderer" : "timestamp", + "type" : "integer" + }, + "pem" : { + "description" : "Certificate in PEM format", + "format" : "pem-certificate", + "optional" : 1, + "type" : "string" + }, + "public-key-bits" : { + "description" : "Certificate's public key size", + "optional" : 1, + "type" : "integer" + }, + "public-key-type" : { + "description" : "Certificate's public key algorithm", + "optional" : 1, + "type" : "string" + }, + "san" : { + "description" : "List of Certificate's SubjectAlternativeName entries.", + "items" : { + "type" : "string" + }, + "optional" : 1, + "renderer" : "yaml", + "type" : "array" + }, + "subject" : { + "description" : "Certificate subject name.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/certificates/custom", + "text" : "custom" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Node index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/certificates", + "text" : "certificates" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get node configuration options.", + "method" : "GET", + "name" : "get_config", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "property" : { + "default" : "all", + "description" : "Return only a specific property from the node configuration.", + "enum" : [ + "acme", + "acmedomain0", + "acmedomain1", + "acmedomain2", + "acmedomain3", + "acmedomain4", + "acmedomain5", + "description", + "startall-onboot-delay", + "wakeonlan" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "properties" : { + "acme" : { + "description" : "Node specific ACME settings.", + "format" : { + "account" : { + "default" : "default", + "description" : "ACME account config file name.", + "format" : "pve-configid", + "format_description" : "name", + "optional" : 1, + "type" : "string" + }, + "domains" : { + "description" : "List of domains for this node's ACME certificate", + "format" : "pve-acme-domain-list", + "format_description" : "domain[;domain;...]", + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "acmedomain[n]" : { + "description" : "ACME domain and validation plugin", + "format" : { + "alias" : { + "description" : "Alias for the Domain to verify ACME Challenge over DNS", + "format" : "pve-acme-alias", + "format_description" : "domain", + "optional" : 1, + "type" : "string" + }, + "domain" : { + "default_key" : 1, + "description" : "domain for this node's ACME certificate", + "format" : "pve-acme-domain", + "format_description" : "domain", + "type" : "string" + }, + "plugin" : { + "default" : "standalone", + "description" : "The ACME plugin ID", + "format" : "pve-configid", + "format_description" : "name of the plugin configuration", + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string" + }, + "description" : { + "description" : "Description for the Node. Shown in the web-interface node notes panel. This is saved as comment inside the configuration file.", + "maxLength" : 65536, + "optional" : 1, + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.", + "maxLength" : 40, + "optional" : 1, + "type" : "string" + }, + "startall-onboot-delay" : { + "default" : 0, + "description" : "Initial delay in seconds, before starting all the Virtual Guests with on-boot enabled.", + "maximum" : 300, + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "wakeonlan" : { + "description" : "MAC address for wake on LAN", + "format" : "mac-addr", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set node configuration options.", + "method" : "PUT", + "name" : "set_options", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "acme" : { + "description" : "Node specific ACME settings.", + "format" : { + "account" : { + "default" : "default", + "description" : "ACME account config file name.", + "format" : "pve-configid", + "format_description" : "name", + "optional" : 1, + "type" : "string" + }, + "domains" : { + "description" : "List of domains for this node's ACME certificate", + "format" : "pve-acme-domain-list", + "format_description" : "domain[;domain;...]", + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[account=] [,domains=]" + }, + "acmedomain[n]" : { + "description" : "ACME domain and validation plugin", + "format" : { + "alias" : { + "description" : "Alias for the Domain to verify ACME Challenge over DNS", + "format" : "pve-acme-alias", + "format_description" : "domain", + "optional" : 1, + "type" : "string" + }, + "domain" : { + "default_key" : 1, + "description" : "domain for this node's ACME certificate", + "format" : "pve-acme-domain", + "format_description" : "domain", + "type" : "string" + }, + "plugin" : { + "default" : "standalone", + "description" : "The ACME plugin ID", + "format" : "pve-configid", + "format_description" : "name of the plugin configuration", + "optional" : 1, + "type" : "string" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[domain=] [,alias=] [,plugin=]" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "description" : { + "description" : "Description for the Node. Shown in the web-interface node notes panel. This is saved as comment inside the configuration file.", + "maxLength" : 65536, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.", + "maxLength" : 40, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "startall-onboot-delay" : { + "default" : 0, + "description" : "Initial delay in seconds, before starting all the Virtual Guests with on-boot enabled.", + "maximum" : 300, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 300)" + }, + "wakeonlan" : { + "description" : "MAC address for wake on LAN", + "format" : "mac-addr", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/config", + "text" : "config" + }, + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List zone content.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "zone" : { + "description" : "The SDN zone object identifier.", + "format" : "pve-sdn-zone-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/zones/{zone}", + [ + "SDN.Audit" + ], + "any", + 1 + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "status" : { + "description" : "Status.", + "optional" : 1, + "type" : "string" + }, + "statusmsg" : { + "description" : "Status details", + "optional" : 1, + "type" : "string" + }, + "vnet" : { + "description" : "Vnet identifier.", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{vnet}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/sdn/zones/{zone}/content", + "text" : "content" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "", + "method" : "GET", + "name" : "diridx", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "zone" : { + "description" : "The SDN zone object identifier.", + "format" : "pve-sdn-zone-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/sdn/zones/{zone}", + [ + "SDN.Audit" + ], + "any", + 1 + ] + }, + "returns" : { + "items" : { + "properties" : { + "subdir" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{subdir}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/sdn/zones/{zone}", + "text" : "{zone}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get status for all zones.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "Only list entries where you have 'SDN.Audit'", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "status" : { + "description" : "Status of zone", + "enum" : [ + "available", + "pending", + "error" + ], + "type" : "string" + }, + "zone" : { + "description" : "The SDN zone object identifier.", + "format" : "pve-sdn-zone-id", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{zone}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/sdn/zones", + "text" : "zones" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "SDN index.", + "method" : "GET", + "name" : "sdnindex", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}/sdn", + "text" : "sdn" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "API version details", + "method" : "GET", + "name" : "version", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "properties" : { + "release" : { + "description" : "The current installed Proxmox VE Release", + "type" : "string" + }, + "repoid" : { + "description" : "The short git commit hash ID from which this version was build", + "type" : "string" + }, + "version" : { + "description" : "The current installed pve-manager package version", + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/version", + "text" : "version" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read node status", + "method" : "GET", + "name" : "status", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Reboot or shutdown a node.", + "method" : "POST", + "name" : "node_cmd", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "command" : { + "description" : "Specify the command.", + "enum" : [ + "reboot", + "shutdown" + ], + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.PowerMgmt" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/status", + "text" : "status" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read tap/vm network device interface counters", + "method" : "GET", + "name" : "netstat", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/netstat", + "text" : "netstat" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Execute multiple commands in order, root only.", + "method" : "POST", + "name" : "execute", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "commands" : { + "description" : "JSON encoded array of commands.", + "format" : "pve-command-batch", + "type" : "string", + "typetext" : "", + "verbose_description" : "JSON encoded array of commands, where each command is an object with the following properties:\n args: \n\t A set of parameter names and their values.\n\n method: (GET|POST|PUT|DELETE)\n\t A method related to the API endpoint (GET, POST etc.).\n\n path: \n\t A relative path to an API endpoint on this node.\n\n" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/execute", + "text" : "execute" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Try to wake a node via 'wake on LAN' network packet.", + "method" : "POST", + "name" : "wakeonlan", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "target node for wake on LAN packet", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.PowerMgmt" + ] + ] + }, + "protected" : 1, + "returns" : { + "description" : "MAC address used to assemble the WoL magic packet.", + "format" : "mac-addr", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/wakeonlan", + "text" : "wakeonlan" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read node RRD statistics (returns PNG)", + "method" : "GET", + "name" : "rrd", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cf" : { + "description" : "The RRD consolidation function", + "enum" : [ + "AVERAGE", + "MAX" + ], + "optional" : 1, + "type" : "string" + }, + "ds" : { + "description" : "The list of datasources you want to display.", + "format" : "pve-configid-list", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "timeframe" : { + "description" : "Specify the time frame you are interested in.", + "enum" : [ + "hour", + "day", + "week", + "month", + "year" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "properties" : { + "filename" : { + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/rrd", + "text" : "rrd" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read node RRD statistics", + "method" : "GET", + "name" : "rrddata", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cf" : { + "description" : "The RRD consolidation function", + "enum" : [ + "AVERAGE", + "MAX" + ], + "optional" : 1, + "type" : "string" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "timeframe" : { + "description" : "Specify the time frame you are interested in.", + "enum" : [ + "hour", + "day", + "week", + "month", + "year" + ], + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/rrddata", + "text" : "rrddata" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read system log", + "method" : "GET", + "name" : "syslog", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "limit" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "service" : { + "description" : "Service ID", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "since" : { + "description" : "Display all log since this date-time string.", + "optional" : 1, + "pattern" : "^\\d{4}-\\d{2}-\\d{2}( \\d{2}:\\d{2}(:\\d{2})?)?$", + "type" : "string" + }, + "start" : { + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "until" : { + "description" : "Display all log until this date-time string.", + "optional" : 1, + "pattern" : "^\\d{4}-\\d{2}-\\d{2}( \\d{2}:\\d{2}(:\\d{2})?)?$", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Syslog" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : { + "n" : { + "description" : "Line number", + "type" : "integer" + }, + "t" : { + "description" : "Line text", + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/syslog", + "text" : "syslog" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read Journal", + "method" : "GET", + "name" : "journal", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "endcursor" : { + "description" : "End before the given Cursor. Conflicts with 'until'", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "lastentries" : { + "description" : "Limit to the last X lines. Conflicts with a range.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "since" : { + "description" : "Display all log since this UNIX epoch. Conflicts with 'startcursor'.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "startcursor" : { + "description" : "Start after the given Cursor. Conflicts with 'since'", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "until" : { + "description" : "Display all log until this UNIX epoch. Conflicts with 'endcursor'.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Syslog" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "items" : { + "type" : "string" + }, + "type" : "array" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/journal", + "text" : "journal" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Creates a VNC Shell proxy.", + "method" : "POST", + "name" : "vncshell", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cmd" : { + "default" : "login", + "description" : "Run specific command or default to login (requires 'root@pam')", + "enum" : [ + "ceph_install", + "login", + "upgrade" + ], + "optional" : 1, + "type" : "string" + }, + "cmd-opts" : { + "default" : "", + "description" : "Add parameters to a command. Encoded as null terminated strings.", + "optional" : 1, + "requires" : "cmd", + "type" : "string", + "typetext" : "" + }, + "height" : { + "description" : "sets the height of the console in pixels.", + "maximum" : 2160, + "minimum" : 16, + "optional" : 1, + "type" : "integer", + "typetext" : " (16 - 2160)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "websocket" : { + "description" : "use websocket instead of standard vnc.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "width" : { + "description" : "sets the width of the console in pixels.", + "maximum" : 4096, + "minimum" : 16, + "optional" : 1, + "type" : "integer", + "typetext" : " (16 - 4096)" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "cert" : { + "type" : "string" + }, + "port" : { + "type" : "integer" + }, + "ticket" : { + "type" : "string" + }, + "upid" : { + "type" : "string" + }, + "user" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/vncshell", + "text" : "vncshell" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Creates a VNC Shell proxy.", + "method" : "POST", + "name" : "termproxy", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cmd" : { + "default" : "login", + "description" : "Run specific command or default to login (requires 'root@pam')", + "enum" : [ + "ceph_install", + "login", + "upgrade" + ], + "optional" : 1, + "type" : "string" + }, + "cmd-opts" : { + "default" : "", + "description" : "Add parameters to a command. Encoded as null terminated strings.", + "optional" : 1, + "requires" : "cmd", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Console" + ] + ] + }, + "protected" : 1, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "port" : { + "type" : "integer" + }, + "ticket" : { + "type" : "string" + }, + "upid" : { + "type" : "string" + }, + "user" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/termproxy", + "text" : "termproxy" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Opens a websocket for VNC traffic.", + "method" : "GET", + "name" : "vncwebsocket", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "port" : { + "description" : "Port number returned by previous vncproxy call.", + "maximum" : 5999, + "minimum" : 5900, + "type" : "integer", + "typetext" : " (5900 - 5999)" + }, + "vncticket" : { + "description" : "Ticket from previous call to vncproxy.", + "maxLength" : 512, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Console" + ] + ], + "description" : "You also need to pass a valid ticket (vncticket)." + }, + "returns" : { + "properties" : { + "port" : { + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/vncwebsocket", + "text" : "vncwebsocket" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Creates a SPICE shell.", + "method" : "POST", + "name" : "spiceshell", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "cmd" : { + "default" : "login", + "description" : "Run specific command or default to login (requires 'root@pam')", + "enum" : [ + "ceph_install", + "login", + "upgrade" + ], + "optional" : 1, + "type" : "string" + }, + "cmd-opts" : { + "default" : "", + "description" : "Add parameters to a command. Encoded as null terminated strings.", + "optional" : 1, + "requires" : "cmd", + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "proxy" : { + "description" : "SPICE proxy server. This can be used by the client to specify the proxy server. All nodes in a cluster runs 'spiceproxy', so it is up to the client to choose one. By default, we return the node where the VM is currently running. As reasonable setting is to use same node you use to connect to the API (This is window.location.hostname for the JS GUI).", + "format" : "address", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Console" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "additionalProperties" : 1, + "description" : "Returned values can be directly passed to the 'remote-viewer' application.", + "properties" : { + "host" : { + "type" : "string" + }, + "password" : { + "type" : "string" + }, + "proxy" : { + "type" : "string" + }, + "tls-port" : { + "type" : "integer" + }, + "type" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/spiceshell", + "text" : "spiceshell" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read DNS settings.", + "method" : "GET", + "name" : "dns", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "additionalProperties" : 0, + "properties" : { + "dns1" : { + "description" : "First name server IP address.", + "optional" : 1, + "type" : "string" + }, + "dns2" : { + "description" : "Second name server IP address.", + "optional" : 1, + "type" : "string" + }, + "dns3" : { + "description" : "Third name server IP address.", + "optional" : 1, + "type" : "string" + }, + "search" : { + "description" : "Search domain for host-name lookup.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Write DNS settings.", + "method" : "PUT", + "name" : "update_dns", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "dns1" : { + "description" : "First name server IP address.", + "format" : "ip", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dns2" : { + "description" : "Second name server IP address.", + "format" : "ip", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "dns3" : { + "description" : "Third name server IP address.", + "format" : "ip", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "search" : { + "description" : "Search domain for host-name lookup.", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/dns", + "text" : "dns" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Read server time and time zone settings.", + "method" : "GET", + "name" : "time", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "additionalProperties" : 0, + "properties" : { + "localtime" : { + "description" : "Seconds since 1970-01-01 00:00:00 (local time)", + "minimum" : 1297163644, + "renderer" : "timestamp_gmt", + "type" : "integer" + }, + "time" : { + "description" : "Seconds since 1970-01-01 00:00:00 UTC.", + "minimum" : 1297163644, + "renderer" : "timestamp", + "type" : "integer" + }, + "timezone" : { + "description" : "Time zone", + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Set time zone.", + "method" : "PUT", + "name" : "set_timezone", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "timezone" : { + "description" : "Time zone. The file '/usr/share/zoneinfo/zone.tab' contains the list of valid names.", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/time", + "text" : "time" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get list of appliances.", + "method" : "GET", + "name" : "aplinfo", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "proxyto" : "node", + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Download appliance templates.", + "method" : "POST", + "name" : "apl_download", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "The storage where the template will be stored", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "template" : { + "description" : "The template which will downloaded", + "maxLength" : 255, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage/{storage}", + [ + "Datastore.AllocateTemplate" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/aplinfo", + "text" : "aplinfo" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Query metadata of an URL: file size, file name and mime type.", + "method" : "GET", + "name" : "query_url_metadata", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "url" : { + "description" : "The URL to query the metadata from.", + "pattern" : "https?://.*", + "type" : "string" + }, + "verify-certificates" : { + "default" : 1, + "description" : "If false, no SSL/TLS certificates will be verified.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit", + "Sys.Modify" + ] + ] + }, + "proxyto" : "node", + "returns" : { + "properties" : { + "filename" : { + "optional" : 1, + "type" : "string" + }, + "mimetype" : { + "optional" : 1, + "type" : "string" + }, + "size" : { + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/query-url-metadata", + "text" : "query-url-metadata" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Gather various systems information about a node", + "method" : "GET", + "name" : "report", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/report", + "text" : "report" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Start all VMs and containers located on this node (by default only those with onboot=1).", + "method" : "POST", + "name" : "startall", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force" : { + "default" : "off", + "description" : "Issue start command even if virtual guest have 'onboot' not set or set to off.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vms" : { + "description" : "Only consider guests from this comma separated list of VMIDs.", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "The 'VM.PowerMgmt' permission is required on '/' or on '/vms/' for each ID passed via the 'vms' parameter.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/startall", + "text" : "startall" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Stop all VMs and Containers.", + "method" : "POST", + "name" : "stopall", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "force-stop" : { + "default" : 1, + "description" : "Force a hard-stop after the timeout.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "timeout" : { + "default" : 180, + "description" : "Timeout for each guest shutdown task. Depending on `force-stop`, the shutdown gets then simply aborted or a hard-stop is forced.", + "maximum" : 7200, + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - 7200)" + }, + "vms" : { + "description" : "Only consider Guests with these IDs.", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "The 'VM.PowerMgmt' permission is required on '/' or on '/vms/' for each ID passed via the 'vms' parameter.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/stopall", + "text" : "stopall" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Suspend all VMs.", + "method" : "POST", + "name" : "suspendall", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vms" : { + "description" : "Only consider Guests with these IDs.", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "The 'VM.PowerMgmt' permission is required on '/' or on '/vms/' for each ID passed via the 'vms' parameter. Additionally, you need 'VM.Config.Disk' on the '/vms/{vmid}' path and 'Datastore.AllocateSpace' for the configured state-storage(s)", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/suspendall", + "text" : "suspendall" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Migrate all VMs and Containers.", + "method" : "POST", + "name" : "migrateall", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "maxworkers" : { + "description" : "Maximal number of parallel migration job. If not set, uses'max_workers' from datacenter.cfg. One of both must be set!", + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - N)" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "target" : { + "description" : "Target node.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + }, + "vms" : { + "description" : "Only consider Guests with these IDs.", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "with-local-disks" : { + "description" : "Enable live storage migration for local disk", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "The 'VM.Migrate' permission is required on '/' or on '/vms/' for each ID passed via the 'vms' parameter.", + "user" : "all" + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/migrateall", + "text" : "migrateall" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get the content of /etc/hosts.", + "method" : "GET", + "name" : "get_etc_hosts", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/", + [ + "Sys.Audit" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "properties" : { + "data" : { + "description" : "The content of /etc/hosts.", + "type" : "string" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Write /etc/hosts.", + "method" : "POST", + "name" : "write_etc_hosts", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "data" : { + "description" : "The target content of /etc/hosts.", + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/nodes/{node}", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "proxyto" : "node", + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/nodes/{node}/hosts", + "text" : "hosts" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Node index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : {}, + "type" : "object" + }, + "links" : [ + { + "href" : "{name}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes/{node}", + "text" : "{node}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Cluster node index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "cpu" : { + "description" : "CPU utilization.", + "optional" : 1, + "renderer" : "fraction_as_percentage", + "type" : "number" + }, + "level" : { + "description" : "Support level.", + "optional" : 1, + "type" : "string" + }, + "maxcpu" : { + "description" : "Number of available CPUs.", + "optional" : 1, + "type" : "integer" + }, + "maxmem" : { + "description" : "Number of available memory in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "mem" : { + "description" : "Used memory in bytes.", + "optional" : 1, + "renderer" : "bytes", + "type" : "integer" + }, + "node" : { + "description" : "The cluster node name.", + "format" : "pve-node", + "type" : "string" + }, + "ssl_fingerprint" : { + "description" : "The SSL fingerprint for the node certificate.", + "optional" : 1, + "type" : "string" + }, + "status" : { + "description" : "Node status.", + "enum" : [ + "unknown", + "online", + "offline" + ], + "type" : "string" + }, + "uptime" : { + "description" : "Node uptime in seconds.", + "optional" : 1, + "renderer" : "duration", + "type" : "integer" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{node}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/nodes", + "text" : "nodes" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete storage configuration.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Read storage configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/storage/{storage}", + [ + "Datastore.Allocate" + ] + ] + }, + "returns" : { + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update storage configuration.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "blocksize" : { + "description" : "block size", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bwlimit" : { + "description" : "Set I/O bandwidth limit for various operations (in KiB/s).", + "format" : { + "clone" : { + "description" : "bandwidth limit in KiB/s for cloning disks", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "default" : { + "description" : "default bandwidth limit in KiB/s", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "migration" : { + "description" : "bandwidth limit in KiB/s for migrating guests (including moving local disks)", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "move" : { + "description" : "bandwidth limit in KiB/s for moving disks", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "restore" : { + "description" : "bandwidth limit in KiB/s for restoring guests from backups", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[clone=] [,default=] [,migration=] [,move=] [,restore=]" + }, + "comstar_hg" : { + "description" : "host group for comstar views", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comstar_tg" : { + "description" : "target group for comstar views", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "content" : { + "description" : "Allowed content types.\n\nNOTE: the value 'rootdir' is used for Containers, and value 'images' for VMs.\n", + "format" : "pve-storage-content-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "content-dirs" : { + "description" : "Overrides for default content type directories.", + "format" : "pve-dir-override-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "create-base-path" : { + "default" : "yes", + "description" : "Create the base directory if it doesn't exist.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "create-subdirs" : { + "default" : "yes", + "description" : "Populate the directory with the default structure.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "data-pool" : { + "description" : "Data Pool (for erasure coding only)", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "description" : "Flag to disable the storage.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "domain" : { + "description" : "CIFS domain.", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "encryption-key" : { + "description" : "Encryption key. Use 'autogen' to generate one automatically without passphrase.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "fingerprint" : { + "description" : "Certificate SHA 256 fingerprint.", + "optional" : 1, + "pattern" : "([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}", + "type" : "string" + }, + "format" : { + "description" : "Default image format.", + "format" : "pve-storage-format", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "fs-name" : { + "description" : "The Ceph filesystem name.", + "format" : "pve-configid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "fuse" : { + "description" : "Mount CephFS through FUSE.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "is_mountpoint" : { + "default" : "no", + "description" : "Assume the given path is an externally managed mountpoint and consider the storage offline if it is not mounted. Using a boolean (yes/no) value serves as a shortcut to using the target path in this field.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "keyring" : { + "description" : "Client keyring contents (for external clusters).", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "krbd" : { + "description" : "Always access rbd through krbd kernel module.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "lio_tpg" : { + "description" : "target portal group for Linux LIO targets", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "master-pubkey" : { + "description" : "Base64-encoded, PEM-formatted public RSA key. Used to encrypt a copy of the encryption-key which will be added to each encrypted backup.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "max-protected-backups" : { + "default" : "Unlimited for users with Datastore.Allocate privilege, 5 for other users", + "description" : "Maximal number of protected backups per guest. Use '-1' for unlimited.", + "minimum" : -1, + "optional" : 1, + "type" : "integer", + "typetext" : " (-1 - N)" + }, + "maxfiles" : { + "description" : "Deprecated: use 'prune-backups' instead. Maximal number of backup files per VM. Use '0' for unlimited.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "mkdir" : { + "default" : "yes", + "description" : "Create the directory if it doesn't exist and populate it with default sub-dirs. NOTE: Deprecated, use the 'create-base-path' and 'create-subdirs' options instead.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "monhost" : { + "description" : "IP addresses of monitors (for external clusters).", + "format" : "pve-storage-portal-dns-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "mountpoint" : { + "description" : "mount point", + "format" : "pve-storage-path", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "namespace" : { + "description" : "Namespace.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "nocow" : { + "default" : 0, + "description" : "Set the NOCOW flag on files. Disables data checksumming and causes data errors to be unrecoverable from while allowing direct I/O. Only use this if data does not need to be any more safe than on a single ext4 formatted disk with no underlying raid system.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "nodes" : { + "description" : "List of cluster node names.", + "format" : "pve-node-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "nowritecache" : { + "description" : "disable write caching on the target", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "options" : { + "description" : "NFS/CIFS mount options (see 'man nfs' or 'man mount.cifs')", + "format" : "pve-storage-options", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "Password for accessing the share/datastore.", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "pool" : { + "description" : "Pool.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "port" : { + "default" : 8007, + "description" : "For non default port.", + "maximum" : 65535, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 65535)" + }, + "preallocation" : { + "default" : "metadata", + "description" : "Preallocation mode for raw and qcow2 images. Using 'metadata' on raw images results in preallocation=off.", + "enum" : [ + "off", + "metadata", + "falloc", + "full" + ], + "optional" : 1, + "type" : "string" + }, + "prune-backups" : { + "description" : "The retention options with shorter intervals are processed first with --keep-last being the very first one. Each option covers a specific period of time. We say that backups within this period are covered by this option. The next option does not take care of already covered backups and only considers older backups.", + "format" : "prune-backups", + "optional" : 1, + "type" : "string", + "typetext" : "[keep-all=<1|0>] [,keep-daily=] [,keep-hourly=] [,keep-last=] [,keep-monthly=] [,keep-weekly=] [,keep-yearly=]" + }, + "saferemove" : { + "description" : "Zero-out data when removing LVs.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "saferemove_throughput" : { + "description" : "Wipe throughput (cstream -t parameter value).", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "server" : { + "description" : "Server IP or DNS name.", + "format" : "pve-storage-server", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "server2" : { + "description" : "Backup volfile server IP or DNS name.", + "format" : "pve-storage-server", + "optional" : 1, + "requires" : "server", + "type" : "string", + "typetext" : "" + }, + "shared" : { + "description" : "Mark storage as shared.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "smbversion" : { + "default" : "default", + "description" : "SMB protocol version. 'default' if not set, negotiates the highest SMB2+ version supported by both the client and server.", + "enum" : [ + "default", + "2.0", + "2.1", + "3", + "3.0", + "3.11" + ], + "optional" : 1, + "type" : "string" + }, + "sparse" : { + "description" : "use sparse volumes", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "subdir" : { + "description" : "Subdir to mount.", + "format" : "pve-storage-path", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tagged_only" : { + "description" : "Only use logical volumes tagged with 'pve-vm-ID'.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "transport" : { + "description" : "Gluster transport: tcp or rdma", + "enum" : [ + "tcp", + "rdma", + "unix" + ], + "optional" : 1, + "type" : "string" + }, + "username" : { + "description" : "RBD Id.", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "properties" : { + "config" : { + "additionalProperties" : 1, + "description" : "Partial, possible server generated, configuration properties.", + "optional" : 1, + "properties" : { + "encryption-key" : { + "description" : "The, possible auto-generated, encryption-key.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "storage" : { + "description" : "The ID of the created storage.", + "type" : "string" + }, + "type" : { + "description" : "The type of the created storage.", + "enum" : [ + "btrfs", + "cephfs", + "cifs", + "dir", + "glusterfs", + "iscsi", + "iscsidirect", + "lvm", + "lvmthin", + "nfs", + "pbs", + "rbd", + "zfs", + "zfspool" + ], + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/storage/{storage}", + "text" : "{storage}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Storage index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "type" : { + "description" : "Only list storage of specific type", + "enum" : [ + "btrfs", + "cephfs", + "cifs", + "dir", + "glusterfs", + "iscsi", + "iscsidirect", + "lvm", + "lvmthin", + "nfs", + "pbs", + "rbd", + "zfs", + "zfspool" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "description" : "Only list entries where you have 'Datastore.Audit' or 'Datastore.AllocateSpace' permissions on '/storage/'", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "storage" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{storage}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create a new storage.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "authsupported" : { + "description" : "Authsupported.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "base" : { + "description" : "Base volume. This volume is automatically activated.", + "format" : "pve-volume-id", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "blocksize" : { + "description" : "block size", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bwlimit" : { + "description" : "Set I/O bandwidth limit for various operations (in KiB/s).", + "format" : { + "clone" : { + "description" : "bandwidth limit in KiB/s for cloning disks", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "default" : { + "description" : "default bandwidth limit in KiB/s", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "migration" : { + "description" : "bandwidth limit in KiB/s for migrating guests (including moving local disks)", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "move" : { + "description" : "bandwidth limit in KiB/s for moving disks", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + }, + "restore" : { + "description" : "bandwidth limit in KiB/s for restoring guests from backups", + "format_description" : "LIMIT", + "minimum" : "0", + "optional" : 1, + "type" : "number" + } + }, + "optional" : 1, + "type" : "string", + "typetext" : "[clone=] [,default=] [,migration=] [,move=] [,restore=]" + }, + "comstar_hg" : { + "description" : "host group for comstar views", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comstar_tg" : { + "description" : "target group for comstar views", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "content" : { + "description" : "Allowed content types.\n\nNOTE: the value 'rootdir' is used for Containers, and value 'images' for VMs.\n", + "format" : "pve-storage-content-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "content-dirs" : { + "description" : "Overrides for default content type directories.", + "format" : "pve-dir-override-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "create-base-path" : { + "default" : "yes", + "description" : "Create the base directory if it doesn't exist.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "create-subdirs" : { + "default" : "yes", + "description" : "Populate the directory with the default structure.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "data-pool" : { + "description" : "Data Pool (for erasure coding only)", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "datastore" : { + "description" : "Proxmox Backup Server datastore name.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "disable" : { + "description" : "Flag to disable the storage.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "domain" : { + "description" : "CIFS domain.", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "encryption-key" : { + "description" : "Encryption key. Use 'autogen' to generate one automatically without passphrase.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "export" : { + "description" : "NFS export path.", + "format" : "pve-storage-path", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "fingerprint" : { + "description" : "Certificate SHA 256 fingerprint.", + "optional" : 1, + "pattern" : "([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}", + "type" : "string" + }, + "format" : { + "description" : "Default image format.", + "format" : "pve-storage-format", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "fs-name" : { + "description" : "The Ceph filesystem name.", + "format" : "pve-configid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "fuse" : { + "description" : "Mount CephFS through FUSE.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "is_mountpoint" : { + "default" : "no", + "description" : "Assume the given path is an externally managed mountpoint and consider the storage offline if it is not mounted. Using a boolean (yes/no) value serves as a shortcut to using the target path in this field.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "iscsiprovider" : { + "description" : "iscsi provider", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "keyring" : { + "description" : "Client keyring contents (for external clusters).", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "krbd" : { + "description" : "Always access rbd through krbd kernel module.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "lio_tpg" : { + "description" : "target portal group for Linux LIO targets", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "master-pubkey" : { + "description" : "Base64-encoded, PEM-formatted public RSA key. Used to encrypt a copy of the encryption-key which will be added to each encrypted backup.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "max-protected-backups" : { + "default" : "Unlimited for users with Datastore.Allocate privilege, 5 for other users", + "description" : "Maximal number of protected backups per guest. Use '-1' for unlimited.", + "minimum" : -1, + "optional" : 1, + "type" : "integer", + "typetext" : " (-1 - N)" + }, + "maxfiles" : { + "description" : "Deprecated: use 'prune-backups' instead. Maximal number of backup files per VM. Use '0' for unlimited.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "mkdir" : { + "default" : "yes", + "description" : "Create the directory if it doesn't exist and populate it with default sub-dirs. NOTE: Deprecated, use the 'create-base-path' and 'create-subdirs' options instead.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "monhost" : { + "description" : "IP addresses of monitors (for external clusters).", + "format" : "pve-storage-portal-dns-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "mountpoint" : { + "description" : "mount point", + "format" : "pve-storage-path", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "namespace" : { + "description" : "Namespace.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "nocow" : { + "default" : 0, + "description" : "Set the NOCOW flag on files. Disables data checksumming and causes data errors to be unrecoverable from while allowing direct I/O. Only use this if data does not need to be any more safe than on a single ext4 formatted disk with no underlying raid system.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "nodes" : { + "description" : "List of cluster node names.", + "format" : "pve-node-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "nowritecache" : { + "description" : "disable write caching on the target", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "options" : { + "description" : "NFS/CIFS mount options (see 'man nfs' or 'man mount.cifs')", + "format" : "pve-storage-options", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "Password for accessing the share/datastore.", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "path" : { + "description" : "File system path.", + "format" : "pve-storage-path", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "pool" : { + "description" : "Pool.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "port" : { + "default" : 8007, + "description" : "For non default port.", + "maximum" : 65535, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 65535)" + }, + "portal" : { + "description" : "iSCSI portal (IP or DNS name with optional port).", + "format" : "pve-storage-portal-dns", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "preallocation" : { + "default" : "metadata", + "description" : "Preallocation mode for raw and qcow2 images. Using 'metadata' on raw images results in preallocation=off.", + "enum" : [ + "off", + "metadata", + "falloc", + "full" + ], + "optional" : 1, + "type" : "string" + }, + "prune-backups" : { + "description" : "The retention options with shorter intervals are processed first with --keep-last being the very first one. Each option covers a specific period of time. We say that backups within this period are covered by this option. The next option does not take care of already covered backups and only considers older backups.", + "format" : "prune-backups", + "optional" : 1, + "type" : "string", + "typetext" : "[keep-all=<1|0>] [,keep-daily=] [,keep-hourly=] [,keep-last=] [,keep-monthly=] [,keep-weekly=] [,keep-yearly=]" + }, + "saferemove" : { + "description" : "Zero-out data when removing LVs.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "saferemove_throughput" : { + "description" : "Wipe throughput (cstream -t parameter value).", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "server" : { + "description" : "Server IP or DNS name.", + "format" : "pve-storage-server", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "server2" : { + "description" : "Backup volfile server IP or DNS name.", + "format" : "pve-storage-server", + "optional" : 1, + "requires" : "server", + "type" : "string", + "typetext" : "" + }, + "share" : { + "description" : "CIFS share.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "shared" : { + "description" : "Mark storage as shared.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "smbversion" : { + "default" : "default", + "description" : "SMB protocol version. 'default' if not set, negotiates the highest SMB2+ version supported by both the client and server.", + "enum" : [ + "default", + "2.0", + "2.1", + "3", + "3.0", + "3.11" + ], + "optional" : 1, + "type" : "string" + }, + "sparse" : { + "description" : "use sparse volumes", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "storage" : { + "description" : "The storage identifier.", + "format" : "pve-storage-id", + "type" : "string", + "typetext" : "" + }, + "subdir" : { + "description" : "Subdir to mount.", + "format" : "pve-storage-path", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tagged_only" : { + "description" : "Only use logical volumes tagged with 'pve-vm-ID'.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "target" : { + "description" : "iSCSI target.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "thinpool" : { + "description" : "LVM thin pool LV name.", + "format" : "pve-storage-vgname", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "transport" : { + "description" : "Gluster transport: tcp or rdma", + "enum" : [ + "tcp", + "rdma", + "unix" + ], + "optional" : 1, + "type" : "string" + }, + "type" : { + "description" : "Storage type.", + "enum" : [ + "btrfs", + "cephfs", + "cifs", + "dir", + "glusterfs", + "iscsi", + "iscsidirect", + "lvm", + "lvmthin", + "nfs", + "pbs", + "rbd", + "zfs", + "zfspool" + ], + "type" : "string" + }, + "username" : { + "description" : "RBD Id.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vgname" : { + "description" : "Volume group name.", + "format" : "pve-storage-vgname", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "volume" : { + "description" : "Glusterfs Volume.", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/storage", + [ + "Datastore.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "properties" : { + "config" : { + "additionalProperties" : 1, + "description" : "Partial, possible server generated, configuration properties.", + "optional" : 1, + "properties" : { + "encryption-key" : { + "description" : "The, possible auto-generated, encryption-key.", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "storage" : { + "description" : "The ID of the created storage.", + "type" : "string" + }, + "type" : { + "description" : "The type of the created storage.", + "enum" : [ + "btrfs", + "cephfs", + "cifs", + "dir", + "glusterfs", + "iscsi", + "iscsidirect", + "lvm", + "lvmthin", + "nfs", + "pbs", + "rbd", + "zfs", + "zfspool" + ], + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 0, + "path" : "/storage", + "text" : "storage" + }, + { + "children" : [ + { + "children" : [ + { + "children" : [ + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get user TFA types (Personal and Realm).", + "method" : "GET", + "name" : "read_user_tfa_type", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "multiple" : { + "default" : 0, + "description" : "Request all entries as an array.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "userid-group", + [ + "User.Modify", + "Sys.Audit" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "realm" : { + "description" : "The type of TFA the users realm has set, if any.", + "enum" : [ + "oath", + "yubico" + ], + "optional" : 1, + "type" : "string" + }, + "types" : { + "description" : "Array of the user configured TFA types, if any. Only available if 'multiple' was not passed.", + "items" : { + "description" : "A TFA type.", + "enum" : [ + "totp", + "u2f", + "yubico", + "webauthn", + "recovedry" + ], + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "user" : { + "description" : "The type of TFA the user has set, if any. Only set if 'multiple' was not passed.", + "enum" : [ + "oath", + "u2f" + ], + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/access/users/{userid}/tfa", + "text" : "tfa" + }, + { + "info" : { + "PUT" : { + "allowtoken" : 1, + "description" : "Unlock a user's TFA authentication.", + "method" : "PUT", + "name" : "unlock_tfa", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "userid-group", + [ + "User.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "boolean" + } + } + }, + "leaf" : 1, + "path" : "/access/users/{userid}/unlock-tfa", + "text" : "unlock-tfa" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Remove API token for a specific user.", + "method" : "DELETE", + "name" : "remove_token", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "tokenid" : { + "description" : "User-specific token identifier.", + "pattern" : "(?^:[A-Za-z][A-Za-z0-9\\.\\-_]+)", + "type" : "string" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "userid-group", + [ + "User.Modify" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get specific API token information.", + "method" : "GET", + "name" : "read_token", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "tokenid" : { + "description" : "User-specific token identifier.", + "pattern" : "(?^:[A-Za-z][A-Za-z0-9\\.\\-_]+)", + "type" : "string" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "userid-group", + [ + "User.Modify" + ] + ] + ] + }, + "returns" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "expire" : { + "default" : "same as user", + "description" : "API token expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "privsep" : { + "default" : 1, + "description" : "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.", + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Generate a new API token for a specific user. NOTE: returns API token value, which needs to be stored as it cannot be retrieved afterwards!", + "method" : "POST", + "name" : "generate_token", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "expire" : { + "default" : "same as user", + "description" : "API token expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "privsep" : { + "default" : 1, + "description" : "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "tokenid" : { + "description" : "User-specific token identifier.", + "pattern" : "(?^:[A-Za-z][A-Za-z0-9\\.\\-_]+)", + "type" : "string" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "userid-group", + [ + "User.Modify" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "full-tokenid" : { + "description" : "The full token id.", + "format_description" : "!", + "type" : "string" + }, + "info" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "expire" : { + "default" : "same as user", + "description" : "API token expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "privsep" : { + "default" : 1, + "description" : "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.", + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + }, + "value" : { + "description" : "API token value used for authentication.", + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update API token for a specific user.", + "method" : "PUT", + "name" : "update_token_info", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "expire" : { + "default" : "same as user", + "description" : "API token expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "privsep" : { + "default" : 1, + "description" : "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "tokenid" : { + "description" : "User-specific token identifier.", + "pattern" : "(?^:[A-Za-z][A-Za-z0-9\\.\\-_]+)", + "type" : "string" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "userid-group", + [ + "User.Modify" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "description" : "Updated token information.", + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "expire" : { + "default" : "same as user", + "description" : "API token expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "privsep" : { + "default" : 1, + "description" : "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.", + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/access/users/{userid}/token/{tokenid}", + "text" : "{tokenid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get user API tokens.", + "method" : "GET", + "name" : "token_index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "userid-group", + [ + "User.Modify" + ] + ] + ] + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "expire" : { + "default" : "same as user", + "description" : "API token expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "privsep" : { + "default" : 1, + "description" : "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.", + "optional" : 1, + "type" : "boolean" + }, + "tokenid" : { + "description" : "User-specific token identifier.", + "pattern" : "(?^:[A-Za-z][A-Za-z0-9\\.\\-_]+)", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{tokenid}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/access/users/{userid}/token", + "text" : "token" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete user.", + "method" : "DELETE", + "name" : "delete_user", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "and", + [ + "userid-param", + "Realm.AllocateUser" + ], + [ + "userid-group", + [ + "User.Modify" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get user configuration.", + "method" : "GET", + "name" : "read_user", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "userid-group", + [ + "User.Modify", + "Sys.Audit" + ] + ] + }, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "email" : { + "format" : "email-opt", + "optional" : 1, + "type" : "string" + }, + "enable" : { + "default" : 1, + "description" : "Enable the account (default). You can set this to '0' to disable the account", + "optional" : 1, + "type" : "boolean" + }, + "expire" : { + "description" : "Account expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "firstname" : { + "optional" : 1, + "type" : "string" + }, + "groups" : { + "items" : { + "format" : "pve-groupid", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + }, + "keys" : { + "description" : "Keys for two factor auth (yubico).", + "optional" : 1, + "type" : "string" + }, + "lastname" : { + "optional" : 1, + "type" : "string" + }, + "tokens" : { + "additionalProperties" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "expire" : { + "default" : "same as user", + "description" : "API token expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "privsep" : { + "default" : 1, + "description" : "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.", + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + }, + "optional" : 1, + "type" : "object" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update user configuration.", + "method" : "PUT", + "name" : "update_user", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "append" : { + "optional" : 1, + "requires" : "groups", + "type" : "boolean", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "email" : { + "format" : "email-opt", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "default" : 1, + "description" : "Enable the account (default). You can set this to '0' to disable the account", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "expire" : { + "description" : "Account expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "firstname" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "groups" : { + "format" : "pve-groupid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "keys" : { + "description" : "Keys for two factor auth (yubico).", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "lastname" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "userid-group", + [ + "User.Modify" + ], + "groups_param", + "update" + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/access/users/{userid}", + "text" : "{userid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "User index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "enabled" : { + "description" : "Optional filter for enable property.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "full" : { + "default" : 0, + "description" : "Include group and token information.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "The returned list is restricted to users where you have 'User.Modify' or 'Sys.Audit' permissions on '/access/groups' or on a group the user belongs too. But it always includes the current (authenticated) user.", + "user" : "all" + }, + "protected" : 1, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "email" : { + "format" : "email-opt", + "optional" : 1, + "type" : "string" + }, + "enable" : { + "default" : 1, + "description" : "Enable the account (default). You can set this to '0' to disable the account", + "optional" : 1, + "type" : "boolean" + }, + "expire" : { + "description" : "Account expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "firstname" : { + "optional" : 1, + "type" : "string" + }, + "groups" : { + "format" : "pve-groupid-list", + "optional" : 1, + "type" : "string" + }, + "keys" : { + "description" : "Keys for two factor auth (yubico).", + "optional" : 1, + "type" : "string" + }, + "lastname" : { + "optional" : 1, + "type" : "string" + }, + "realm-type" : { + "description" : "The type of the users realm", + "format" : "pve-realm", + "optional" : 1, + "type" : "string" + }, + "tfa-locked-until" : { + "description" : "Contains a timestamp until when a user is locked out of 2nd factors.", + "optional" : 1, + "type" : "integer" + }, + "tokens" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "expire" : { + "default" : "same as user", + "description" : "API token expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer" + }, + "privsep" : { + "default" : 1, + "description" : "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.", + "optional" : 1, + "type" : "boolean" + }, + "tokenid" : { + "description" : "User-specific token identifier.", + "pattern" : "(?^:[A-Za-z][A-Za-z0-9\\.\\-_]+)", + "type" : "string" + } + }, + "type" : "object" + }, + "optional" : 1, + "type" : "array" + }, + "totp-locked" : { + "description" : "True if the user is currently locked out of TOTP factors.", + "optional" : 1, + "type" : "boolean" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{userid}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new user.", + "method" : "POST", + "name" : "create_user", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "email" : { + "format" : "email-opt", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "default" : 1, + "description" : "Enable the account (default). You can set this to '0' to disable the account", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "expire" : { + "description" : "Account expiration date (seconds since epoch). '0' means no expiration date.", + "minimum" : 0, + "optional" : 1, + "type" : "integer", + "typetext" : " (0 - N)" + }, + "firstname" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "groups" : { + "format" : "pve-groupid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "keys" : { + "description" : "Keys for two factor auth (yubico).", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "lastname" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "Initial password.", + "maxLength" : 64, + "minLength" : 5, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "and", + [ + "userid-param", + "Realm.AllocateUser" + ], + [ + "userid-group", + [ + "User.Modify" + ], + "groups_param", + "create" + ] + ], + "description" : "You need 'Realm.AllocateUser' on '/access/realm/' on the realm of user , and 'User.Modify' permissions to '/access/groups/' for any group specified (or 'User.Modify' on '/access/groups' if you pass no groups." + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/access/users", + "text" : "users" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete group.", + "method" : "DELETE", + "name" : "delete_group", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "groupid" : { + "format" : "pve-groupid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/access/groups", + [ + "Group.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get group configuration.", + "method" : "GET", + "name" : "read_group", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "groupid" : { + "format" : "pve-groupid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/access/groups", + [ + "Sys.Audit", + "Group.Allocate" + ], + "any", + 1 + ] + }, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "members" : { + "items" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update group data.", + "method" : "PUT", + "name" : "update_group", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "groupid" : { + "format" : "pve-groupid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/access/groups", + [ + "Group.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/access/groups/{groupid}", + "text" : "{groupid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Group index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "description" : "The returned list is restricted to groups where you have 'User.Modify', 'Sys.Audit' or 'Group.Allocate' permissions on /access/groups/.", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "groupid" : { + "format" : "pve-groupid", + "type" : "string" + }, + "users" : { + "description" : "list of users which form this group", + "format" : "pve-userid-list", + "optional" : 1, + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{groupid}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new group.", + "method" : "POST", + "name" : "create_group", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "groupid" : { + "format" : "pve-groupid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/access/groups", + [ + "Group.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/access/groups", + "text" : "groups" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete role.", + "method" : "DELETE", + "name" : "delete_role", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "roleid" : { + "format" : "pve-roleid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/access", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get role configuration.", + "method" : "GET", + "name" : "read_role", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "roleid" : { + "format" : "pve-roleid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "Datastore.Allocate" : { + "optional" : 1, + "type" : "boolean" + }, + "Datastore.AllocateSpace" : { + "optional" : 1, + "type" : "boolean" + }, + "Datastore.AllocateTemplate" : { + "optional" : 1, + "type" : "boolean" + }, + "Datastore.Audit" : { + "optional" : 1, + "type" : "boolean" + }, + "Group.Allocate" : { + "optional" : 1, + "type" : "boolean" + }, + "Mapping.Audit" : { + "optional" : 1, + "type" : "boolean" + }, + "Mapping.Modify" : { + "optional" : 1, + "type" : "boolean" + }, + "Mapping.Use" : { + "optional" : 1, + "type" : "boolean" + }, + "Permissions.Modify" : { + "optional" : 1, + "type" : "boolean" + }, + "Pool.Allocate" : { + "optional" : 1, + "type" : "boolean" + }, + "Pool.Audit" : { + "optional" : 1, + "type" : "boolean" + }, + "Realm.Allocate" : { + "optional" : 1, + "type" : "boolean" + }, + "Realm.AllocateUser" : { + "optional" : 1, + "type" : "boolean" + }, + "SDN.Allocate" : { + "optional" : 1, + "type" : "boolean" + }, + "SDN.Audit" : { + "optional" : 1, + "type" : "boolean" + }, + "SDN.Use" : { + "optional" : 1, + "type" : "boolean" + }, + "Sys.Audit" : { + "optional" : 1, + "type" : "boolean" + }, + "Sys.Console" : { + "optional" : 1, + "type" : "boolean" + }, + "Sys.Incoming" : { + "optional" : 1, + "type" : "boolean" + }, + "Sys.Modify" : { + "optional" : 1, + "type" : "boolean" + }, + "Sys.PowerMgmt" : { + "optional" : 1, + "type" : "boolean" + }, + "Sys.Syslog" : { + "optional" : 1, + "type" : "boolean" + }, + "User.Modify" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Allocate" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Audit" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Backup" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Clone" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Config.CDROM" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Config.CPU" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Config.Cloudinit" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Config.Disk" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Config.HWType" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Config.Memory" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Config.Network" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Config.Options" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Console" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Migrate" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Monitor" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.PowerMgmt" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Snapshot" : { + "optional" : 1, + "type" : "boolean" + }, + "VM.Snapshot.Rollback" : { + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update an existing role.", + "method" : "PUT", + "name" : "update_role", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "append" : { + "optional" : 1, + "requires" : "privs", + "type" : "boolean", + "typetext" : "" + }, + "privs" : { + "format" : "pve-priv-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "roleid" : { + "format" : "pve-roleid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/access", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/access/roles/{roleid}", + "text" : "{roleid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Role index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "privs" : { + "format" : "pve-priv-list", + "optional" : 1, + "type" : "string" + }, + "roleid" : { + "format" : "pve-roleid", + "type" : "string" + }, + "special" : { + "default" : 0, + "optional" : 1, + "type" : "boolean" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{roleid}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new role.", + "method" : "POST", + "name" : "create_role", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "privs" : { + "format" : "pve-priv-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "roleid" : { + "format" : "pve-roleid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/access", + [ + "Sys.Modify" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/access/roles", + "text" : "roles" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Get Access Control List (ACLs).", + "method" : "GET", + "name" : "read_acl", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "description" : "The returned list is restricted to objects where you have rights to modify permissions.", + "user" : "all" + }, + "returns" : { + "items" : { + "additionalProperties" : 0, + "properties" : { + "path" : { + "description" : "Access control path", + "type" : "string" + }, + "propagate" : { + "default" : 1, + "description" : "Allow to propagate (inherit) permissions.", + "optional" : 1, + "type" : "boolean" + }, + "roleid" : { + "type" : "string" + }, + "type" : { + "enum" : [ + "user", + "group", + "token" + ], + "type" : "string" + }, + "ugid" : { + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update Access Control List (add or remove permissions).", + "method" : "PUT", + "name" : "update_acl", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "delete" : { + "description" : "Remove permissions (instead of adding it).", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "groups" : { + "description" : "List of groups.", + "format" : "pve-groupid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "path" : { + "description" : "Access control path", + "type" : "string", + "typetext" : "" + }, + "propagate" : { + "default" : 1, + "description" : "Allow to propagate (inherit) permissions.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "roles" : { + "description" : "List of roles.", + "format" : "pve-roleid-list", + "type" : "string", + "typetext" : "" + }, + "tokens" : { + "description" : "List of API tokens.", + "format" : "pve-tokenid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "users" : { + "description" : "List of users.", + "format" : "pve-userid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm-modify", + "{path}" + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/access/acl", + "text" : "acl" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Syncs users and/or groups from the configured LDAP to user.cfg. NOTE: Synced groups will have the name 'name-$realm', so make sure those groups do not exist to prevent overwriting.", + "method" : "POST", + "name" : "sync", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "dry-run" : { + "default" : 0, + "description" : "If set, does not write anything.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "enable-new" : { + "default" : "1", + "description" : "Enable newly synced users immediately.", + "optional" : "1", + "type" : "boolean", + "typetext" : "" + }, + "full" : { + "description" : "DEPRECATED: use 'remove-vanished' instead. If set, uses the LDAP Directory as source of truth, deleting users or groups not returned from the sync and removing all locally modified properties of synced users. If not set, only syncs information which is present in the synced data, and does not delete or modify anything else.", + "optional" : "1", + "type" : "boolean", + "typetext" : "" + }, + "purge" : { + "description" : "DEPRECATED: use 'remove-vanished' instead. Remove ACLs for users or groups which were removed from the config during a sync.", + "optional" : "1", + "type" : "boolean", + "typetext" : "" + }, + "realm" : { + "description" : "Authentication domain ID", + "format" : "pve-realm", + "maxLength" : 32, + "type" : "string", + "typetext" : "" + }, + "remove-vanished" : { + "default" : "none", + "description" : "A semicolon-seperated list of things to remove when they or the user vanishes during a sync. The following values are possible: 'entry' removes the user/group when not returned from the sync. 'properties' removes the set properties on existing user/group that do not appear in the source (even custom ones). 'acl' removes acls when the user/group is not returned from the sync. Instead of a list it also can be 'none' (the default).", + "optional" : "1", + "pattern" : "(?:(?:(?:acl|properties|entry);)*(?:acl|properties|entry))|none", + "type" : "string", + "typetext" : "([acl];[properties];[entry])|none" + }, + "scope" : { + "description" : "Select what to sync.", + "enum" : [ + "users", + "groups", + "both" + ], + "optional" : "1", + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "and", + [ + "perm", + "/access/realm/{realm}", + [ + "Realm.AllocateUser" + ] + ], + [ + "perm", + "/access/groups", + [ + "User.Modify" + ] + ] + ], + "description" : "'Realm.AllocateUser' on '/access/realm/' and 'User.Modify' permissions to '/access/groups/'." + }, + "protected" : 1, + "returns" : { + "description" : "Worker Task-UPID", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/access/domains/{realm}/sync", + "text" : "sync" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete an authentication server.", + "method" : "DELETE", + "name" : "delete", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "realm" : { + "description" : "Authentication domain ID", + "format" : "pve-realm", + "maxLength" : 32, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/access/realm", + [ + "Realm.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get auth server configuration.", + "method" : "GET", + "name" : "read", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "realm" : { + "description" : "Authentication domain ID", + "format" : "pve-realm", + "maxLength" : 32, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/access/realm", + [ + "Realm.Allocate", + "Sys.Audit" + ], + "any", + 1 + ] + }, + "returns" : {} + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update authentication server settings.", + "method" : "PUT", + "name" : "update", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "acr-values" : { + "description" : "Specifies the Authentication Context Class Reference values that theAuthorization Server is being requested to use for the Auth Request.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "autocreate" : { + "default" : 0, + "description" : "Automatically create users if they do not exist.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "base_dn" : { + "description" : "LDAP base domain name", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bind_dn" : { + "description" : "LDAP bind domain name", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "capath" : { + "default" : "/etc/ssl/certs", + "description" : "Path to the CA certificate store", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "case-sensitive" : { + "default" : 1, + "description" : "username is case-sensitive", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "cert" : { + "description" : "Path to the client certificate", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "certkey" : { + "description" : "Path to the client certificate key", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "check-connection" : { + "default" : 0, + "description" : "Check bind connection to the server.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "client-id" : { + "description" : "OpenID Client ID", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "client-key" : { + "description" : "OpenID Client Key", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comment" : { + "description" : "Description.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "default" : { + "description" : "Use this as default realm", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "delete" : { + "description" : "A list of settings you want to delete.", + "format" : "pve-configid-list", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "digest" : { + "description" : "Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications.", + "maxLength" : 64, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "domain" : { + "description" : "AD domain name", + "maxLength" : 256, + "optional" : 1, + "pattern" : "\\S+", + "type" : "string" + }, + "filter" : { + "description" : "LDAP filter for user sync.", + "maxLength" : 2048, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group_classes" : { + "default" : "groupOfNames, group, univentionGroup, ipausergroup", + "description" : "The objectclasses for groups.", + "format" : "ldap-simple-attr-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group_dn" : { + "description" : "LDAP base domain name for group sync. If not set, the base_dn will be used.", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group_filter" : { + "description" : "LDAP filter for group sync.", + "maxLength" : 2048, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group_name_attr" : { + "description" : "LDAP attribute representing a groups name. If not set or found, the first value of the DN will be used as name.", + "format" : "ldap-simple-attr", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "issuer-url" : { + "description" : "OpenID Issuer Url", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "mode" : { + "default" : "ldap", + "description" : "LDAP protocol mode.", + "enum" : [ + "ldap", + "ldaps", + "ldap+starttls" + ], + "optional" : 1, + "type" : "string" + }, + "password" : { + "description" : "LDAP bind password. Will be stored in '/etc/pve/priv/realm/.pw'.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "port" : { + "description" : "Server port.", + "maximum" : 65535, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 65535)" + }, + "prompt" : { + "description" : "Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.", + "optional" : 1, + "pattern" : "(?:none|login|consent|select_account|\\S+)", + "type" : "string" + }, + "realm" : { + "description" : "Authentication domain ID", + "format" : "pve-realm", + "maxLength" : 32, + "type" : "string", + "typetext" : "" + }, + "scopes" : { + "default" : "email profile", + "description" : "Specifies the scopes (user details) that should be authorized and returned, for example 'email' or 'profile'.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "secure" : { + "description" : "Use secure LDAPS protocol. DEPRECATED: use 'mode' instead.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "server1" : { + "description" : "Server IP address (or DNS name)", + "format" : "address", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "server2" : { + "description" : "Fallback Server IP address (or DNS name)", + "format" : "address", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sslversion" : { + "description" : "LDAPS TLS/SSL version. It's not recommended to use version older than 1.2!", + "enum" : [ + "tlsv1", + "tlsv1_1", + "tlsv1_2", + "tlsv1_3" + ], + "optional" : 1, + "type" : "string" + }, + "sync-defaults-options" : { + "description" : "The default options for behavior of synchronizations.", + "format" : "realm-sync-options", + "optional" : 1, + "type" : "string", + "typetext" : "[enable-new=<1|0>] [,full=<1|0>] [,purge=<1|0>] [,remove-vanished=([acl];[properties];[entry])|none] [,scope=]" + }, + "sync_attributes" : { + "description" : "Comma separated list of key=value pairs for specifying which LDAP attributes map to which PVE user field. For example, to map the LDAP attribute 'mail' to PVEs 'email', write 'email=mail'. By default, each PVE user field is represented by an LDAP attribute of the same name.", + "optional" : 1, + "pattern" : "\\w+=[^,]+(,\\s*\\w+=[^,]+)*", + "type" : "string" + }, + "tfa" : { + "description" : "Use Two-factor authentication.", + "format" : "pve-tfa-config", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "type= [,digits=] [,id=] [,key=] [,step=] [,url=]" + }, + "user_attr" : { + "description" : "LDAP user attribute name", + "maxLength" : 256, + "optional" : 1, + "pattern" : "\\S{2,}", + "type" : "string" + }, + "user_classes" : { + "default" : "inetorgperson, posixaccount, person, user", + "description" : "The objectclasses for users.", + "format" : "ldap-simple-attr-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "verify" : { + "default" : 0, + "description" : "Verify the server's SSL certificate", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/access/realm", + [ + "Realm.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/access/domains/{realm}", + "text" : "{realm}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Authentication domain index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "description" : "Anyone can access that, because we need that list for the login box (before the user is authenticated).", + "user" : "world" + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "description" : "A comment. The GUI use this text when you select a domain (Realm) on the login window.", + "optional" : 1, + "type" : "string" + }, + "realm" : { + "type" : "string" + }, + "tfa" : { + "description" : "Two-factor authentication provider.", + "enum" : [ + "yubico", + "oath" + ], + "optional" : 1, + "type" : "string" + }, + "type" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{realm}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Add an authentication server.", + "method" : "POST", + "name" : "create", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "acr-values" : { + "description" : "Specifies the Authentication Context Class Reference values that theAuthorization Server is being requested to use for the Auth Request.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "autocreate" : { + "default" : 0, + "description" : "Automatically create users if they do not exist.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "base_dn" : { + "description" : "LDAP base domain name", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "bind_dn" : { + "description" : "LDAP bind domain name", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "capath" : { + "default" : "/etc/ssl/certs", + "description" : "Path to the CA certificate store", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "case-sensitive" : { + "default" : 1, + "description" : "username is case-sensitive", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "cert" : { + "description" : "Path to the client certificate", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "certkey" : { + "description" : "Path to the client certificate key", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "check-connection" : { + "default" : 0, + "description" : "Check bind connection to the server.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "client-id" : { + "description" : "OpenID Client ID", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "client-key" : { + "description" : "OpenID Client Key", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "comment" : { + "description" : "Description.", + "maxLength" : 4096, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "default" : { + "description" : "Use this as default realm", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "domain" : { + "description" : "AD domain name", + "maxLength" : 256, + "optional" : 1, + "pattern" : "\\S+", + "type" : "string" + }, + "filter" : { + "description" : "LDAP filter for user sync.", + "maxLength" : 2048, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group_classes" : { + "default" : "groupOfNames, group, univentionGroup, ipausergroup", + "description" : "The objectclasses for groups.", + "format" : "ldap-simple-attr-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group_dn" : { + "description" : "LDAP base domain name for group sync. If not set, the base_dn will be used.", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group_filter" : { + "description" : "LDAP filter for group sync.", + "maxLength" : 2048, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "group_name_attr" : { + "description" : "LDAP attribute representing a groups name. If not set or found, the first value of the DN will be used as name.", + "format" : "ldap-simple-attr", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "issuer-url" : { + "description" : "OpenID Issuer Url", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "mode" : { + "default" : "ldap", + "description" : "LDAP protocol mode.", + "enum" : [ + "ldap", + "ldaps", + "ldap+starttls" + ], + "optional" : 1, + "type" : "string" + }, + "password" : { + "description" : "LDAP bind password. Will be stored in '/etc/pve/priv/realm/.pw'.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "port" : { + "description" : "Server port.", + "maximum" : 65535, + "minimum" : 1, + "optional" : 1, + "type" : "integer", + "typetext" : " (1 - 65535)" + }, + "prompt" : { + "description" : "Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.", + "optional" : 1, + "pattern" : "(?:none|login|consent|select_account|\\S+)", + "type" : "string" + }, + "realm" : { + "description" : "Authentication domain ID", + "format" : "pve-realm", + "maxLength" : 32, + "type" : "string", + "typetext" : "" + }, + "scopes" : { + "default" : "email profile", + "description" : "Specifies the scopes (user details) that should be authorized and returned, for example 'email' or 'profile'.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "secure" : { + "description" : "Use secure LDAPS protocol. DEPRECATED: use 'mode' instead.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "server1" : { + "description" : "Server IP address (or DNS name)", + "format" : "address", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "server2" : { + "description" : "Fallback Server IP address (or DNS name)", + "format" : "address", + "maxLength" : 256, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "sslversion" : { + "description" : "LDAPS TLS/SSL version. It's not recommended to use version older than 1.2!", + "enum" : [ + "tlsv1", + "tlsv1_1", + "tlsv1_2", + "tlsv1_3" + ], + "optional" : 1, + "type" : "string" + }, + "sync-defaults-options" : { + "description" : "The default options for behavior of synchronizations.", + "format" : "realm-sync-options", + "optional" : 1, + "type" : "string", + "typetext" : "[enable-new=<1|0>] [,full=<1|0>] [,purge=<1|0>] [,remove-vanished=([acl];[properties];[entry])|none] [,scope=]" + }, + "sync_attributes" : { + "description" : "Comma separated list of key=value pairs for specifying which LDAP attributes map to which PVE user field. For example, to map the LDAP attribute 'mail' to PVEs 'email', write 'email=mail'. By default, each PVE user field is represented by an LDAP attribute of the same name.", + "optional" : 1, + "pattern" : "\\w+=[^,]+(,\\s*\\w+=[^,]+)*", + "type" : "string" + }, + "tfa" : { + "description" : "Use Two-factor authentication.", + "format" : "pve-tfa-config", + "maxLength" : 128, + "optional" : 1, + "type" : "string", + "typetext" : "type= [,digits=] [,id=] [,key=] [,step=] [,url=]" + }, + "type" : { + "description" : "Realm type.", + "enum" : [ + "ad", + "ldap", + "openid", + "pam", + "pve" + ], + "type" : "string" + }, + "user_attr" : { + "description" : "LDAP user attribute name", + "maxLength" : 256, + "optional" : 1, + "pattern" : "\\S{2,}", + "type" : "string" + }, + "user_classes" : { + "default" : "inetorgperson, posixaccount, person, user", + "description" : "The objectclasses for users.", + "format" : "ldap-simple-attr-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "username-claim" : { + "description" : "OpenID claim used to generate the unique username.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "verify" : { + "default" : 0, + "description" : "Verify the server's SSL certificate", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + } + }, + "type" : "object" + }, + "permissions" : { + "check" : [ + "perm", + "/access/realm", + [ + "Realm.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/access/domains", + "text" : "domains" + }, + { + "children" : [ + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : "Get the OpenId Authorization Url for the specified realm.", + "method" : "POST", + "name" : "auth_url", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "realm" : { + "description" : "Authentication domain ID", + "format" : "pve-realm", + "maxLength" : 32, + "type" : "string", + "typetext" : "" + }, + "redirect-url" : { + "description" : "Redirection Url. The client should set this to the used server url (location.origin).", + "maxLength" : 255, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "world" + }, + "protected" : 1, + "returns" : { + "description" : "Redirection URL.", + "type" : "string" + } + } + }, + "leaf" : 1, + "path" : "/access/openid/auth-url", + "text" : "auth-url" + }, + { + "info" : { + "POST" : { + "allowtoken" : 1, + "description" : " Verify OpenID authorization code and create a ticket.", + "method" : "POST", + "name" : "login", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "code" : { + "description" : "OpenId authorization code.", + "maxLength" : 4096, + "type" : "string", + "typetext" : "" + }, + "redirect-url" : { + "description" : "Redirection Url. The client should set this to the used server url (location.origin).", + "maxLength" : 255, + "type" : "string", + "typetext" : "" + }, + "state" : { + "description" : "OpenId state.", + "maxLength" : 1024, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "user" : "world" + }, + "protected" : 1, + "returns" : { + "properties" : { + "CSRFPreventionToken" : { + "type" : "string" + }, + "cap" : { + "type" : "object" + }, + "clustername" : { + "optional" : 1, + "type" : "string" + }, + "ticket" : { + "type" : "string" + }, + "username" : { + "type" : "string" + } + } + } + } + }, + "leaf" : 1, + "path" : "/access/openid/login", + "text" : "login" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "subdir" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{subdir}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/access/openid", + "text" : "openid" + }, + { + "children" : [ + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 0, + "description" : "Delete a TFA entry by ID.", + "method" : "DELETE", + "name" : "delete_tfa", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "A TFA entry id.", + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "The current password.", + "maxLength" : 64, + "minLength" : 5, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "userid-group", + [ + "User.Modify" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Fetch a requested TFA entry if present.", + "method" : "GET", + "name" : "get_tfa_entry", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "id" : { + "description" : "A TFA entry id.", + "type" : "string", + "typetext" : "" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "userid-group", + [ + "User.Modify", + "Sys.Audit" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "description" : "TFA Entry.", + "properties" : { + "created" : { + "description" : "Creation time of this entry as unix epoch.", + "type" : "integer" + }, + "description" : { + "description" : "User chosen description for this entry.", + "type" : "string" + }, + "enable" : { + "default" : 1, + "description" : "Whether this TFA entry is currently enabled.", + "optional" : 1, + "type" : "boolean" + }, + "id" : { + "description" : "The id used to reference this entry.", + "type" : "string" + }, + "type" : { + "description" : "TFA Entry Type.", + "enum" : [ + "totp", + "u2f", + "webauthn", + "recovery", + "yubico" + ], + "type" : "string" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 0, + "description" : "Add a TFA entry for a user.", + "method" : "PUT", + "name" : "update_tfa_entry", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "description" : { + "description" : "A description to distinguish multiple entries from one another", + "maxLength" : 255, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "enable" : { + "description" : "Whether the entry should be enabled for login.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "id" : { + "description" : "A TFA entry id.", + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "The current password.", + "maxLength" : 64, + "minLength" : 5, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "userid-group", + [ + "User.Modify" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/access/tfa/{userid}/{id}", + "text" : "{id}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List TFA configurations of users.", + "method" : "GET", + "name" : "list_user_tfa", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "userid-group", + [ + "User.Modify", + "Sys.Audit" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "description" : "A list of the user's TFA entries.", + "items" : { + "description" : "TFA Entry.", + "properties" : { + "created" : { + "description" : "Creation time of this entry as unix epoch.", + "type" : "integer" + }, + "description" : { + "description" : "User chosen description for this entry.", + "type" : "string" + }, + "enable" : { + "default" : 1, + "description" : "Whether this TFA entry is currently enabled.", + "optional" : 1, + "type" : "boolean" + }, + "id" : { + "description" : "The id used to reference this entry.", + "type" : "string" + }, + "type" : { + "description" : "TFA Entry Type.", + "enum" : [ + "totp", + "u2f", + "webauthn", + "recovery", + "yubico" + ], + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{id}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 0, + "description" : "Add a TFA entry for a user.", + "method" : "POST", + "name" : "add_tfa_entry", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "challenge" : { + "description" : "When responding to a u2f challenge: the original challenge string", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "description" : { + "description" : "A description to distinguish multiple entries from one another", + "maxLength" : 255, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "The current password.", + "maxLength" : 64, + "minLength" : 5, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "totp" : { + "description" : "A totp URI.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "description" : "TFA Entry Type.", + "enum" : [ + "totp", + "u2f", + "webauthn", + "recovery", + "yubico" + ], + "type" : "string" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + }, + "value" : { + "description" : "The current value for the provided totp URI, or a Webauthn/U2F challenge response", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "userid-group", + [ + "User.Modify" + ] + ] + ] + }, + "protected" : 1, + "returns" : { + "properties" : { + "challenge" : { + "description" : "When adding u2f entries, this contains a challenge the user must respond to in order to finish the registration.", + "optional" : 1, + "type" : "string" + }, + "id" : { + "description" : "The id of a newly added TFA entry.", + "type" : "string" + }, + "recovery" : { + "description" : "When adding recovery codes, this contains the list of codes to be displayed to the user", + "items" : { + "description" : "A recovery entry.", + "type" : "string" + }, + "optional" : 1, + "type" : "array" + } + }, + "type" : "object" + } + } + }, + "leaf" : 0, + "path" : "/access/tfa/{userid}", + "text" : "{userid}" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "List TFA configurations of users.", + "method" : "GET", + "name" : "list_tfa", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "description" : "Returns all or just the logged-in user, depending on privileges.", + "user" : "all" + }, + "protected" : 1, + "returns" : { + "description" : "The list tuples of user and TFA entries.", + "items" : { + "properties" : { + "entries" : { + "items" : { + "description" : "TFA Entry.", + "properties" : { + "created" : { + "description" : "Creation time of this entry as unix epoch.", + "type" : "integer" + }, + "description" : { + "description" : "User chosen description for this entry.", + "type" : "string" + }, + "enable" : { + "default" : 1, + "description" : "Whether this TFA entry is currently enabled.", + "optional" : 1, + "type" : "boolean" + }, + "id" : { + "description" : "The id used to reference this entry.", + "type" : "string" + }, + "type" : { + "description" : "TFA Entry Type.", + "enum" : [ + "totp", + "u2f", + "webauthn", + "recovery", + "yubico" + ], + "type" : "string" + } + }, + "type" : "object" + }, + "type" : "array" + }, + "tfa-locked-until" : { + "description" : "Contains a timestamp until when a user is locked out of 2nd factors.", + "optional" : 1, + "type" : "integer" + }, + "totp-locked" : { + "description" : "True if the user is currently locked out of TOTP factors.", + "optional" : 1, + "type" : "boolean" + }, + "userid" : { + "description" : "User this entry belongs to.", + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{userid}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/access/tfa", + "text" : "tfa" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Dummy. Useful for formatters which want to provide a login page.", + "method" : "GET", + "name" : "get_ticket", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "world" + }, + "returns" : { + "type" : "null" + } + }, + "POST" : { + "allowtoken" : 0, + "description" : "Create or verify authentication ticket.", + "method" : "POST", + "name" : "create_ticket", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "new-format" : { + "default" : 1, + "description" : "This parameter is now ignored and assumed to be 1.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "otp" : { + "description" : "One-time password for Two-factor authentication.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "password" : { + "description" : "The secret password. This can also be a valid ticket.", + "type" : "string", + "typetext" : "" + }, + "path" : { + "description" : "Verify ticket, and check if user have access 'privs' on 'path'", + "maxLength" : 64, + "optional" : 1, + "requires" : "privs", + "type" : "string", + "typetext" : "" + }, + "privs" : { + "description" : "Verify ticket, and check if user have access 'privs' on 'path'", + "format" : "pve-priv-list", + "maxLength" : 64, + "optional" : 1, + "requires" : "path", + "type" : "string", + "typetext" : "" + }, + "realm" : { + "description" : "You can optionally pass the realm using this parameter. Normally the realm is simply added to the username @.", + "format" : "pve-realm", + "maxLength" : 32, + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "tfa-challenge" : { + "description" : "The signed TFA challenge string the user wants to respond to.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "username" : { + "description" : "User name", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "description" : "You need to pass valid credientials.", + "user" : "world" + }, + "protected" : 1, + "returns" : { + "properties" : { + "CSRFPreventionToken" : { + "optional" : 1, + "type" : "string" + }, + "clustername" : { + "optional" : 1, + "type" : "string" + }, + "ticket" : { + "optional" : 1, + "type" : "string" + }, + "username" : { + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/access/ticket", + "text" : "ticket" + }, + { + "info" : { + "PUT" : { + "allowtoken" : 0, + "description" : "Change user password.", + "method" : "PUT", + "name" : "change_password", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "password" : { + "description" : "The new password.", + "maxLength" : 64, + "minLength" : 5, + "type" : "string", + "typetext" : "" + }, + "userid" : { + "description" : "Full User ID, in the `name@realm` format.", + "format" : "pve-userid", + "maxLength" : 64, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "or", + [ + "userid-param", + "self" + ], + [ + "and", + [ + "userid-param", + "Realm.AllocateUser" + ], + [ + "userid-group", + [ + "User.Modify" + ] + ] + ] + ], + "description" : "Each user is allowed to change his own password. A user can change the password of another user if he has 'Realm.AllocateUser' (on the realm of user ) and 'User.Modify' permission on /access/groups/ on a group where user is member of." + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/access/password", + "text" : "password" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Retrieve effective permissions of given user/token.", + "method" : "GET", + "name" : "permissions", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "path" : { + "description" : "Only dump this specific path, not the whole tree.", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "userid" : { + "description" : "User ID or full API token ID", + "optional" : 1, + "pattern" : "(?^:^(?^:[^\\s:/]+)\\@(?^:[A-Za-z][A-Za-z0-9\\.\\-_]+)(?:!(?^:[A-Za-z][A-Za-z0-9\\.\\-_]+))?$)", + "type" : "string" + } + } + }, + "permissions" : { + "description" : "Each user/token is allowed to dump their own permissions. A user can dump the permissions of another user if they have 'Sys.Audit' permission on /access.", + "user" : "all" + }, + "returns" : { + "description" : "Map of \"path\" => (Map of \"privilege\" => \"propagate boolean\").", + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/access/permissions", + "text" : "permissions" + } + ], + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "Directory index.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "subdir" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{subdir}", + "rel" : "child" + } + ], + "type" : "array" + } + } + }, + "leaf" : 0, + "path" : "/access", + "text" : "access" + }, + { + "children" : [ + { + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete pool (deprecated, no support for nested pools, use 'DELETE /pools/?poolid={poolid}').", + "method" : "DELETE", + "name" : "delete_pool_deprecated", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "poolid" : { + "format" : "pve-poolid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/pool/{poolid}", + [ + "Pool.Allocate" + ] + ], + "description" : "You can only delete empty pools (no members)." + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "Get pool configuration (deprecated, no support for nested pools, use 'GET /pools/?poolid={poolid}').", + "method" : "GET", + "name" : "read_pool", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "poolid" : { + "format" : "pve-poolid", + "type" : "string", + "typetext" : "" + }, + "type" : { + "enum" : [ + "qemu", + "lxc", + "storage" + ], + "optional" : 1, + "type" : "string" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/pool/{poolid}", + [ + "Pool.Audit" + ] + ] + }, + "returns" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "members" : { + "items" : { + "additionalProperties" : 1, + "properties" : { + "id" : { + "type" : "string" + }, + "node" : { + "type" : "string" + }, + "storage" : { + "optional" : 1, + "type" : "string" + }, + "type" : { + "enum" : [ + "qemu", + "lxc", + "openvz", + "storage" + ], + "type" : "string" + }, + "vmid" : { + "optional" : 1, + "type" : "integer" + } + }, + "type" : "object" + }, + "type" : "array" + } + }, + "type" : "object" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update pool data (deprecated, no support for nested pools - use 'PUT /pools/?poolid={poolid}' instead).", + "method" : "PUT", + "name" : "update_pool_deprecated", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "allow-move" : { + "default" : 0, + "description" : "Allow adding a guest even if already in another pool. The guest will be removed from its current pool and added to this one.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "default" : 0, + "description" : "Remove the passed VMIDs and/or storage IDs instead of adding them.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "poolid" : { + "format" : "pve-poolid", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "List of storage IDs to add or remove from this pool.", + "format" : "pve-storage-id-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vms" : { + "description" : "List of guest VMIDs to add or remove from this pool.", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/pool/{poolid}", + [ + "Pool.Allocate" + ] + ], + "description" : "You also need the right to modify permissions on any object you add/delete." + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 1, + "path" : "/pools/{poolid}", + "text" : "{poolid}" + } + ], + "info" : { + "DELETE" : { + "allowtoken" : 1, + "description" : "Delete pool.", + "method" : "DELETE", + "name" : "delete_pool", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "poolid" : { + "format" : "pve-poolid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/pool/{poolid}", + [ + "Pool.Allocate" + ] + ], + "description" : "You can only delete empty pools (no members)." + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "GET" : { + "allowtoken" : 1, + "description" : "List pools or get pool configuration.", + "method" : "GET", + "name" : "index", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "poolid" : { + "format" : "pve-poolid", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "type" : { + "enum" : [ + "qemu", + "lxc", + "storage" + ], + "optional" : 1, + "requires" : "poolid", + "type" : "string" + } + } + }, + "permissions" : { + "description" : "List all pools where you have Pool.Audit permissions on /pool/, or the pool specific with {poolid}", + "user" : "all" + }, + "returns" : { + "items" : { + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string" + }, + "members" : { + "items" : { + "additionalProperties" : 1, + "properties" : { + "id" : { + "type" : "string" + }, + "node" : { + "type" : "string" + }, + "storage" : { + "optional" : 1, + "type" : "string" + }, + "type" : { + "enum" : [ + "qemu", + "lxc", + "openvz", + "storage" + ], + "type" : "string" + }, + "vmid" : { + "optional" : 1, + "type" : "integer" + } + }, + "type" : "object" + }, + "optional" : 1, + "type" : "array" + }, + "poolid" : { + "type" : "string" + } + }, + "type" : "object" + }, + "links" : [ + { + "href" : "{poolid}", + "rel" : "child" + } + ], + "type" : "array" + } + }, + "POST" : { + "allowtoken" : 1, + "description" : "Create new pool.", + "method" : "POST", + "name" : "create_pool", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "poolid" : { + "format" : "pve-poolid", + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/pool/{poolid}", + [ + "Pool.Allocate" + ] + ] + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + }, + "PUT" : { + "allowtoken" : 1, + "description" : "Update pool.", + "method" : "PUT", + "name" : "update_pool", + "parameters" : { + "additionalProperties" : 0, + "properties" : { + "allow-move" : { + "default" : 0, + "description" : "Allow adding a guest even if already in another pool. The guest will be removed from its current pool and added to this one.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "comment" : { + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "delete" : { + "default" : 0, + "description" : "Remove the passed VMIDs and/or storage IDs instead of adding them.", + "optional" : 1, + "type" : "boolean", + "typetext" : "" + }, + "poolid" : { + "format" : "pve-poolid", + "type" : "string", + "typetext" : "" + }, + "storage" : { + "description" : "List of storage IDs to add or remove from this pool.", + "format" : "pve-storage-id-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + }, + "vms" : { + "description" : "List of guest VMIDs to add or remove from this pool.", + "format" : "pve-vmid-list", + "optional" : 1, + "type" : "string", + "typetext" : "" + } + } + }, + "permissions" : { + "check" : [ + "perm", + "/pool/{poolid}", + [ + "Pool.Allocate" + ] + ], + "description" : "You also need the right to modify permissions on any object you add/delete." + }, + "protected" : 1, + "returns" : { + "type" : "null" + } + } + }, + "leaf" : 0, + "path" : "/pools", + "text" : "pools" + }, + { + "info" : { + "GET" : { + "allowtoken" : 1, + "description" : "API version details, including some parts of the global datacenter config.", + "method" : "GET", + "name" : "version", + "parameters" : { + "additionalProperties" : 0 + }, + "permissions" : { + "user" : "all" + }, + "returns" : { + "properties" : { + "console" : { + "description" : "The default console viewer to use.", + "enum" : [ + "applet", + "vv", + "html5", + "xtermjs" + ], + "optional" : 1, + "type" : "string" + }, + "release" : { + "description" : "The current Proxmox VE point release in `x.y` format.", + "type" : "string" + }, + "repoid" : { + "description" : "The short git revision from which this version was build.", + "pattern" : "[0-9a-fA-F]{8,64}", + "type" : "string" + }, + "version" : { + "description" : "The full pve-manager package version of this node.", + "type" : "string" + } + }, + "type" : "object" + } + } + }, + "leaf" : 1, + "path" : "/version", + "text" : "version" + } +] +; + +let method2cmd = { + GET: 'get', + POST: 'create', + PUT: 'set', + DELETE: 'delete' +}; + +function cliUsageRenderer(method, path) { + return ` CLI:pvesh ${method2cmd[method]} ${path}`; +} +/*global apiSchema*/ + +Ext.onReady(function() { + Ext.define('pmx-param-schema', { + extend: 'Ext.data.Model', + fields: [ + 'name', 'type', 'typetext', 'description', 'verbose_description', + 'enum', 'minimum', 'maximum', 'minLength', 'maxLength', + 'pattern', 'title', 'requires', 'format', 'default', + 'disallow', 'extends', 'links', 'instance-types', + { + name: 'optional', + type: 'boolean', + }, + ], + }); + + let store = Ext.define('pmx-updated-treestore', { + extend: 'Ext.data.TreeStore', + model: Ext.define('pmx-api-doc', { + extend: 'Ext.data.Model', + fields: [ + 'path', 'info', 'text', + ], + }), + proxy: { + type: 'memory', + data: apiSchema, + }, + sorters: [{ + property: 'leaf', + direction: 'ASC', + }, { + property: 'text', + direction: 'ASC', + }], + filterer: 'bottomup', + doFilter: function(node) { + this.filterNodes(node, this.getFilters().getFilterFn(), true); + }, + + filterNodes: function(node, filterFn, parentVisible) { + let me = this; + + let match = filterFn(node) && (parentVisible || (node.isRoot() && !me.getRootVisible())); + + if (node.childNodes && node.childNodes.length) { + let bottomUpFiltering = me.filterer === 'bottomup'; + let childMatch; + for (const child of node.childNodes) { + childMatch = me.filterNodes(child, filterFn, match || bottomUpFiltering) || childMatch; + } + if (bottomUpFiltering) { + match = childMatch || match; + } + } + + node.set("visible", match, me._silentOptions); + return match; + }, + + }).create(); + + let render_description = function(value, metaData, record) { + let pdef = record.data; + + value = pdef.verbose_description || value; + + // TODO: try to render asciidoc correctly + + metaData.style = 'white-space:pre-wrap;'; + + return Ext.htmlEncode(value); + }; + + let render_type = function(value, metaData, record) { + let pdef = record.data; + + return pdef.enum ? 'enum' : pdef.type || 'string'; + }; + + const renderFormatString = function(obj) { + if (!Ext.isObject(obj)) { + return obj; + } + const mandatory = []; + const optional = []; + Object.entries(obj).forEach(function([name, param]) { + let list = param.optional ? optional : mandatory; + let str = param.default_key ? `[${name}=]` : `${name}=`; + if (param.alias) { + return; + } else if (param.enum) { + str += `(${param.enum?.join(' | ')})`; + } else { + str += `<${param.format_description || param.pattern || param.type}>`; + } + list.push(str); + }); + return mandatory.join(", ") + ' ' + optional.map(each => `[,${each}]`).join(' '); + }; + + let render_simple_format = function(pdef, type_fallback) { + if (pdef.typetext) { + return pdef.typetext; + } + if (pdef.enum) { + return pdef.enum.join(' | '); + } + if (pdef.format) { + return renderFormatString(pdef.format); + } + if (pdef.pattern) { + return pdef.pattern; + } + if (pdef.type === 'boolean') { + return ``; + } + if (type_fallback && pdef.type) { + return `<${pdef.type}>`; + } + if (pdef.minimum || pdef.maximum) { + return `${pdef.minimum || 'N'} - ${pdef.maximum || 'N'}`; + } + return ''; + }; + + let render_format = function(value, metaData, record) { + let pdef = record.data; + + metaData.style = 'white-space:normal;'; + + if (pdef.type === 'array' && pdef.items) { + let format = render_simple_format(pdef.items, true); + return `[${Ext.htmlEncode(format)}, ...]`; + } + + return Ext.htmlEncode(render_simple_format(pdef)); + }; + + let real_path = function(path) { + if (!path.match(/^[/]/)) { + path = `/${path}`; + } + return path.replace(/^.*\/_upgrade_(\/)?/, "/"); + }; + + let permission_text = function(permission) { + let permhtml = ""; + + if (permission.user) { + if (!permission.description) { + if (permission.user === 'world') { + permhtml += "Accessible without any authentication."; + } else if (permission.user === 'all') { + permhtml += "Accessible by all authenticated users."; + } else { + permhtml += `Only accessible by user "${permission.user}"`; + } + } + } else if (permission.check) { + permhtml += `
Check: ${Ext.htmlEncode(JSON.stringify(permission.check))}
`; + } else if (permission.userParam) { + permhtml += `
Check if user matches parameter '${permission.userParam}'`; + } else if (permission.or) { + permhtml += "
Or
"; + permhtml += permission.or.map(v => permission_text(v)).join(''); + permhtml += "
"; + } else if (permission.and) { + permhtml += "
And
"; + permhtml += permission.and.map(v => permission_text(v)).join(''); + permhtml += "
"; + } else { + permhtml += "Unknown syntax!"; + } + + return permhtml; + }; + + let render_docu = function(data) { + let md = data.info; + + let items = []; + + Ext.Array.each(['GET', 'POST', 'PUT', 'DELETE'], function(method) { + let info = md[method]; + if (info) { + let endpoint = real_path(data.path); + let usage = ``; + + if (typeof cliUsageRenderer === 'function') { + usage += cliUsageRenderer(method, endpoint); // eslint-disable-line no-undef + } + + let sections = [ + { + title: 'Description', + html: Ext.htmlEncode(info.description), + bodyPadding: 10, + }, + { + title: 'Usage', + html: usage, + bodyPadding: 10, + }, + ]; + + if (info.parameters && info.parameters.properties) { + let pstore = Ext.create('Ext.data.Store', { + model: 'pmx-param-schema', + proxy: { + type: 'memory', + }, + groupField: 'optional', + sorters: [ + { + property: 'instance-types', + direction: 'ASC', + }, + { + property: 'name', + direction: 'ASC', + }, + ], + }); + + let has_type_properties = false; + + Ext.Object.each(info.parameters.properties, function(name, pdef) { + if (pdef.oneOf) { + pdef.oneOf.forEach((alternative) => { + alternative.name = name; + pstore.add(alternative); + has_type_properties = true; + }); + } else if (pdef['instance-types']) { + pdef['instance-types'].forEach((type) => { + let typePdef = Ext.apply({}, pdef); + typePdef.name = name; + typePdef['instance-types'] = [type]; + pstore.add(typePdef); + has_type_properties = true; + }); + } else { + pdef.name = name; + pstore.add(pdef); + } + }); + + pstore.sort(); + + let groupingFeature = Ext.create('Ext.grid.feature.Grouping', { + enableGroupingMenu: false, + groupHeaderTpl: 'OptionalRequired', + }); + + sections.push({ + xtype: 'gridpanel', + title: 'Parameters', + features: [groupingFeature], + store: pstore, + viewConfig: { + trackOver: false, + stripeRows: true, + enableTextSelection: true, + }, + columns: [ + { + header: 'Name', + dataIndex: 'name', + flex: 1, + }, + { + header: 'Type', + dataIndex: 'type', + renderer: render_type, + flex: 1, + }, + { + header: 'For Types', + dataIndex: 'instance-types', + hidden: !has_type_properties, + flex: 1, + }, + { + header: 'Default', + dataIndex: 'default', + flex: 1, + }, + { + header: 'Format', + dataIndex: 'type', + renderer: render_format, + flex: 2, + }, + { + header: 'Description', + dataIndex: 'description', + renderer: render_description, + flex: 6, + }, + ], + }); + } + + if (info.returns) { + let retinf = info.returns; + let rtype = retinf.type; + if (!rtype && retinf.items) {rtype = 'array';} + if (!rtype) {rtype = 'object';} + + let rpstore = Ext.create('Ext.data.Store', { + model: 'pmx-param-schema', + proxy: { + type: 'memory', + }, + groupField: 'optional', + sorters: [ + { + property: 'name', + direction: 'ASC', + }, + ], + }); + + let properties; + if (rtype === 'array' && retinf.items.properties) { + properties = retinf.items.properties; + } + + if (rtype === 'object' && retinf.properties) { + properties = retinf.properties; + } + + Ext.Object.each(properties, function(name, pdef) { + pdef.name = name; + rpstore.add(pdef); + }); + + rpstore.sort(); + + let groupingFeature = Ext.create('Ext.grid.feature.Grouping', { + enableGroupingMenu: false, + groupHeaderTpl: 'OptionalObligatory', + }); + let returnhtml; + if (retinf.items) { + returnhtml = '
items: ' + Ext.htmlEncode(JSON.stringify(retinf.items, null, 4)) + '
'; + } + + if (retinf.properties) { + returnhtml = returnhtml || ''; + returnhtml += '
properties:' + Ext.htmlEncode(JSON.stringify(retinf.properties, null, 4)) + '
'; + } + + let rawSection = Ext.create('Ext.panel.Panel', { + bodyPadding: '0px 10px 10px 10px', + html: returnhtml, + hidden: true, + }); + + sections.push({ + xtype: 'gridpanel', + title: 'Returns: ' + rtype, + features: [groupingFeature], + store: rpstore, + viewConfig: { + trackOver: false, + stripeRows: true, + enableTextSelection: true, + }, + columns: [ + { + header: 'Name', + dataIndex: 'name', + flex: 1, + }, + { + header: 'Type', + dataIndex: 'type', + renderer: render_type, + flex: 1, + }, + { + header: 'Default', + dataIndex: 'default', + flex: 1, + }, + { + header: 'Format', + dataIndex: 'type', + renderer: render_format, + flex: 2, + }, + { + header: 'Description', + dataIndex: 'description', + renderer: render_description, + flex: 6, + }, + ], + bbar: [ + { + xtype: 'button', + text: 'Show RAW', + handler: function(btn) { + rawSection.setVisible(!rawSection.isVisible()); + btn.setText(rawSection.isVisible() ? 'Hide RAW' : 'Show RAW'); + }, + }, + ], + }); + + sections.push(rawSection); + } + + if (!data.path.match(/\/_upgrade_/)) { + let permhtml = ''; + + if (!info.permissions) { + permhtml = "Root only."; + } else { + if (info.permissions.description) { + permhtml += "
" + + Ext.htmlEncode(info.permissions.description) + "
"; + } + permhtml += permission_text(info.permissions); + } + + if (info.allowtoken !== undefined && !info.allowtoken) { + permhtml += "
This API endpoint is not available for API tokens."; + } + + sections.push({ + title: 'Required permissions', + bodyPadding: 10, + html: permhtml, + }); + } + + items.push({ + title: method, + autoScroll: true, + defaults: { + border: false, + }, + items: sections, + }); + } + }); + + let ct = Ext.getCmp('docview'); + ct.setTitle("Path: " + real_path(data.path)); + ct.removeAll(true); + ct.add(items); + ct.setActiveTab(0); + }; + + Ext.define('Ext.form.SearchField', { + extend: 'Ext.form.field.Text', + alias: 'widget.searchfield', + + emptyText: 'Search...', + + flex: 1, + + inputType: 'search', + listeners: { + 'change': function() { + let value = this.getValue(); + if (!Ext.isEmpty(value)) { + store.filter({ + property: 'path', + value: value, + anyMatch: true, + }); + } else { + store.clearFilter(); + } + }, + }, + }); + + let treePanel = Ext.create('Ext.tree.Panel', { + title: 'Resource Tree', + tbar: [ + { + xtype: 'searchfield', + }, + ], + tools: [ + { + type: 'expand', + tooltip: 'Expand all', + tooltipType: 'title', + callback: tree => tree.expandAll(), + }, + { + type: 'collapse', + tooltip: 'Collapse all', + tooltipType: 'title', + callback: tree => tree.collapseAll(), + }, + ], + store: store, + width: 200, + region: 'west', + split: true, + margins: '5 0 5 5', + rootVisible: false, + listeners: { + selectionchange: function(v, selections) { + if (!selections[0]) {return;} + let rec = selections[0]; + render_docu(rec.data); + location.hash = '#' + rec.data.path; + }, + }, + }); + + Ext.create('Ext.container.Viewport', { + layout: 'border', + renderTo: Ext.getBody(), + items: [ + treePanel, + { + xtype: 'tabpanel', + title: 'Documentation', + id: 'docview', + region: 'center', + margins: '5 5 5 0', + layout: 'fit', + items: [], + }, + ], + }); + + let deepLink = function() { + let path = window.location.hash.substring(1).replace(/\/\s*$/, ''); + let endpoint = store.findNode('path', path); + + if (endpoint) { + treePanel.getSelectionModel().select(endpoint); + treePanel.expandPath(endpoint.getPath()); + render_docu(endpoint.data); + } + }; + window.onhashchange = deepLink; + + deepLink(); +}); diff --git a/stable-7/pve-docs/api-viewer/apidoc.js.patch b/stable-8/pve-docs/api-viewer/apidoc.js.patch similarity index 81% rename from stable-7/pve-docs/api-viewer/apidoc.js.patch rename to stable-8/pve-docs/api-viewer/apidoc.js.patch index 477f9f4..281fcc3 100644 --- a/stable-7/pve-docs/api-viewer/apidoc.js.patch +++ b/stable-8/pve-docs/api-viewer/apidoc.js.patch @@ -1,6 +1,6 @@ ---- apidoc.js.orig 2021-11-15 10:07:34.000000000 -0500 -+++ apidoc.js 2021-12-06 08:04:01.648822707 -0500 -@@ -44064,6 +44064,31 @@ +--- apidoc.js.orig 2024-01-06 13:02:06.730512378 -0500 ++++ apidoc.js 2024-01-06 13:02:55.349787105 -0500 +@@ -50336,6 +50336,37 @@ "type" : "string", "typetext" : "" }, @@ -11,7 +11,13 @@ + "typetext" : "" + }, + "freenas_password" : { -+ "description" : "FreeNAS password for API access", ++ "description" : "FreeNAS password for API access (Deprecated)", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "truenas_secret" : { ++ "description" : "TrueNAS Secret for API access", + "optional" : 1, + "type" : "string", + "typetext" : "" @@ -32,7 +38,7 @@ "fuse" : { "description" : "Mount CephFS through FUSE.", "optional" : 1, -@@ -44275,6 +44300,12 @@ +@@ -50555,6 +50586,12 @@ "type" : "boolean", "typetext" : "" }, @@ -45,7 +51,7 @@ "transport" : { "description" : "Gluster transport: tcp or rdma", "enum" : [ -@@ -44547,6 +44578,31 @@ +@@ -50854,6 +50891,37 @@ "optional" : 1, "type" : "string", "typetext" : "" @@ -57,7 +63,13 @@ + "typetext" : "" + }, + "freenas_password" : { -+ "description" : "FreeNAS password for API access", ++ "description" : "FreeNAS password for API access (Deprecated)", ++ "optional" : 1, ++ "type" : "string", ++ "typetext" : "" ++ }, ++ "truenas_secret" : { ++ "description" : "TrueNAS secret for API access", + "optional" : 1, + "type" : "string", + "typetext" : "" diff --git a/stable-8/pve-manager/js/pvemanagerlib-8.0.5_1.js.patch b/stable-8/pve-manager/js/pvemanagerlib-8.0.5_1.js.patch new file mode 100644 index 0000000..9fa45ec --- /dev/null +++ b/stable-8/pve-manager/js/pvemanagerlib-8.0.5_1.js.patch @@ -0,0 +1,289 @@ +--- pvemanagerlib.js.orig 2023-12-30 15:36:27.913505863 -0500 ++++ pvemanagerlib.js 2024-01-02 09:30:56.000000000 -0500 +@@ -9228,6 +9228,7 @@ + alias: ['widget.pveiScsiProviderSelector'], + comboItems: [ + ['comstar', 'Comstar'], ++ ['freenas', 'FreeNAS/TrueNAS API'], + ['istgt', 'istgt'], + ['iet', 'IET'], + ['LIO', 'LIO'], +@@ -58017,16 +58018,24 @@ + me.callParent(); + }, + }); ++ + Ext.define('PVE.storage.ZFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + viewModel: { + parent: null, + data: { ++isComstar: true, ++ isFreeNAS: false, + isLIO: false, +- isComstar: true, ++ isToken: false, + hasWriteCacheOption: true, + }, ++formulas: { ++ hideUsername: function(get) { ++ return (!get('isFreeNAS') || !(get('isFreeNAS') && !get('isToken'))); ++ }, ++ }, + }, + + controller: { +@@ -58034,13 +58043,42 @@ + control: { + 'field[name=iscsiprovider]': { + change: 'changeISCSIProvider', ++}, ++ 'field[name=truenas_token_auth]': { ++ change: 'changeUsername', + }, + }, + changeISCSIProvider: function(f, newVal, oldVal) { ++var me = this; + var vm = this.getViewModel(); + vm.set('isLIO', newVal === 'LIO'); + vm.set('isComstar', newVal === 'comstar'); +- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt'); ++ vm.set('isFreeNAS', newVal === 'freenas'); ++ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt'); ++ if (newVal !== 'freenas') { ++ me.lookupReference('freenas_use_ssl_field').setValue(false); ++ me.lookupReference('truenas_token_auth_field').setValue(false); ++ me.lookupReference('freenas_apiv4_host_field').setValue(''); ++ me.lookupReference('freenas_user_field').setValue(''); ++ me.lookupReference('freenas_user_field').allowBlank = true; ++ me.lookupReference('truenas_secret_field').setValue(''); ++ me.lookupReference('truenas_secret_field').allowBlank = true; ++ me.lookupReference('truenas_confirm_secret_field').setValue(''); ++ me.lookupReference('truenas_confirm_secret_field').allowBlank = true; ++ } else { ++ me.lookupReference('freenas_user_field').allowBlank = false; ++ me.lookupReference('truenas_secret_field').allowBlank = false; ++ me.lookupReference('truenas_confirm_secret_field').allowBlank = false; ++ } ++ }, ++ changeUsername: function(f, newVal, oldVal) { ++ var me = this; ++ var vm = me.getViewModel(); ++ vm.set('isToken', newVal); ++ me.lookupReference('freenas_user_field').allowBlank = newVal; ++ if (newVal) { ++ me.lookupReference('freenas_user_field').setValue(''); ++ } + }, + }, + +@@ -58053,28 +58091,78 @@ + + values.nowritecache = values.writecache ? 0 : 1; + delete values.writecache; ++ console.warn(values.freenas_password); ++ if (values.freenas_password) { ++ values.truenas_secret = values.freenas_password; ++ } ++ console.warn(values.truenas_secret); + + return me.callParent([values]); + }, + + setValues: function(values) { +- values.writecache = values.nowritecache ? 0 : 1; +- this.callParent([values]); ++ if (values.freenas_password) { ++ values.truenas_secret = values.freenas_password; ++ } ++ values.truenas_confirm_secret = values.truenas_secret; ++ values.writecache = values.nowritecache ? 0 : 1; ++ this.callParent([values]); + }, + + initComponent: function() { +- var me = this; ++ var me = this; ++ ++ var tnsecret = Ext.create('Ext.form.TextField', { ++ xtype: 'proxmoxtextfield', ++ name: 'truenas_secret', ++ reference: 'truenas_secret_field', ++ inputType: me.isCreate ? '' : 'password', ++ value: '', ++ editable: true, ++ emptyText: Proxmox.Utils.noneText, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ fieldLabel: gettext('API Password'), ++ change: function(f, value) { ++ if (f.rendered) { ++ f.up().down('field[name=truenas_confirm_secret]').validate(); ++ } ++ }, ++ }); + +- me.column1 = [ +- { +- xtype: me.isCreate ? 'textfield' : 'displayfield', +- name: 'portal', ++ var tnconfirmsecret = Ext.create('Ext.form.TextField', { ++ xtype: 'proxmoxtextfield', ++ name: 'truenas_confirm_secret', ++ reference: 'truenas_confirm_secret_field', ++ inputType: me.isCreate ? '' : 'password', ++ value: '', ++ editable: true, ++ submitValue: false, ++ emptyText: Proxmox.Utils.noneText, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ fieldLabel: gettext('Confirm API Password'), ++ validator: function(value) { ++ var pw = me.up().down('field[name=truenas_secret]').getValue(); ++ if (pw !== value) { ++ return "Secrets do not match!"; ++ } ++ return true; ++ }, ++ }); ++ ++ me.column1 = [ ++ { ++ xtype: me.isCreate ? 'textfield' : 'displayfield', ++ name: 'portal', + value: '', + fieldLabel: gettext('Portal'), + allowBlank: false, + }, + { +- xtype: me.isCreate ? 'textfield' : 'displayfield', ++ xtype: 'textfield', + name: 'pool', + value: '', + fieldLabel: gettext('Pool'), +@@ -58084,11 +58172,11 @@ + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'blocksize', + value: '4k', +- fieldLabel: gettext('Block Size'), ++ fieldLabel: gettext('ZFS Block Size'), + allowBlank: false, + }, + { +- xtype: me.isCreate ? 'textfield' : 'displayfield', ++ xtype: 'textfield', + name: 'target', + value: '', + fieldLabel: gettext('Target'), +@@ -58099,8 +58187,59 @@ + name: 'comstar_tg', + value: '', + fieldLabel: gettext('Target group'), +- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, ++ bind: { ++ hidden: '{!isComstar}' ++ }, + allowBlank: true, ++}, ++ { ++ xtype: 'proxmoxcheckbox', ++ name: 'freenas_use_ssl', ++ reference: 'freenas_use_ssl_field', ++ inputId: 'freenas_use_ssl_field', ++ checked: false, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ uncheckedValue: 0, ++ fieldLabel: gettext('API use SSL'), ++ }, ++ { ++ xtype: 'proxmoxcheckbox', ++ name: 'truenas_token_auth', ++ reference: 'truenas_token_auth_field', ++ inputId: 'truenas_use_token_auth_field', ++ checked: false, ++ listeners: { ++ change: function(field, newValue) { ++ if (newValue === true) { ++ tnsecret.labelEl.update('API Token'); ++ tnconfirmsecret.labelEl.update('Confirm API Token'); ++ me.lookupReference('freenas_user_field').setValue(''); ++ me.lookupReference('freenas_user_field').allowBlank = true; ++ } else { ++ tnsecret.labelEl.update('API Password'); ++ tnconfirmsecret.labelEl.update('Confirm API Password'); ++ me.lookupReference('freenas_user_field').allowBlank = false; ++ } ++ }, ++ }, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ uncheckedValue: 0, ++ fieldLabel: gettext('API Token Auth'), ++ }, ++ { ++ xtype: 'textfield', ++ name: 'freenas_user', ++ reference: 'freenas_user_field', ++ inputId: 'freenas_user_field', ++ value: '', ++ fieldLabel: gettext('API Username'), ++ bind: { ++ hidden: '{hideUsername}' ++ }, + }, + ]; + +@@ -58131,7 +58270,9 @@ + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'comstar_hg', + value: '', +- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, ++ bind: { ++ hidden: '{!isComstar}' ++ }, + fieldLabel: gettext('Host group'), + allowBlank: true, + }, +@@ -58139,15 +58280,32 @@ + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'lio_tpg', + value: '', +- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' }, +- allowBlank: false, +- fieldLabel: gettext('Target portal group'), ++ bind: { ++ hidden: '{!isLIO}' ++ }, ++ fieldLabel: gettext('Target portal group'), ++ allowBlank: true + }, ++ { ++ xtype: 'proxmoxtextfield', ++ name: 'freenas_apiv4_host', ++ reference: 'freenas_apiv4_host_field', ++ value: '', ++ editable: true, ++ emptyText: Proxmox.Utils.noneText, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ fieldLabel: gettext('API IPv4 Host'), ++ }, ++ tnsecret, ++ tnconfirmsecret, + ]; + + me.callParent(); + }, + }); ++ + Ext.define('PVE.storage.ZFSPoolSelector', { + extend: 'PVE.form.ComboBoxSetStoreNode', + alias: 'widget.pveZFSPoolSelector', diff --git a/stable-8/pve-manager/js/pvemanagerlib.js.patch b/stable-8/pve-manager/js/pvemanagerlib.js.patch new file mode 100644 index 0000000..9fa45ec --- /dev/null +++ b/stable-8/pve-manager/js/pvemanagerlib.js.patch @@ -0,0 +1,289 @@ +--- pvemanagerlib.js.orig 2023-12-30 15:36:27.913505863 -0500 ++++ pvemanagerlib.js 2024-01-02 09:30:56.000000000 -0500 +@@ -9228,6 +9228,7 @@ + alias: ['widget.pveiScsiProviderSelector'], + comboItems: [ + ['comstar', 'Comstar'], ++ ['freenas', 'FreeNAS/TrueNAS API'], + ['istgt', 'istgt'], + ['iet', 'IET'], + ['LIO', 'LIO'], +@@ -58017,16 +58018,24 @@ + me.callParent(); + }, + }); ++ + Ext.define('PVE.storage.ZFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + viewModel: { + parent: null, + data: { ++isComstar: true, ++ isFreeNAS: false, + isLIO: false, +- isComstar: true, ++ isToken: false, + hasWriteCacheOption: true, + }, ++formulas: { ++ hideUsername: function(get) { ++ return (!get('isFreeNAS') || !(get('isFreeNAS') && !get('isToken'))); ++ }, ++ }, + }, + + controller: { +@@ -58034,13 +58043,42 @@ + control: { + 'field[name=iscsiprovider]': { + change: 'changeISCSIProvider', ++}, ++ 'field[name=truenas_token_auth]': { ++ change: 'changeUsername', + }, + }, + changeISCSIProvider: function(f, newVal, oldVal) { ++var me = this; + var vm = this.getViewModel(); + vm.set('isLIO', newVal === 'LIO'); + vm.set('isComstar', newVal === 'comstar'); +- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt'); ++ vm.set('isFreeNAS', newVal === 'freenas'); ++ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt'); ++ if (newVal !== 'freenas') { ++ me.lookupReference('freenas_use_ssl_field').setValue(false); ++ me.lookupReference('truenas_token_auth_field').setValue(false); ++ me.lookupReference('freenas_apiv4_host_field').setValue(''); ++ me.lookupReference('freenas_user_field').setValue(''); ++ me.lookupReference('freenas_user_field').allowBlank = true; ++ me.lookupReference('truenas_secret_field').setValue(''); ++ me.lookupReference('truenas_secret_field').allowBlank = true; ++ me.lookupReference('truenas_confirm_secret_field').setValue(''); ++ me.lookupReference('truenas_confirm_secret_field').allowBlank = true; ++ } else { ++ me.lookupReference('freenas_user_field').allowBlank = false; ++ me.lookupReference('truenas_secret_field').allowBlank = false; ++ me.lookupReference('truenas_confirm_secret_field').allowBlank = false; ++ } ++ }, ++ changeUsername: function(f, newVal, oldVal) { ++ var me = this; ++ var vm = me.getViewModel(); ++ vm.set('isToken', newVal); ++ me.lookupReference('freenas_user_field').allowBlank = newVal; ++ if (newVal) { ++ me.lookupReference('freenas_user_field').setValue(''); ++ } + }, + }, + +@@ -58053,28 +58091,78 @@ + + values.nowritecache = values.writecache ? 0 : 1; + delete values.writecache; ++ console.warn(values.freenas_password); ++ if (values.freenas_password) { ++ values.truenas_secret = values.freenas_password; ++ } ++ console.warn(values.truenas_secret); + + return me.callParent([values]); + }, + + setValues: function(values) { +- values.writecache = values.nowritecache ? 0 : 1; +- this.callParent([values]); ++ if (values.freenas_password) { ++ values.truenas_secret = values.freenas_password; ++ } ++ values.truenas_confirm_secret = values.truenas_secret; ++ values.writecache = values.nowritecache ? 0 : 1; ++ this.callParent([values]); + }, + + initComponent: function() { +- var me = this; ++ var me = this; ++ ++ var tnsecret = Ext.create('Ext.form.TextField', { ++ xtype: 'proxmoxtextfield', ++ name: 'truenas_secret', ++ reference: 'truenas_secret_field', ++ inputType: me.isCreate ? '' : 'password', ++ value: '', ++ editable: true, ++ emptyText: Proxmox.Utils.noneText, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ fieldLabel: gettext('API Password'), ++ change: function(f, value) { ++ if (f.rendered) { ++ f.up().down('field[name=truenas_confirm_secret]').validate(); ++ } ++ }, ++ }); + +- me.column1 = [ +- { +- xtype: me.isCreate ? 'textfield' : 'displayfield', +- name: 'portal', ++ var tnconfirmsecret = Ext.create('Ext.form.TextField', { ++ xtype: 'proxmoxtextfield', ++ name: 'truenas_confirm_secret', ++ reference: 'truenas_confirm_secret_field', ++ inputType: me.isCreate ? '' : 'password', ++ value: '', ++ editable: true, ++ submitValue: false, ++ emptyText: Proxmox.Utils.noneText, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ fieldLabel: gettext('Confirm API Password'), ++ validator: function(value) { ++ var pw = me.up().down('field[name=truenas_secret]').getValue(); ++ if (pw !== value) { ++ return "Secrets do not match!"; ++ } ++ return true; ++ }, ++ }); ++ ++ me.column1 = [ ++ { ++ xtype: me.isCreate ? 'textfield' : 'displayfield', ++ name: 'portal', + value: '', + fieldLabel: gettext('Portal'), + allowBlank: false, + }, + { +- xtype: me.isCreate ? 'textfield' : 'displayfield', ++ xtype: 'textfield', + name: 'pool', + value: '', + fieldLabel: gettext('Pool'), +@@ -58084,11 +58172,11 @@ + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'blocksize', + value: '4k', +- fieldLabel: gettext('Block Size'), ++ fieldLabel: gettext('ZFS Block Size'), + allowBlank: false, + }, + { +- xtype: me.isCreate ? 'textfield' : 'displayfield', ++ xtype: 'textfield', + name: 'target', + value: '', + fieldLabel: gettext('Target'), +@@ -58099,8 +58187,59 @@ + name: 'comstar_tg', + value: '', + fieldLabel: gettext('Target group'), +- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, ++ bind: { ++ hidden: '{!isComstar}' ++ }, + allowBlank: true, ++}, ++ { ++ xtype: 'proxmoxcheckbox', ++ name: 'freenas_use_ssl', ++ reference: 'freenas_use_ssl_field', ++ inputId: 'freenas_use_ssl_field', ++ checked: false, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ uncheckedValue: 0, ++ fieldLabel: gettext('API use SSL'), ++ }, ++ { ++ xtype: 'proxmoxcheckbox', ++ name: 'truenas_token_auth', ++ reference: 'truenas_token_auth_field', ++ inputId: 'truenas_use_token_auth_field', ++ checked: false, ++ listeners: { ++ change: function(field, newValue) { ++ if (newValue === true) { ++ tnsecret.labelEl.update('API Token'); ++ tnconfirmsecret.labelEl.update('Confirm API Token'); ++ me.lookupReference('freenas_user_field').setValue(''); ++ me.lookupReference('freenas_user_field').allowBlank = true; ++ } else { ++ tnsecret.labelEl.update('API Password'); ++ tnconfirmsecret.labelEl.update('Confirm API Password'); ++ me.lookupReference('freenas_user_field').allowBlank = false; ++ } ++ }, ++ }, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ uncheckedValue: 0, ++ fieldLabel: gettext('API Token Auth'), ++ }, ++ { ++ xtype: 'textfield', ++ name: 'freenas_user', ++ reference: 'freenas_user_field', ++ inputId: 'freenas_user_field', ++ value: '', ++ fieldLabel: gettext('API Username'), ++ bind: { ++ hidden: '{hideUsername}' ++ }, + }, + ]; + +@@ -58131,7 +58270,9 @@ + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'comstar_hg', + value: '', +- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, ++ bind: { ++ hidden: '{!isComstar}' ++ }, + fieldLabel: gettext('Host group'), + allowBlank: true, + }, +@@ -58139,15 +58280,32 @@ + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'lio_tpg', + value: '', +- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' }, +- allowBlank: false, +- fieldLabel: gettext('Target portal group'), ++ bind: { ++ hidden: '{!isLIO}' ++ }, ++ fieldLabel: gettext('Target portal group'), ++ allowBlank: true + }, ++ { ++ xtype: 'proxmoxtextfield', ++ name: 'freenas_apiv4_host', ++ reference: 'freenas_apiv4_host_field', ++ value: '', ++ editable: true, ++ emptyText: Proxmox.Utils.noneText, ++ bind: { ++ hidden: '{!isFreeNAS}' ++ }, ++ fieldLabel: gettext('API IPv4 Host'), ++ }, ++ tnsecret, ++ tnconfirmsecret, + ]; + + me.callParent(); + }, + }); ++ + Ext.define('PVE.storage.ZFSPoolSelector', { + extend: 'PVE.form.ComboBoxSetStoreNode', + alias: 'widget.pveZFSPoolSelector', diff --git a/stable-8/pve-manager/js/pvemanagerlib_working_copy.js b/stable-8/pve-manager/js/pvemanagerlib_working_copy.js new file mode 100644 index 0000000..056e847 --- /dev/null +++ b/stable-8/pve-manager/js/pvemanagerlib_working_copy.js @@ -0,0 +1,54196 @@ +const pveOnlineHelpInfo = { + "ceph_rados_block_devices" : { + "link" : "/pve-docs/chapter-pvesm.html#ceph_rados_block_devices", + "title" : "Ceph RADOS Block Devices (RBD)" + }, + "chapter_ha_manager" : { + "link" : "/pve-docs/chapter-ha-manager.html#chapter_ha_manager", + "title" : "High Availability" + }, + "chapter_lvm" : { + "link" : "/pve-docs/chapter-sysadmin.html#chapter_lvm", + "title" : "Logical Volume Manager (LVM)" + }, + "chapter_pct" : { + "link" : "/pve-docs/chapter-pct.html#chapter_pct", + "title" : "Proxmox Container Toolkit" + }, + "chapter_pve_firewall" : { + "link" : "/pve-docs/chapter-pve-firewall.html#chapter_pve_firewall", + "title" : "Proxmox VE Firewall" + }, + "chapter_pveceph" : { + "link" : "/pve-docs/chapter-pveceph.html#chapter_pveceph", + "title" : "Deploy Hyper-Converged Ceph Cluster" + }, + "chapter_pvecm" : { + "link" : "/pve-docs/chapter-pvecm.html#chapter_pvecm", + "title" : "Cluster Manager" + }, + "chapter_pvesdn" : { + "link" : "/pve-docs/chapter-pvesdn.html#chapter_pvesdn", + "title" : "Software Defined Network" + }, + "chapter_pvesr" : { + "link" : "/pve-docs/chapter-pvesr.html#chapter_pvesr", + "title" : "Storage Replication" + }, + "chapter_storage" : { + "link" : "/pve-docs/chapter-pvesm.html#chapter_storage", + "title" : "Proxmox VE Storage" + }, + "chapter_system_administration" : { + "link" : "/pve-docs/chapter-sysadmin.html#chapter_system_administration", + "title" : "Host System Administration" + }, + "chapter_user_management" : { + "link" : "/pve-docs/chapter-pveum.html#chapter_user_management", + "title" : "User Management" + }, + "chapter_virtual_machines" : { + "link" : "/pve-docs/chapter-qm.html#chapter_virtual_machines", + "title" : "QEMU/KVM Virtual Machines" + }, + "chapter_vzdump" : { + "link" : "/pve-docs/chapter-vzdump.html#chapter_vzdump", + "title" : "Backup and Restore" + }, + "chapter_zfs" : { + "link" : "/pve-docs/chapter-sysadmin.html#chapter_zfs", + "title" : "ZFS on Linux" + }, + "datacenter_configuration_file" : { + "link" : "/pve-docs/pve-admin-guide.html#datacenter_configuration_file", + "title" : "Datacenter Configuration" + }, + "external_metric_server" : { + "link" : "/pve-docs/chapter-sysadmin.html#external_metric_server", + "title" : "External Metric Server" + }, + "getting_help" : { + "link" : "/pve-docs/pve-admin-guide.html#getting_help", + "title" : "Getting Help" + }, + "gui_my_settings" : { + "link" : "/pve-docs/chapter-pve-gui.html#gui_my_settings", + "subtitle" : "My Settings", + "title" : "Graphical User Interface" + }, + "ha_manager_crs" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_crs", + "subtitle" : "Cluster Resource Scheduling", + "title" : "High Availability" + }, + "ha_manager_fencing" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_fencing", + "subtitle" : "Fencing", + "title" : "High Availability" + }, + "ha_manager_groups" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_groups", + "subtitle" : "Groups", + "title" : "High Availability" + }, + "ha_manager_resource_config" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resource_config", + "subtitle" : "Resources", + "title" : "High Availability" + }, + "ha_manager_resources" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resources", + "subtitle" : "Resources", + "title" : "High Availability" + }, + "ha_manager_shutdown_policy" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_shutdown_policy", + "subtitle" : "Shutdown Policy", + "title" : "High Availability" + }, + "markdown_basics" : { + "link" : "/pve-docs/pve-admin-guide.html#markdown_basics", + "title" : "Markdown Primer" + }, + "metric_server_graphite" : { + "link" : "/pve-docs/chapter-sysadmin.html#metric_server_graphite", + "subtitle" : "Graphite server configuration", + "title" : "External Metric Server" + }, + "metric_server_influxdb" : { + "link" : "/pve-docs/chapter-sysadmin.html#metric_server_influxdb", + "subtitle" : "Influxdb plugin configuration", + "title" : "External Metric Server" + }, + "pct_configuration" : { + "link" : "/pve-docs/chapter-pct.html#pct_configuration", + "subtitle" : "Configuration", + "title" : "Proxmox Container Toolkit" + }, + "pct_container_images" : { + "link" : "/pve-docs/chapter-pct.html#pct_container_images", + "subtitle" : "Container Images", + "title" : "Proxmox Container Toolkit" + }, + "pct_container_network" : { + "link" : "/pve-docs/chapter-pct.html#pct_container_network", + "subtitle" : "Network", + "title" : "Proxmox Container Toolkit" + }, + "pct_container_storage" : { + "link" : "/pve-docs/chapter-pct.html#pct_container_storage", + "subtitle" : "Container Storage", + "title" : "Proxmox Container Toolkit" + }, + "pct_cpu" : { + "link" : "/pve-docs/chapter-pct.html#pct_cpu", + "subtitle" : "CPU", + "title" : "Proxmox Container Toolkit" + }, + "pct_general" : { + "link" : "/pve-docs/chapter-pct.html#pct_general", + "subtitle" : "General Settings", + "title" : "Proxmox Container Toolkit" + }, + "pct_memory" : { + "link" : "/pve-docs/chapter-pct.html#pct_memory", + "subtitle" : "Memory", + "title" : "Proxmox Container Toolkit" + }, + "pct_migration" : { + "link" : "/pve-docs/chapter-pct.html#pct_migration", + "subtitle" : "Migration", + "title" : "Proxmox Container Toolkit" + }, + "pct_options" : { + "link" : "/pve-docs/chapter-pct.html#pct_options", + "subtitle" : "Options", + "title" : "Proxmox Container Toolkit" + }, + "pct_startup_and_shutdown" : { + "link" : "/pve-docs/chapter-pct.html#pct_startup_and_shutdown", + "subtitle" : "Automatic Start and Shutdown of Containers", + "title" : "Proxmox Container Toolkit" + }, + "proxmox_node_management" : { + "link" : "/pve-docs/chapter-sysadmin.html#proxmox_node_management", + "title" : "Proxmox Node Management" + }, + "pve_admin_guide" : { + "link" : "/pve-docs/pve-admin-guide.html", + "title" : "Proxmox VE Administration Guide" + }, + "pve_ceph_install" : { + "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_install", + "subtitle" : "CLI Installation of Ceph Packages", + "title" : "Deploy Hyper-Converged Ceph Cluster" + }, + "pve_ceph_osds" : { + "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_osds", + "subtitle" : "Ceph OSDs", + "title" : "Deploy Hyper-Converged Ceph Cluster" + }, + "pve_ceph_pools" : { + "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_pools", + "subtitle" : "Ceph Pools", + "title" : "Deploy Hyper-Converged Ceph Cluster" + }, + "pve_documentation_index" : { + "link" : "/pve-docs/index.html", + "title" : "Proxmox VE Documentation Index" + }, + "pve_firewall_cluster_wide_setup" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_cluster_wide_setup", + "subtitle" : "Cluster Wide Setup", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_host_specific_configuration" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_host_specific_configuration", + "subtitle" : "Host Specific Configuration", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_ip_aliases" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_aliases", + "subtitle" : "IP Aliases", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_ip_sets" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_sets", + "subtitle" : "IP Sets", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_security_groups" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_security_groups", + "subtitle" : "Security Groups", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_vm_container_configuration" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_vm_container_configuration", + "subtitle" : "VM/Container Configuration", + "title" : "Proxmox VE Firewall" + }, + "pve_service_daemons" : { + "link" : "/pve-docs/index.html#_service_daemons", + "title" : "Service Daemons" + }, + "pveceph_fs" : { + "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs", + "subtitle" : "CephFS", + "title" : "Deploy Hyper-Converged Ceph Cluster" + }, + "pveceph_fs_create" : { + "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs_create", + "subtitle" : "Create CephFS", + "title" : "Deploy Hyper-Converged Ceph Cluster" + }, + "pvecm_create_cluster" : { + "link" : "/pve-docs/chapter-pvecm.html#pvecm_create_cluster", + "subtitle" : "Create a Cluster", + "title" : "Cluster Manager" + }, + "pvecm_join_node_to_cluster" : { + "link" : "/pve-docs/chapter-pvecm.html#pvecm_join_node_to_cluster", + "subtitle" : "Adding Nodes to the Cluster", + "title" : "Cluster Manager" + }, + "pvesdn_config_controllers" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_controllers", + "subtitle" : "Controllers", + "title" : "Software Defined Network" + }, + "pvesdn_config_vnet" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_vnet", + "subtitle" : "VNets", + "title" : "Software Defined Network" + }, + "pvesdn_config_zone" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_zone", + "subtitle" : "Zones", + "title" : "Software Defined Network" + }, + "pvesdn_controller_plugin_evpn" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_controller_plugin_evpn", + "subtitle" : "EVPN Controller", + "title" : "Software Defined Network" + }, + "pvesdn_dns_plugin_powerdns" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_dns_plugin_powerdns", + "subtitle" : "PowerDNS Plugin", + "title" : "Software Defined Network" + }, + "pvesdn_ipam_plugin_netbox" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_netbox", + "subtitle" : "NetBox IPAM Plugin", + "title" : "Software Defined Network" + }, + "pvesdn_ipam_plugin_phpipam" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_phpipam", + "subtitle" : "phpIPAM Plugin", + "title" : "Software Defined Network" + }, + "pvesdn_ipam_plugin_pveipam" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_pveipam", + "subtitle" : "Proxmox VE IPAM Plugin", + "title" : "Software Defined Network" + }, + "pvesdn_zone_plugin_evpn" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_evpn", + "subtitle" : "EVPN Zones", + "title" : "Software Defined Network" + }, + "pvesdn_zone_plugin_qinq" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_qinq", + "subtitle" : "QinQ Zones", + "title" : "Software Defined Network" + }, + "pvesdn_zone_plugin_simple" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_simple", + "subtitle" : "Simple Zones", + "title" : "Software Defined Network" + }, + "pvesdn_zone_plugin_vlan" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_vlan", + "subtitle" : "VLAN Zones", + "title" : "Software Defined Network" + }, + "pvesdn_zone_plugin_vxlan" : { + "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_vxlan", + "subtitle" : "VXLAN Zones", + "title" : "Software Defined Network" + }, + "pvesr_schedule_time_format" : { + "link" : "/pve-docs/chapter-pvesr.html#pvesr_schedule_time_format", + "subtitle" : "Schedule Format", + "title" : "Storage Replication" + }, + "pveum_authentication_realms" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_authentication_realms", + "subtitle" : "Authentication Realms", + "title" : "User Management" + }, + "pveum_configure_u2f" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_configure_u2f", + "subtitle" : "Server Side U2F Configuration", + "title" : "User Management" + }, + "pveum_configure_webauthn" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_configure_webauthn", + "subtitle" : "Server Side Webauthn Configuration", + "title" : "User Management" + }, + "pveum_groups" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_groups", + "subtitle" : "Groups", + "title" : "User Management" + }, + "pveum_ldap_sync" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_ldap_sync", + "subtitle" : "Syncing LDAP-Based Realms", + "title" : "User Management" + }, + "pveum_permission_management" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_permission_management", + "subtitle" : "Permission Management", + "title" : "User Management" + }, + "pveum_pools" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_pools", + "subtitle" : "Pools", + "title" : "User Management" + }, + "pveum_roles" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_roles", + "subtitle" : "Roles", + "title" : "User Management" + }, + "pveum_tokens" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_tokens", + "subtitle" : "API Tokens", + "title" : "User Management" + }, + "pveum_users" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_users", + "subtitle" : "Users", + "title" : "User Management" + }, + "qm_bios_and_uefi" : { + "link" : "/pve-docs/chapter-qm.html#qm_bios_and_uefi", + "subtitle" : "BIOS and UEFI", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_bootorder" : { + "link" : "/pve-docs/chapter-qm.html#qm_bootorder", + "subtitle" : "Device Boot Order", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_cloud_init" : { + "link" : "/pve-docs/chapter-qm.html#qm_cloud_init", + "title" : "Cloud-Init Support" + }, + "qm_copy_and_clone" : { + "link" : "/pve-docs/chapter-qm.html#qm_copy_and_clone", + "subtitle" : "Copies and Clones", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_cpu" : { + "link" : "/pve-docs/chapter-qm.html#qm_cpu", + "subtitle" : "CPU", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_display" : { + "link" : "/pve-docs/chapter-qm.html#qm_display", + "subtitle" : "Display", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_general_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_general_settings", + "subtitle" : "General Settings", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_hard_disk" : { + "link" : "/pve-docs/chapter-qm.html#qm_hard_disk", + "subtitle" : "Hard Disk", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_memory" : { + "link" : "/pve-docs/chapter-qm.html#qm_memory", + "subtitle" : "Memory", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_migration" : { + "link" : "/pve-docs/chapter-qm.html#qm_migration", + "subtitle" : "Migration", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_network_device" : { + "link" : "/pve-docs/chapter-qm.html#qm_network_device", + "subtitle" : "Network Device", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_options" : { + "link" : "/pve-docs/chapter-qm.html#qm_options", + "subtitle" : "Options", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_os_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_os_settings", + "subtitle" : "OS Settings", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_pci_passthrough_vm_config" : { + "link" : "/pve-docs/chapter-qm.html#qm_pci_passthrough_vm_config", + "subtitle" : "VM Configuration", + "title" : "PCI(e) Passthrough" + }, + "qm_qemu_agent" : { + "link" : "/pve-docs/chapter-qm.html#qm_qemu_agent", + "subtitle" : "QEMU Guest Agent", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_spice_enhancements" : { + "link" : "/pve-docs/chapter-qm.html#qm_spice_enhancements", + "subtitle" : "SPICE Enhancements", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_startup_and_shutdown" : { + "link" : "/pve-docs/chapter-qm.html#qm_startup_and_shutdown", + "subtitle" : "Automatic Start and Shutdown of Virtual Machines", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_system_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_system_settings", + "subtitle" : "System Settings", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_usb_passthrough" : { + "link" : "/pve-docs/chapter-qm.html#qm_usb_passthrough", + "subtitle" : "USB Passthrough", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_virtio_rng" : { + "link" : "/pve-docs/chapter-qm.html#qm_virtio_rng", + "subtitle" : "VirtIO RNG", + "title" : "QEMU/KVM Virtual Machines" + }, + "qm_virtual_machines_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_virtual_machines_settings", + "subtitle" : "Virtual Machines Settings", + "title" : "QEMU/KVM Virtual Machines" + }, + "storage_btrfs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_btrfs", + "title" : "BTRFS Backend" + }, + "storage_cephfs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_cephfs", + "title" : "Ceph Filesystem (CephFS)" + }, + "storage_cifs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_cifs", + "title" : "CIFS Backend" + }, + "storage_directory" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_directory", + "title" : "Directory Backend" + }, + "storage_glusterfs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_glusterfs", + "title" : "GlusterFS Backend" + }, + "storage_lvm" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_lvm", + "title" : "LVM Backend" + }, + "storage_lvmthin" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_lvmthin", + "title" : "LVM thin Backend" + }, + "storage_nfs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_nfs", + "title" : "NFS Backend" + }, + "storage_open_iscsi" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_open_iscsi", + "title" : "Open-iSCSI initiator" + }, + "storage_pbs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_pbs", + "title" : "Proxmox Backup Server" + }, + "storage_pbs_encryption" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_pbs_encryption", + "subtitle" : "Encryption", + "title" : "Proxmox Backup Server" + }, + "storage_zfspool" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_zfspool", + "title" : "Local ZFS Pool Backend" + }, + "sysadmin_certificate_management" : { + "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certificate_management", + "title" : "Certificate Management" + }, + "sysadmin_certs_acme_plugins" : { + "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certs_acme_plugins", + "subtitle" : "ACME Plugins", + "title" : "Certificate Management" + }, + "sysadmin_network_configuration" : { + "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_network_configuration", + "title" : "Network Configuration" + }, + "sysadmin_package_repositories" : { + "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_package_repositories", + "title" : "Package Repositories" + }, + "user-realms-ldap" : { + "link" : "/pve-docs/chapter-pveum.html#user-realms-ldap", + "subtitle" : "LDAP", + "title" : "User Management" + }, + "user_mgmt" : { + "link" : "/pve-docs/chapter-pveum.html#user_mgmt", + "title" : "User Management" + }, + "vzdump_retention" : { + "link" : "/pve-docs/chapter-vzdump.html#vzdump_retention", + "subtitle" : "Backup Retention", + "title" : "Backup and Restore" + } +}; +// Some configuration values are complex strings - so we need parsers/generators for them. +Ext.define('PVE.Parser', { + statics: { + + // this class only contains static functions + + printACME: function(value) { + if (Ext.isArray(value.domains)) { + value.domains = value.domains.join(';'); + } + return PVE.Parser.printPropertyString(value); + }, + + parseACME: function(value) { + if (!value) { + return {}; + } + + let res = {}; + try { + value.split(',').forEach(property => { + let [k, v] = property.split('=', 2); + if (Ext.isDefined(v)) { + res[k] = v; + } else { + throw `Failed to parse key-value pair: ${property}`; + } + }); + } catch (err) { + console.warn(err); + return undefined; + } + + if (res.domains !== undefined) { + res.domains = res.domains.split(/;/); + } + + return res; + }, + + parseBoolean: function(value, default_value) { + if (!Ext.isDefined(value)) { + return default_value; + } + value = value.toLowerCase(); + return value === '1' || + value === 'on' || + value === 'yes' || + value === 'true'; + }, + + parsePropertyString: function(value, defaultKey) { + let res = {}; + + if (typeof value !== 'string' || value === '') { + return res; + } + + try { + value.split(',').forEach(property => { + let [k, v] = property.split('=', 2); + if (Ext.isDefined(v)) { + res[k] = v; + } else if (Ext.isDefined(defaultKey)) { + if (Ext.isDefined(res[defaultKey])) { + throw 'defaultKey may be only defined once in propertyString'; + } + res[defaultKey] = k; // k ist the value in this case + } else { + throw `Failed to parse key-value pair: ${property}`; + } + }); + } catch (err) { + console.warn(err); + return undefined; + } + + return res; + }, + + printPropertyString: function(data, defaultKey) { + var stringparts = [], + gotDefaultKeyVal = false, + defaultKeyVal; + + Ext.Object.each(data, function(key, value) { + if (defaultKey !== undefined && key === defaultKey) { + gotDefaultKeyVal = true; + defaultKeyVal = value; + } else if (value !== '') { + stringparts.push(key + '=' + value); + } + }); + + stringparts = stringparts.sort(); + if (gotDefaultKeyVal) { + stringparts.unshift(defaultKeyVal); + } + + return stringparts.join(','); + }, + + parseQemuNetwork: function(key, value) { + if (!(key && value)) { + return undefined; + } + + let res = {}, + errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return undefined; // continue + } + + let match_res; + + if ((match_res = p.match(/^(ne2k_pci|e1000|e1000-82540em|e1000-82544gc|e1000-82545em|vmxnet3|rtl8139|pcnet|virtio|ne2k_isa|i82551|i82557b|i82559er)(=([0-9a-f]{2}(:[0-9a-f]{2}){5}))?$/i)) !== null) { + res.model = match_res[1].toLowerCase(); + if (match_res[3]) { + res.macaddr = match_res[3]; + } + } else if ((match_res = p.match(/^bridge=(\S+)$/)) !== null) { + res.bridge = match_res[1]; + } else if ((match_res = p.match(/^rate=(\d+(\.\d+)?)$/)) !== null) { + res.rate = match_res[1]; + } else if ((match_res = p.match(/^tag=(\d+(\.\d+)?)$/)) !== null) { + res.tag = match_res[1]; + } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) { + res.firewall = match_res[1]; + } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) { + res.disconnect = match_res[1]; + } else if ((match_res = p.match(/^queues=(\d+)$/)) !== null) { + res.queues = match_res[1]; + } else if ((match_res = p.match(/^trunks=(\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*)$/)) !== null) { + res.trunks = match_res[1]; + } else if ((match_res = p.match(/^mtu=(\d+)$/)) !== null) { + res.mtu = match_res[1]; + } else { + errors = true; + return false; // break + } + return undefined; // continue + }); + + if (errors || !res.model) { + return undefined; + } + + return res; + }, + + printQemuNetwork: function(net) { + var netstr = net.model; + if (net.macaddr) { + netstr += "=" + net.macaddr; + } + if (net.bridge) { + netstr += ",bridge=" + net.bridge; + if (net.tag) { + netstr += ",tag=" + net.tag; + } + if (net.firewall) { + netstr += ",firewall=" + net.firewall; + } + } + if (net.rate) { + netstr += ",rate=" + net.rate; + } + if (net.queues) { + netstr += ",queues=" + net.queues; + } + if (net.disconnect) { + netstr += ",link_down=" + net.disconnect; + } + if (net.trunks) { + netstr += ",trunks=" + net.trunks; + } + if (net.mtu) { + netstr += ",mtu=" + net.mtu; + } + return netstr; + }, + + parseQemuDrive: function(key, value) { + if (!(key && value)) { + return undefined; + } + + const [, bus, index] = key.match(/^([a-z]+)(\d+)$/); + if (!bus) { + return undefined; + } + let res = { + 'interface': bus, + index, + }; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return undefined; // continue + } + let match = p.match(/^([a-z_]+)=(\S+)$/); + if (!match) { + if (!p.match(/[=]/)) { + res.file = p; + return undefined; // continue + } + errors = true; + return false; // break + } + let [, k, v] = match; + if (k === 'volume') { + k = 'file'; + } + + if (Ext.isDefined(res[k])) { + errors = true; + return false; // break + } + + if (k === 'cache' && v === 'off') { + v = 'none'; + } + + res[k] = v; + + return undefined; // continue + }); + + if (errors || !res.file) { + return undefined; + } + + return res; + }, + + printQemuDrive: function(drive) { + var drivestr = drive.file; + + Ext.Object.each(drive, function(key, value) { + if (!Ext.isDefined(value) || key === 'file' || + key === 'index' || key === 'interface') { + return; // continue + } + drivestr += ',' + key + '=' + value; + }); + + return drivestr; + }, + + parseIPConfig: function(key, value) { + if (!(key && value)) { + return undefined; // continue + } + + let res = {}; + try { + value.split(',').forEach(p => { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + + const match = p.match(/^(ip|gw|ip6|gw6)=(\S+)$/); + if (!match) { + throw `could not parse as IP config: ${p}`; + } + let [, k, v] = match; + res[k] = v; + }); + } catch (err) { + console.warn(err); + return undefined; // continue + } + + return res; + }, + + printIPConfig: function(cfg) { + return Object.entries(cfg) + .filter(([k, v]) => v && k.match(/^(ip|gw|ip6|gw6)$/)) + .map(([k, v]) => `${k}=${v}`) + .join(','); + }, + + parseLxcNetwork: function(value) { + if (!value) { + return undefined; + } + + let data = {}; + value.split(',').forEach(p => { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + let match_res = p.match(/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|tag|rate)=(\S+)$/); + if (match_res) { + data[match_res[1]] = match_res[2]; + } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) { + data.firewall = PVE.Parser.parseBoolean(match_res[1]); + } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) { + data.link_down = PVE.Parser.parseBoolean(match_res[1]); + } else if (!p.match(/^type=\S+$/)) { + console.warn(`could not parse LXC network string ${p}`); + } + }); + + return data; + }, + + printLxcNetwork: function(config) { + let knownKeys = { + bridge: 1, + firewall: 1, + gw6: 1, + gw: 1, + hwaddr: 1, + ip6: 1, + ip: 1, + mtu: 1, + name: 1, + rate: 1, + tag: 1, + link_down: 1, + }; + return Object.entries(config) + .filter(([k, v]) => v !== undefined && v !== '' && knownKeys[k]) + .map(([k, v]) => `${k}=${v}`) + .join(','); + }, + + parseLxcMountPoint: function(value) { + if (!value) { + return undefined; + } + + let res = {}; + let errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return undefined; // continue + } + let match = p.match(/^([a-z_]+)=(.+)$/); + if (!match) { + if (!p.match(/[=]/)) { + res.file = p; + return undefined; // continue + } + errors = true; + return false; // break + } + let [, k, v] = match; + if (k === 'volume') { + k = 'file'; + } + + if (Ext.isDefined(res[k])) { + errors = true; + return false; // break + } + + res[k] = v; + + return undefined; + }); + + if (errors || !res.file) { + return undefined; + } + + const match = res.file.match(/^([a-z][a-z0-9\-_.]*[a-z0-9]):/i); + if (match) { + res.storage = match[1]; + res.type = 'volume'; + } else if (res.file.match(/^\/dev\//)) { + res.type = 'device'; + } else { + res.type = 'bind'; + } + + return res; + }, + + printLxcMountPoint: function(mp) { + let drivestr = mp.file; + for (const [key, value] of Object.entries(mp)) { + if (!Ext.isDefined(value) || key === 'file' || key === 'type' || key === 'storage') { + continue; + } + drivestr += `,${key}=${value}`; + } + return drivestr; + }, + + parseStartup: function(value) { + if (value === undefined) { + return undefined; + } + + let res = {}; + try { + value.split(',').forEach(p => { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + + let match_res; + if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) { + res.order = match_res[2]; + } else if ((match_res = p.match(/^up=(\d+)$/)) !== null) { + res.up = match_res[1]; + } else if ((match_res = p.match(/^down=(\d+)$/)) !== null) { + res.down = match_res[1]; + } else { + throw `could not parse startup config ${p}`; + } + }); + } catch (err) { + console.warn(err); + return undefined; + } + + return res; + }, + + printStartup: function(startup) { + let arr = []; + if (startup.order !== undefined && startup.order !== '') { + arr.push('order=' + startup.order); + } + if (startup.up !== undefined && startup.up !== '') { + arr.push('up=' + startup.up); + } + if (startup.down !== undefined && startup.down !== '') { + arr.push('down=' + startup.down); + } + + return arr.join(','); + }, + + parseQemuSmbios1: function(value) { + let res = value.split(',').reduce((acc, currentValue) => { + const [k, v] = currentValue.split(/[=](.+)/); + acc[k] = v; + return acc; + }, {}); + + if (PVE.Parser.parseBoolean(res.base64, false)) { + for (const [k, v] of Object.entries(res)) { + if (k !== 'uuid') { + res[k] = Ext.util.Base64.decode(v); + } + } + } + + return res; + }, + + printQemuSmbios1: function(data) { + let base64 = false; + let datastr = Object.entries(data) + .map(([key, value]) => { + if (value === '') { + return undefined; + } + if (key !== 'uuid') { + base64 = true; // smbios values can be arbitrary, so encode and mark config as such + value = Ext.util.Base64.encode(value); + } + return `${key}=${value}`; + }) + .filter(v => v !== undefined) + .join(','); + + if (base64) { + datastr += ',base64=1'; + } + return datastr; + }, + + parseTfaConfig: function(value) { + let res = {}; + value.split(',').forEach(p => { + const [k, v] = p.split('=', 2); + res[k] = v; + }); + + return res; + }, + + parseTfaType: function(value) { + let match; + if (!value || !value.length) { + return undefined; + } else if (value === 'x!oath') { + return 'totp'; + } else if ((match = value.match(/^x!(.+)$/)) !== null) { + return match[1]; + } else { + return 1; + } + }, + + parseQemuCpu: function(value) { + if (!value) { + return {}; + } + + let res = {}; + let errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return undefined; // continue + } + + if (!p.match(/[=]/)) { + if (Ext.isDefined(res.cpu)) { + errors = true; + return false; // break + } + res.cputype = p; + return undefined; // continue + } + + let match = p.match(/^([a-z_]+)=(\S+)$/); + if (!match || Ext.isDefined(res[match[1]])) { + errors = true; + return false; // break + } + + let [, k, v] = match; + res[k] = v; + + return undefined; + }); + + if (errors || !res.cputype) { + return undefined; + } + + return res; + }, + + printQemuCpu: function(cpu) { + let cpustr = cpu.cputype; + let optstr = ''; + + Ext.Object.each(cpu, function(key, value) { + if (!Ext.isDefined(value) || key === 'cputype') { + return; // continue + } + optstr += ',' + key + '=' + value; + }); + + if (!cpustr) { + if (optstr) { + return 'kvm64' + optstr; + } else { + return undefined; + } + } + + return cpustr + optstr; + }, + + parseSSHKey: function(key) { + // |--- options can have quotes--| type key comment + let keyre = /^(?:((?:[^\s"]|"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/; + let typere = /^(?:(?:sk-)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)(?:@(?:[a-z0-9_-]+\.)+[a-z]{2,})?)$/; + + let m = key.match(keyre); + if (!m || m.length < 3 || !m[2]) { // [2] is always either type or key + return null; + } + if (m[1] && m[1].match(typere)) { + return { + type: m[1], + key: m[2], + comment: m[3], + }; + } + if (m[2].match(typere)) { + return { + options: m[1], + type: m[2], + key: m[3], + comment: m[4], + }; + } + return null; + }, + + parseACMEPluginData: function(data) { + let res = {}; + let extradata = []; + data.split('\n').forEach((line) => { + // capture everything after the first = as value + let [key, value] = line.split(/[=](.+)/); + if (value !== undefined) { + res[key] = value; + } else { + extradata.push(line); + } + }); + return [res, extradata]; + }, +}, +}); +/* This state provider keeps part of the state inside the browser history. + * + * We compress (shorten) url using dictionary based compression, i.e., we use + * column separated list instead of url encoded hash: + * #v\d* version/format + * := indicates string values + * :\d+ lookup value in dictionary hash + * #v1:=value1:5:=value2:=value3:... +*/ + +Ext.define('PVE.StateProvider', { + extend: 'Ext.state.LocalStorageProvider', + + // private + setHV: function(name, newvalue, fireEvents) { + let me = this; + + let changes = false; + let oldtext = Ext.encode(me.UIState[name]); + let newtext = Ext.encode(newvalue); + if (newtext !== oldtext) { + changes = true; + me.UIState[name] = newvalue; + if (fireEvents) { + me.fireEvent("statechange", me, name, { value: newvalue }); + } + } + return changes; + }, + + // private + hslist: [ + // order is important for notifications + // [ name, default ] + ['view', 'server'], + ['rid', 'root'], + ['ltab', 'tasks'], + ['nodetab', ''], + ['storagetab', ''], + ['sdntab', ''], + ['pooltab', ''], + ['kvmtab', ''], + ['lxctab', ''], + ['dctab', ''], + ], + + hprefix: 'v1', + + compDict: { + tfa: 54, + sdn: 53, + cloudinit: 52, + replication: 51, + system: 50, + monitor: 49, + 'ha-fencing': 48, + 'ha-groups': 47, + 'ha-resources': 46, + 'ceph-log': 45, + 'ceph-crushmap': 44, + 'ceph-pools': 43, + 'ceph-osdtree': 42, + 'ceph-disklist': 41, + 'ceph-monlist': 40, + 'ceph-config': 39, + ceph: 38, + 'firewall-fwlog': 37, + 'firewall-options': 36, + 'firewall-ipset': 35, + 'firewall-aliases': 34, + 'firewall-sg': 33, + firewall: 32, + apt: 31, + members: 30, + snapshot: 29, + ha: 28, + support: 27, + pools: 26, + syslog: 25, + ubc: 24, + initlog: 23, + openvz: 22, + backup: 21, + resources: 20, + content: 19, + root: 18, + domains: 17, + roles: 16, + groups: 15, + users: 14, + time: 13, + dns: 12, + network: 11, + services: 10, + options: 9, + console: 8, + hardware: 7, + permissions: 6, + summary: 5, + tasks: 4, + clog: 3, + storage: 2, + folder: 1, + server: 0, + }, + + decodeHToken: function(token) { + let me = this; + + let state = {}; + if (!token) { + me.hslist.forEach(([k, v]) => { state[k] = v; }); + return state; + } + + let [prefix, ...items] = token.split(':'); + + if (prefix !== me.hprefix) { + return me.decodeHToken(); + } + + Ext.Array.each(me.hslist, function(rec) { + let value = items.shift(); + if (value) { + if (value[0] === '=') { + value = decodeURIComponent(value.slice(1)); + } + for (const [key, hash] of Object.entries(me.compDict)) { + if (String(value) === String(hash)) { + value = key; + break; + } + } + } + state[rec[0]] = value; + }); + + return state; + }, + + encodeHToken: function(state) { + let me = this; + + let ctoken = me.hprefix; + Ext.Array.each(me.hslist, function(rec) { + let value = state[rec[0]]; + if (!Ext.isDefined(value)) { + value = rec[1]; + } + value = encodeURIComponent(value); + if (!value) { + ctoken += ':'; + } else if (Ext.isDefined(me.compDict[value])) { + ctoken += ":" + me.compDict[value]; + } else { + ctoken += ":=" + value; + } + }); + + return ctoken; + }, + + constructor: function(config) { + let me = this; + + me.callParent([config]); + + me.UIState = me.decodeHToken(); // set default + + let history_change_cb = function(token) { + if (!token) { + Ext.History.back(); + return; + } + + let newstate = me.decodeHToken(token); + Ext.Array.each(me.hslist, function(rec) { + if (typeof newstate[rec[0]] === "undefined") { + return; + } + me.setHV(rec[0], newstate[rec[0]], true); + }); + }; + + let start_token = Ext.History.getToken(); + if (start_token) { + history_change_cb(start_token); + } else { + let htext = me.encodeHToken(me.UIState); + Ext.History.add(htext); + } + + Ext.History.on('change', history_change_cb); + }, + + get: function(name, defaultValue) { + let me = this; + + let data; + if (typeof me.UIState[name] !== "undefined") { + data = { value: me.UIState[name] }; + } else { + data = me.callParent(arguments); + if (!data && name === 'GuiCap') { + data = { + vms: {}, + storage: {}, + access: {}, + nodes: {}, + dc: {}, + sdn: {}, + }; + } + } + return data; + }, + + clear: function(name) { + let me = this; + + if (typeof me.UIState[name] !== "undefined") { + me.UIState[name] = null; + } + me.callParent(arguments); + }, + + set: function(name, value, fireevent) { + let me = this; + + if (typeof me.UIState[name] !== "undefined") { + var newvalue = value ? value.value : null; + if (me.setHV(name, newvalue, fireevent)) { + let htext = me.encodeHToken(me.UIState); + Ext.History.add(htext); + } + } else { + me.callParent(arguments); + } + }, +}); +Ext.ns('PVE'); + +console.log("Starting Proxmox VE Manager"); + +Ext.Ajax.defaultHeaders = { + 'Accept': 'application/json', +}; + +Ext.define('PVE.Utils', { + utilities: { + + // this singleton contains miscellaneous utilities + + toolkit: undefined, // (extjs|touch), set inside Toolkit.js + + bus_match: /^(ide|sata|virtio|scsi)(\d+)$/, + + log_severity_hash: { + 0: "panic", + 1: "alert", + 2: "critical", + 3: "error", + 4: "warning", + 5: "notice", + 6: "info", + 7: "debug", + }, + + support_level_hash: { + 'c': gettext('Community'), + 'b': gettext('Basic'), + 's': gettext('Standard'), + 'p': gettext('Premium'), + }, + + noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit ' + +'' + +'www.proxmox.com to get a list of available options.', + + kvm_ostypes: { + 'Linux': [ + { desc: '6.x - 2.6 Kernel', val: 'l26' }, + { desc: '2.4 Kernel', val: 'l24' }, + ], + 'Microsoft Windows': [ + { desc: '11/2022', val: 'win11' }, + { desc: '10/2016/2019', val: 'win10' }, + { desc: '8.x/2012/2012r2', val: 'win8' }, + { desc: '7/2008r2', val: 'win7' }, + { desc: 'Vista/2008', val: 'w2k8' }, + { desc: 'XP/2003', val: 'wxp' }, + { desc: '2000', val: 'w2k' }, + ], + 'Solaris Kernel': [ + { desc: '-', val: 'solaris' }, + ], + 'Other': [ + { desc: '-', val: 'other' }, + ], + }, + + is_windows: function(ostype) { + for (let entry of PVE.Utils.kvm_ostypes['Microsoft Windows']) { + if (entry.val === ostype) { + return true; + } + } + return false; + }, + + get_health_icon: function(state, circle) { + if (circle === undefined) { + circle = false; + } + + if (state === undefined) { + state = 'uknown'; + } + + var icon = 'faded fa-question'; + switch (state) { + case 'good': + icon = 'good fa-check'; + break; + case 'upgrade': + icon = 'warning fa-upload'; + break; + case 'old': + icon = 'warning fa-refresh'; + break; + case 'warning': + icon = 'warning fa-exclamation'; + break; + case 'critical': + icon = 'critical fa-times'; + break; + default: break; + } + + if (circle) { + icon += '-circle'; + } + + return icon; + }, + + parse_ceph_version: function(service) { + if (service.ceph_version_short) { + return service.ceph_version_short; + } + + if (service.ceph_version) { + var match = service.ceph_version.match(/version (\d+(\.\d+)*)/); + if (match) { + return match[1]; + } + } + + return undefined; + }, + + compare_ceph_versions: function(a, b) { + let avers = []; + let bvers = []; + + if (a === b) { + return 0; + } + + if (Ext.isArray(a)) { + avers = a.slice(); // copy array + } else { + avers = a.toString().split('.'); + } + + if (Ext.isArray(b)) { + bvers = b.slice(); // copy array + } else { + bvers = b.toString().split('.'); + } + + for (;;) { + let av = avers.shift(); + let bv = bvers.shift(); + + if (av === undefined && bv === undefined) { + return 0; + } else if (av === undefined) { + return -1; + } else if (bv === undefined) { + return 1; + } else { + let diff = parseInt(av, 10) - parseInt(bv, 10); + if (diff !== 0) return diff; + // else we need to look at the next parts + } + } + }, + + get_ceph_icon_html: function(health, fw) { + var state = PVE.Utils.map_ceph_health[health]; + var cls = PVE.Utils.get_health_icon(state); + if (fw) { + cls += ' fa-fw'; + } + return " "; + }, + + map_ceph_health: { + 'HEALTH_OK': 'good', + 'HEALTH_UPGRADE': 'upgrade', + 'HEALTH_OLD': 'old', + 'HEALTH_WARN': 'warning', + 'HEALTH_ERR': 'critical', + }, + + render_sdn_pending: function(rec, value, key, index) { + if (rec.data.state === undefined || rec.data.state === null) { + return value; + } + + if (rec.data.state === 'deleted') { + if (value === undefined) { + return ' '; + } else { + return '
'+ value +'
'; + } + } else if (rec.data.pending[key] !== undefined && rec.data.pending[key] !== null) { + if (rec.data.pending[key] === 'deleted') { + return ' '; + } else { + return rec.data.pending[key]; + } + } + return value; + }, + + render_sdn_pending_state: function(rec, value) { + if (value === undefined || value === null) { + return ' '; + } + + let icon = ``; + + if (value === 'deleted') { + return '' + icon + value + ''; + } + + let tip = gettext('Pending Changes') + ':
'; + + for (const [key, keyvalue] of Object.entries(rec.data.pending)) { + if ((rec.data[key] !== undefined && rec.data.pending[key] !== rec.data[key]) || + rec.data[key] === undefined + ) { + tip += `${key}: ${keyvalue}
`; + } + } + return ''+ icon + value + ''; + }, + + render_ceph_health: function(healthObj) { + var state = { + iconCls: PVE.Utils.get_health_icon(), + text: '', + }; + + if (!healthObj || !healthObj.status) { + return state; + } + + var health = PVE.Utils.map_ceph_health[healthObj.status]; + + state.iconCls = PVE.Utils.get_health_icon(health, true); + state.text = healthObj.status; + + return state; + }, + + render_zfs_health: function(value) { + if (typeof value === 'undefined') { + return ""; + } + var iconCls = 'question-circle'; + switch (value) { + case 'AVAIL': + case 'ONLINE': + iconCls = 'check-circle good'; + break; + case 'REMOVED': + case 'DEGRADED': + iconCls = 'exclamation-circle warning'; + break; + case 'UNAVAIL': + case 'FAULTED': + case 'OFFLINE': + iconCls = 'times-circle critical'; + break; + default: //unknown + } + + return ' ' + value; + }, + + render_pbs_fingerprint: fp => fp.substring(0, 23), + + render_backup_encryption: function(v, meta, record) { + if (!v) { + return gettext('No'); + } + + let tip = ''; + if (v.match(/^[a-fA-F0-9]{2}:/)) { // fingerprint + tip = `Key fingerprint ${PVE.Utils.render_pbs_fingerprint(v)}`; + } + let icon = ``; + return `${icon} ${gettext('Encrypted')}`; + }, + + render_backup_verification: function(v, meta, record) { + let i = (cls, txt) => ` ${txt}`; + if (v === undefined || v === null) { + return i('question-circle-o warning', gettext('None')); + } + let tip = ""; + let txt = gettext('Failed'); + let iconCls = 'times critical'; + if (v.state === 'ok') { + txt = gettext('OK'); + iconCls = 'check good'; + let now = Date.now() / 1000; + let task = Proxmox.Utils.parse_task_upid(v.upid); + let verify_time = Proxmox.Utils.render_timestamp(task.starttime); + tip = `Last verify task started on ${verify_time}`; + if (now - v.starttime > 30 * 24 * 60 * 60) { + tip = `Last verify task over 30 days ago: ${verify_time}`; + iconCls = 'check warning'; + } + } + return ` ${i(iconCls, txt)} `; + }, + + render_backup_status: function(value, meta, record) { + if (typeof value === 'undefined') { + return ""; + } + + let iconCls = 'check-circle good'; + let text = gettext('Yes'); + + if (!PVE.Parser.parseBoolean(value.toString())) { + iconCls = 'times-circle critical'; + + text = gettext('No'); + + let reason = record.get('reason'); + if (typeof reason !== 'undefined') { + if (reason in PVE.Utils.backup_reasons_table) { + reason = PVE.Utils.backup_reasons_table[record.get('reason')]; + } + text = `${text} - ${reason}`; + } + } + + return ` ${text}`; + }, + + render_backup_days_of_week: function(val) { + var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + var selected = []; + var cur = -1; + val.split(',').forEach(function(day) { + cur++; + var dow = (dows.indexOf(day)+6)%7; + if (cur === dow) { + if (selected.length === 0 || selected[selected.length-1] === 0) { + selected.push(1); + } else { + selected[selected.length-1]++; + } + } else { + while (cur < dow) { + cur++; + selected.push(0); + } + selected.push(1); + } + }); + + cur = -1; + var days = []; + selected.forEach(function(item) { + cur++; + if (item > 2) { + days.push(Ext.Date.dayNames[cur+1] + '-' + Ext.Date.dayNames[(cur+item)%7]); + cur += item-1; + } else if (item === 2) { + days.push(Ext.Date.dayNames[cur+1]); + days.push(Ext.Date.dayNames[(cur+2)%7]); + cur++; + } else if (item === 1) { + days.push(Ext.Date.dayNames[(cur+1)%7]); + } + }); + return days.join(', '); + }, + + render_backup_selection: function(value, metaData, record) { + let allExceptText = gettext('All except {0}'); + let allText = '-- ' + gettext('All') + ' --'; + if (record.data.all) { + if (record.data.exclude) { + return Ext.String.format(allExceptText, record.data.exclude); + } + return allText; + } + if (record.data.vmid) { + return record.data.vmid; + } + + if (record.data.pool) { + return "Pool '"+ record.data.pool + "'"; + } + + return "-"; + }, + + backup_reasons_table: { + 'backup=yes': gettext('Enabled'), + 'backup=no': gettext('Disabled'), + 'enabled': gettext('Enabled'), + 'disabled': gettext('Disabled'), + 'not a volume': gettext('Not a volume'), + 'efidisk but no OMVF BIOS': gettext('EFI Disk without OMVF BIOS'), + }, + + renderNotFound: what => Ext.String.format(gettext("No {0} found"), what), + + get_kvm_osinfo: function(value) { + var info = { base: 'Other' }; // default + if (value) { + Ext.each(Object.keys(PVE.Utils.kvm_ostypes), function(k) { + Ext.each(PVE.Utils.kvm_ostypes[k], function(e) { + if (e.val === value) { + info = { desc: e.desc, base: k }; + } + }); + }); + } + return info; + }, + + render_kvm_ostype: function(value) { + var osinfo = PVE.Utils.get_kvm_osinfo(value); + if (osinfo.desc && osinfo.desc !== '-') { + return osinfo.base + ' ' + osinfo.desc; + } else { + return osinfo.base; + } + }, + + render_hotplug_features: function(value) { + var fa = []; + + if (!value || value === '0') { + return gettext('Disabled'); + } + + if (value === '1') { + value = 'disk,network,usb'; + } + + Ext.each(value.split(','), function(el) { + if (el === 'disk') { + fa.push(gettext('Disk')); + } else if (el === 'network') { + fa.push(gettext('Network')); + } else if (el === 'usb') { + fa.push('USB'); + } else if (el === 'memory') { + fa.push(gettext('Memory')); + } else if (el === 'cpu') { + fa.push(gettext('CPU')); + } else { + fa.push(el); + } + }); + + return fa.join(', '); + }, + + render_localtime: function(value) { + if (value === '__default__') { + return Proxmox.Utils.defaultText + ' (' + gettext('Enabled for Windows') + ')'; + } + return Proxmox.Utils.format_boolean(value); + }, + + render_qga_features: function(config) { + if (!config) { + return Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')'; + } + let qga = PVE.Parser.parsePropertyString(config, 'enabled'); + if (!PVE.Parser.parseBoolean(qga.enabled)) { + return Proxmox.Utils.disabledText; + } + delete qga.enabled; + + let agentstring = Proxmox.Utils.enabledText; + + for (const [key, value] of Object.entries(qga)) { + let displayText = Proxmox.Utils.disabledText; + if (key === 'type') { + let map = { + isa: "ISA", + virtio: "VirtIO", + }; + displayText = map[value] || Proxmox.Utils.unknownText; + } else if (PVE.Parser.parseBoolean(value)) { + displayText = Proxmox.Utils.enabledText; + } + agentstring += `, ${key}: ${displayText}`; + } + + return agentstring; + }, + + render_qemu_machine: function(value) { + return value || Proxmox.Utils.defaultText + ' (i440fx)'; + }, + + render_qemu_bios: function(value) { + if (!value) { + return Proxmox.Utils.defaultText + ' (SeaBIOS)'; + } else if (value === 'seabios') { + return "SeaBIOS"; + } else if (value === 'ovmf') { + return "OVMF (UEFI)"; + } else { + return value; + } + }, + + render_dc_ha_opts: function(value) { + if (!value) { + return Proxmox.Utils.defaultText; + } else { + return PVE.Parser.printPropertyString(value); + } + }, + render_as_property_string: v => !v ? Proxmox.Utils.defaultText : PVE.Parser.printPropertyString(v), + + render_scsihw: function(value) { + if (!value || value === '__default__') { + return Proxmox.Utils.defaultText + ' (LSI 53C895A)'; + } else if (value === 'lsi') { + return 'LSI 53C895A'; + } else if (value === 'lsi53c810') { + return 'LSI 53C810'; + } else if (value === 'megasas') { + return 'MegaRAID SAS 8708EM2'; + } else if (value === 'virtio-scsi-pci') { + return 'VirtIO SCSI'; + } else if (value === 'virtio-scsi-single') { + return 'VirtIO SCSI single'; + } else if (value === 'pvscsi') { + return 'VMware PVSCSI'; + } else { + return value; + } + }, + + render_spice_enhancements: function(values) { + let props = PVE.Parser.parsePropertyString(values); + if (Ext.Object.isEmpty(props)) { + return Proxmox.Utils.noneText; + } + + let output = []; + if (PVE.Parser.parseBoolean(props.foldersharing)) { + output.push('Folder Sharing: ' + gettext('Enabled')); + } + if (props.videostreaming === 'all' || props.videostreaming === 'filter') { + output.push('Video Streaming: ' + props.videostreaming); + } + return output.join(', '); + }, + + // fixme: auto-generate this + // for now, please keep in sync with PVE::Tools::kvmkeymaps + kvm_keymaps: { + '__default__': Proxmox.Utils.defaultText, + //ar: 'Arabic', + da: 'Danish', + de: 'German', + 'de-ch': 'German (Swiss)', + 'en-gb': 'English (UK)', + 'en-us': 'English (USA)', + es: 'Spanish', + //et: 'Estonia', + fi: 'Finnish', + //fo: 'Faroe Islands', + fr: 'French', + 'fr-be': 'French (Belgium)', + 'fr-ca': 'French (Canada)', + 'fr-ch': 'French (Swiss)', + //hr: 'Croatia', + hu: 'Hungarian', + is: 'Icelandic', + it: 'Italian', + ja: 'Japanese', + lt: 'Lithuanian', + //lv: 'Latvian', + mk: 'Macedonian', + nl: 'Dutch', + //'nl-be': 'Dutch (Belgium)', + no: 'Norwegian', + pl: 'Polish', + pt: 'Portuguese', + 'pt-br': 'Portuguese (Brazil)', + //ru: 'Russian', + sl: 'Slovenian', + sv: 'Swedish', + //th: 'Thai', + tr: 'Turkish', + }, + + kvm_vga_drivers: { + '__default__': Proxmox.Utils.defaultText, + std: gettext('Standard VGA'), + vmware: gettext('VMware compatible'), + qxl: 'SPICE', + qxl2: 'SPICE dual monitor', + qxl3: 'SPICE three monitors', + qxl4: 'SPICE four monitors', + serial0: gettext('Serial terminal') + ' 0', + serial1: gettext('Serial terminal') + ' 1', + serial2: gettext('Serial terminal') + ' 2', + serial3: gettext('Serial terminal') + ' 3', + virtio: 'VirtIO-GPU', + 'virtio-gl': 'VirGL GPU', + none: Proxmox.Utils.noneText, + }, + + render_kvm_language: function(value) { + if (!value || value === '__default__') { + return Proxmox.Utils.defaultText; + } + let text = PVE.Utils.kvm_keymaps[value]; + return text ? `${text} (${value})` : value; + }, + + console_map: { + '__default__': Proxmox.Utils.defaultText + ' (xterm.js)', + 'vv': 'SPICE (remote-viewer)', + 'html5': 'HTML5 (noVNC)', + 'xtermjs': 'xterm.js', + }, + + render_console_viewer: function(value) { + value = value || '__default__'; + return PVE.Utils.console_map[value] || value; + }, + + render_kvm_vga_driver: function(value) { + if (!value) { + return Proxmox.Utils.defaultText; + } + let vga = PVE.Parser.parsePropertyString(value, 'type'); + let text = PVE.Utils.kvm_vga_drivers[vga.type]; + if (!vga.type) { + text = Proxmox.Utils.defaultText; + } + return text ? `${text} (${value})` : value; + }, + + render_kvm_startup: function(value) { + var startup = PVE.Parser.parseStartup(value); + + var res = 'order='; + if (startup.order === undefined) { + res += 'any'; + } else { + res += startup.order; + } + if (startup.up !== undefined) { + res += ',up=' + startup.up; + } + if (startup.down !== undefined) { + res += ',down=' + startup.down; + } + + return res; + }, + + extractFormActionError: function(action) { + var msg; + switch (action.failureType) { + case Ext.form.action.Action.CLIENT_INVALID: + msg = gettext('Form fields may not be submitted with invalid values'); + break; + case Ext.form.action.Action.CONNECT_FAILURE: + msg = gettext('Connection error'); + var resp = action.response; + if (resp.status && resp.statusText) { + msg += " " + resp.status + ": " + resp.statusText; + } + break; + case Ext.form.action.Action.LOAD_FAILURE: + case Ext.form.action.Action.SERVER_INVALID: + msg = Proxmox.Utils.extractRequestError(action.result, true); + break; + } + return msg; + }, + + contentTypes: { + 'images': gettext('Disk image'), + 'backup': gettext('VZDump backup file'), + 'vztmpl': gettext('Container template'), + 'iso': gettext('ISO image'), + 'rootdir': gettext('Container'), + 'snippets': gettext('Snippets'), + }, + + volume_is_qemu_backup: function(volid, format) { + return format === 'pbs-vm' || volid.match(':backup/vzdump-qemu-'); + }, + + volume_is_lxc_backup: function(volid, format) { + return format === 'pbs-ct' || volid.match(':backup/vzdump-(lxc|openvz)-'); + }, + + authSchema: { + ad: { + name: gettext('Active Directory Server'), + ipanel: 'pveAuthADPanel', + syncipanel: 'pveAuthLDAPSyncPanel', + add: true, + tfa: true, + pwchange: true, + }, + ldap: { + name: gettext('LDAP Server'), + ipanel: 'pveAuthLDAPPanel', + syncipanel: 'pveAuthLDAPSyncPanel', + add: true, + tfa: true, + pwchange: true, + }, + openid: { + name: gettext('OpenID Connect Server'), + ipanel: 'pveAuthOpenIDPanel', + add: true, + tfa: false, + pwchange: false, + iconCls: 'pmx-itype-icon-openid-logo', + }, + pam: { + name: 'Linux PAM', + ipanel: 'pveAuthBasePanel', + add: false, + tfa: true, + pwchange: true, + }, + pve: { + name: 'Proxmox VE authentication server', + ipanel: 'pveAuthBasePanel', + add: false, + tfa: true, + pwchange: true, + }, + }, + + storageSchema: { + dir: { + name: Proxmox.Utils.directoryText, + ipanel: 'DirInputPanel', + faIcon: 'folder', + backups: true, + }, + lvm: { + name: 'LVM', + ipanel: 'LVMInputPanel', + faIcon: 'folder', + backups: false, + }, + lvmthin: { + name: 'LVM-Thin', + ipanel: 'LvmThinInputPanel', + faIcon: 'folder', + backups: false, + }, + btrfs: { + name: 'BTRFS', + ipanel: 'BTRFSInputPanel', + faIcon: 'folder', + backups: true, + }, + nfs: { + name: 'NFS', + ipanel: 'NFSInputPanel', + faIcon: 'building', + backups: true, + }, + cifs: { + name: 'SMB/CIFS', + ipanel: 'CIFSInputPanel', + faIcon: 'building', + backups: true, + }, + glusterfs: { + name: 'GlusterFS', + ipanel: 'GlusterFsInputPanel', + faIcon: 'building', + backups: true, + }, + iscsi: { + name: 'iSCSI', + ipanel: 'IScsiInputPanel', + faIcon: 'building', + backups: false, + }, + cephfs: { + name: 'CephFS', + ipanel: 'CephFSInputPanel', + faIcon: 'building', + backups: true, + }, + pvecephfs: { + name: 'CephFS (PVE)', + ipanel: 'CephFSInputPanel', + hideAdd: true, + faIcon: 'building', + backups: true, + }, + rbd: { + name: 'RBD', + ipanel: 'RBDInputPanel', + faIcon: 'building', + backups: false, + }, + pveceph: { + name: 'RBD (PVE)', + ipanel: 'RBDInputPanel', + hideAdd: true, + faIcon: 'building', + backups: false, + }, + zfs: { + name: 'ZFS over iSCSI', + ipanel: 'ZFSInputPanel', + faIcon: 'building', + backups: false, + }, + zfspool: { + name: 'ZFS', + ipanel: 'ZFSPoolInputPanel', + faIcon: 'folder', + backups: false, + }, + pbs: { + name: 'Proxmox Backup Server', + ipanel: 'PBSInputPanel', + faIcon: 'floppy-o', + backups: true, + }, + drbd: { + name: 'DRBD', + hideAdd: true, + backups: false, + }, + }, + + sdnvnetSchema: { + vnet: { + name: 'vnet', + faIcon: 'folder', + }, + }, + + sdnzoneSchema: { + zone: { + name: 'zone', + hideAdd: true, + }, + simple: { + name: 'Simple', + ipanel: 'SimpleInputPanel', + faIcon: 'th', + }, + vlan: { + name: 'VLAN', + ipanel: 'VlanInputPanel', + faIcon: 'th', + }, + qinq: { + name: 'QinQ', + ipanel: 'QinQInputPanel', + faIcon: 'th', + }, + vxlan: { + name: 'VXLAN', + ipanel: 'VxlanInputPanel', + faIcon: 'th', + }, + evpn: { + name: 'EVPN', + ipanel: 'EvpnInputPanel', + faIcon: 'th', + }, + }, + + sdncontrollerSchema: { + controller: { + name: 'controller', + hideAdd: true, + }, + evpn: { + name: 'evpn', + ipanel: 'EvpnInputPanel', + faIcon: 'crosshairs', + }, + bgp: { + name: 'bgp', + ipanel: 'BgpInputPanel', + faIcon: 'crosshairs', + }, + }, + + sdnipamSchema: { + ipam: { + name: 'ipam', + hideAdd: true, + }, + pve: { + name: 'PVE', + ipanel: 'PVEIpamInputPanel', + faIcon: 'th', + hideAdd: true, + }, + netbox: { + name: 'Netbox', + ipanel: 'NetboxInputPanel', + faIcon: 'th', + }, + phpipam: { + name: 'PhpIpam', + ipanel: 'PhpIpamInputPanel', + faIcon: 'th', + }, + }, + + sdndnsSchema: { + dns: { + name: 'dns', + hideAdd: true, + }, + powerdns: { + name: 'powerdns', + ipanel: 'PowerdnsInputPanel', + faIcon: 'th', + }, + }, + + format_sdnvnet_type: function(value, md, record) { + var schema = PVE.Utils.sdnvnetSchema[value]; + if (schema) { + return schema.name; + } + return Proxmox.Utils.unknownText; + }, + + format_sdnzone_type: function(value, md, record) { + var schema = PVE.Utils.sdnzoneSchema[value]; + if (schema) { + return schema.name; + } + return Proxmox.Utils.unknownText; + }, + + format_sdncontroller_type: function(value, md, record) { + var schema = PVE.Utils.sdncontrollerSchema[value]; + if (schema) { + return schema.name; + } + return Proxmox.Utils.unknownText; + }, + + format_sdnipam_type: function(value, md, record) { + var schema = PVE.Utils.sdnipamSchema[value]; + if (schema) { + return schema.name; + } + return Proxmox.Utils.unknownText; + }, + + format_sdndns_type: function(value, md, record) { + var schema = PVE.Utils.sdndnsSchema[value]; + if (schema) { + return schema.name; + } + return Proxmox.Utils.unknownText; + }, + + format_storage_type: function(value, md, record) { + if (value === 'rbd') { + value = !record || record.get('monhost') ? 'rbd' : 'pveceph'; + } else if (value === 'cephfs') { + value = !record || record.get('monhost') ? 'cephfs' : 'pvecephfs'; + } + + let schema = PVE.Utils.storageSchema[value]; + return schema?.name ?? value; + }, + + format_ha: function(value) { + var text = Proxmox.Utils.noneText; + + if (value.managed) { + text = value.state || Proxmox.Utils.noneText; + + text += ', ' + Proxmox.Utils.groupText + ': '; + text += value.group || Proxmox.Utils.noneText; + } + + return text; + }, + + format_content_types: function(value) { + return value.split(',').sort().map(function(ct) { + return PVE.Utils.contentTypes[ct] || ct; + }).join(', '); + }, + + render_storage_content: function(value, metaData, record) { + var data = record.data; + if (Ext.isNumber(data.channel) && + Ext.isNumber(data.id) && + Ext.isNumber(data.lun)) { + return "CH " + + Ext.String.leftPad(data.channel, 2, '0') + + " ID " + data.id + " LUN " + data.lun; + } + return data.volid.replace(/^.*?:(.*?\/)?/, ''); + }, + + render_serverity: function(value) { + return PVE.Utils.log_severity_hash[value] || value; + }, + + calculate_hostcpu: function(data) { + if (!(data.uptime && Ext.isNumeric(data.cpu))) { + return -1; + } + + if (data.type !== 'qemu' && data.type !== 'lxc') { + return -1; + } + + var index = PVE.data.ResourceStore.findExact('id', 'node/' + data.node); + var node = PVE.data.ResourceStore.getAt(index); + if (!Ext.isDefined(node) || node === null) { + return -1; + } + var maxcpu = node.data.maxcpu || 1; + + if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) { + return -1; + } + + return (data.cpu/maxcpu) * data.maxcpu; + }, + + render_hostcpu: function(value, metaData, record, rowIndex, colIndex, store) { + if (!(record.data.uptime && Ext.isNumeric(record.data.cpu))) { + return ''; + } + + if (record.data.type !== 'qemu' && record.data.type !== 'lxc') { + return ''; + } + + var index = PVE.data.ResourceStore.findExact('id', 'node/' + record.data.node); + var node = PVE.data.ResourceStore.getAt(index); + if (!Ext.isDefined(node) || node === null) { + return ''; + } + var maxcpu = node.data.maxcpu || 1; + + if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) { + return ''; + } + + var per = (record.data.cpu/maxcpu) * record.data.maxcpu * 100; + + return per.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU'); + }, + + render_bandwidth: function(value) { + if (!Ext.isNumeric(value)) { + return ''; + } + + return Proxmox.Utils.format_size(value) + '/s'; + }, + + render_timestamp_human_readable: function(value) { + return Ext.Date.format(new Date(value * 1000), 'l d F Y H:i:s'); + }, + + // render a timestamp or pending + render_next_event: function(value) { + if (!value) { + return '-'; + } + let now = new Date(), next = new Date(value * 1000); + if (next < now) { + return gettext('pending'); + } + return Proxmox.Utils.render_timestamp(value); + }, + + calculate_mem_usage: function(data) { + if (!Ext.isNumeric(data.mem) || + data.maxmem === 0 || + data.uptime < 1) { + return -1; + } + + return data.mem / data.maxmem; + }, + + calculate_hostmem_usage: function(data) { + if (data.type !== 'qemu' && data.type !== 'lxc') { + return -1; + } + + var index = PVE.data.ResourceStore.findExact('id', 'node/' + data.node); + var node = PVE.data.ResourceStore.getAt(index); + + if (!Ext.isDefined(node) || node === null) { + return -1; + } + var maxmem = node.data.maxmem || 0; + + if (!Ext.isNumeric(data.mem) || + maxmem === 0 || + data.uptime < 1) { + return -1; + } + + return data.mem / maxmem; + }, + + render_mem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) { + if (!Ext.isNumeric(value) || value === -1) { + return ''; + } + if (value > 1) { + // we got no percentage but bytes + var mem = value; + var maxmem = record.data.maxmem; + if (!record.data.uptime || + maxmem === 0 || + !Ext.isNumeric(mem)) { + return ''; + } + + return (mem*100/maxmem).toFixed(1) + " %"; + } + return (value*100).toFixed(1) + " %"; + }, + + render_hostmem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) { + if (!Ext.isNumeric(record.data.mem) || value === -1) { + return ''; + } + + if (record.data.type !== 'qemu' && record.data.type !== 'lxc') { + return ''; + } + + var index = PVE.data.ResourceStore.findExact('id', 'node/' + record.data.node); + var node = PVE.data.ResourceStore.getAt(index); + var maxmem = node.data.maxmem || 0; + + if (record.data.mem > 1) { + // we got no percentage but bytes + var mem = record.data.mem; + if (!record.data.uptime || + maxmem === 0 || + !Ext.isNumeric(mem)) { + return ''; + } + + return ((mem*100)/maxmem).toFixed(1) + " %"; + } + return (value*100).toFixed(1) + " %"; + }, + + render_mem_usage: function(value, metaData, record, rowIndex, colIndex, store) { + var mem = value; + var maxmem = record.data.maxmem; + + if (!record.data.uptime) { + return ''; + } + + if (!(Ext.isNumeric(mem) && maxmem)) { + return ''; + } + + return Proxmox.Utils.render_size(value); + }, + + calculate_disk_usage: function(data) { + if (!Ext.isNumeric(data.disk) || + ((data.type === 'qemu' || data.type === 'lxc') && data.uptime === 0) || + data.maxdisk === 0 + ) { + return -1; + } + + return data.disk / data.maxdisk; + }, + + render_disk_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) { + if (!Ext.isNumeric(value) || value === -1) { + return ''; + } + + return (value * 100).toFixed(1) + " %"; + }, + + render_disk_usage: function(value, metaData, record, rowIndex, colIndex, store) { + var disk = value; + var maxdisk = record.data.maxdisk; + var type = record.data.type; + + if (!Ext.isNumeric(disk) || + maxdisk === 0 || + ((type === 'qemu' || type === 'lxc') && record.data.uptime === 0) + ) { + return ''; + } + + return Proxmox.Utils.render_size(value); + }, + + get_object_icon_class: function(type, record) { + var status = ''; + var objType = type; + + if (type === 'type') { + // for folder view + objType = record.groupbyid; + } else if (record.template) { + // templates + objType = 'template'; + status = type; + } else { + // everything else + status = record.status + ' ha-' + record.hastate; + } + + if (record.lock) { + status += ' locked lock-' + record.lock; + } + + var defaults = PVE.tree.ResourceTree.typeDefaults[objType]; + if (defaults && defaults.iconCls) { + var retVal = defaults.iconCls + ' ' + status; + return retVal; + } + + return ''; + }, + + render_resource_type: function(value, metaData, record, rowIndex, colIndex, store) { + var cls = PVE.Utils.get_object_icon_class(value, record.data); + + var fa = ' '; + return fa + value; + }, + + render_support_level: function(value, metaData, record) { + return PVE.Utils.support_level_hash[value] || '-'; + }, + + render_upid: function(value, metaData, record) { + var type = record.data.type; + var id = record.data.id; + + return Proxmox.Utils.format_task_description(type, id); + }, + + render_optional_url: function(value) { + if (value && value.match(/^https?:\/\//)) { + return '' + value + ''; + } + return value; + }, + + render_san: function(value) { + var names = []; + if (Ext.isArray(value)) { + value.forEach(function(val) { + if (!Ext.isNumber(val)) { + names.push(val); + } + }); + return names.join('
'); + } + return value; + }, + + render_full_name: function(firstname, metaData, record) { + var first = firstname || ''; + var last = record.data.lastname || ''; + return Ext.htmlEncode(first + " " + last); + }, + + // expecting the following format: + // [v2:10.10.10.1:6802/2008,v1:10.10.10.1:6803/2008] + render_ceph_osd_addr: function(value) { + value = value.trim(); + if (value.startsWith('[') && value.endsWith(']')) { + value = value.slice(1, -1); // remove [] + } + value = value.replaceAll(',', '\n'); // split IPs in lines + let retVal = ''; + for (const i of value.matchAll(/^(v[0-9]):(.*):([0-9]*)\/([0-9]*)$/gm)) { + retVal += `${i[1]}: ${i[2]}:${i[3]}
`; + } + return retVal.length < 1 ? value : retVal; + }, + + windowHostname: function() { + return window.location.hostname.replace(Proxmox.Utils.IP6_bracket_match, + function(m, addr, offset, original) { return addr; }); + }, + + openDefaultConsoleWindow: function(consoles, consoleType, vmid, nodename, vmname, cmd) { + var dv = PVE.Utils.defaultViewer(consoles, consoleType); + PVE.Utils.openConsoleWindow(dv, consoleType, vmid, nodename, vmname, cmd); + }, + + openConsoleWindow: function(viewer, consoleType, vmid, nodename, vmname, cmd) { + if (vmid === undefined && (consoleType === 'kvm' || consoleType === 'lxc')) { + throw "missing vmid"; + } + if (!nodename) { + throw "no nodename specified"; + } + + if (viewer === 'html5') { + PVE.Utils.openVNCViewer(consoleType, vmid, nodename, vmname, cmd); + } else if (viewer === 'xtermjs') { + Proxmox.Utils.openXtermJsViewer(consoleType, vmid, nodename, vmname, cmd); + } else if (viewer === 'vv') { + let url = '/nodes/' + nodename + '/spiceshell'; + let params = { + proxy: PVE.Utils.windowHostname(), + }; + if (consoleType === 'kvm') { + url = '/nodes/' + nodename + '/qemu/' + vmid.toString() + '/spiceproxy'; + } else if (consoleType === 'lxc') { + url = '/nodes/' + nodename + '/lxc/' + vmid.toString() + '/spiceproxy'; + } else if (consoleType === 'upgrade') { + params.cmd = 'upgrade'; + } else if (consoleType === 'cmd') { + params.cmd = cmd; + } else if (consoleType !== 'shell') { + throw `unknown spice viewer type '${consoleType}'`; + } + PVE.Utils.openSpiceViewer(url, params); + } else { + throw `unknown viewer type '${viewer}'`; + } + }, + + defaultViewer: function(consoles, type) { + var allowSpice, allowXtermjs; + + if (consoles === true) { + allowSpice = true; + allowXtermjs = true; + } else if (typeof consoles === 'object') { + allowSpice = consoles.spice; + allowXtermjs = !!consoles.xtermjs; + } + let dv = PVE.UIOptions.options.console || (type === 'kvm' ? 'vv' : 'xtermjs'); + if (dv === 'vv' && !allowSpice) { + dv = allowXtermjs ? 'xtermjs' : 'html5'; + } else if (dv === 'xtermjs' && !allowXtermjs) { + dv = allowSpice ? 'vv' : 'html5'; + } + + return dv; + }, + + openVNCViewer: function(vmtype, vmid, nodename, vmname, cmd) { + let scaling = 'off'; + if (Proxmox.Utils.toolkit !== 'touch') { + var sp = Ext.state.Manager.getProvider(); + scaling = sp.get('novnc-scaling', 'off'); + } + var url = Ext.Object.toQueryString({ + console: vmtype, // kvm, lxc, upgrade or shell + novnc: 1, + vmid: vmid, + vmname: vmname, + node: nodename, + resize: scaling, + cmd: cmd, + }); + var nw = window.open("?" + url, '_blank', "innerWidth=745,innerheight=427"); + if (nw) { + nw.focus(); + } + }, + + openSpiceViewer: function(url, params) { + var downloadWithName = function(uri, name) { + var link = Ext.DomHelper.append(document.body, { + tag: 'a', + href: uri, + css: 'display:none;visibility:hidden;height:0px;', + }); + + // Note: we need to tell Android and Chrome the correct file name extension + // but we do not set 'download' tag for other environments, because + // It can have strange side effects (additional user prompt on firefox) + if (navigator.userAgent.match(/Android|Chrome/i)) { + link.download = name; + } + + if (link.fireEvent) { + link.fireEvent('onclick'); + } else { + let evt = document.createEvent("MouseEvents"); + evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); + link.dispatchEvent(evt); + } + }; + + Proxmox.Utils.API2Request({ + url: url, + params: params, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, opts) { + let cfg = response.result.data; + let raw = Object.entries(cfg).reduce((acc, [k, v]) => acc + `${k}=${v}\n`, "[virt-viewer]\n"); + let spiceDownload = 'data:application/x-virt-viewer;charset=UTF-8,' + encodeURIComponent(raw); + downloadWithName(spiceDownload, "pve-spice.vv"); + }, + }); + }, + + openTreeConsole: function(tree, record, item, index, e) { + e.stopEvent(); + let nodename = record.data.node; + let vmid = record.data.vmid; + let vmname = record.data.name; + if (record.data.type === 'qemu' && !record.data.template) { + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/qemu/${vmid}/status/current`, + failure: response => Ext.Msg.alert('Error', response.htmlStatus), + success: function(response, opts) { + let conf = response.result.data; + let consoles = { + spice: !!conf.spice, + xtermjs: !!conf.serial, + }; + PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname); + }, + }); + } else if (record.data.type === 'lxc' && !record.data.template) { + PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname); + } + }, + + // test automation helper + call_menu_handler: function(menu, text) { + let item = menu.query('menuitem').find(el => el.text === text); + if (item && item.handler) { + item.handler(); + } + }, + + createCmdMenu: function(v, record, item, index, event) { + event.stopEvent(); + if (!(v instanceof Ext.tree.View)) { + v.select(record); + } + let menu; + let type = record.data.type; + + if (record.data.template) { + if (type === 'qemu' || type === 'lxc') { + menu = Ext.create('PVE.menu.TemplateMenu', { + pveSelNode: record, + }); + } + } else if (type === 'qemu' || type === 'lxc' || type === 'node') { + menu = Ext.create('PVE.' + type + '.CmdMenu', { + pveSelNode: record, + nodename: record.data.node, + }); + } else { + return undefined; + } + + menu.showAt(event.getXY()); + return menu; + }, + + // helper for deleting field which are set to there default values + delete_if_default: function(values, fieldname, default_val, create) { + if (values[fieldname] === '' || values[fieldname] === default_val) { + if (!create) { + if (values.delete) { + if (Ext.isArray(values.delete)) { + values.delete.push(fieldname); + } else { + values.delete += ',' + fieldname; + } + } else { + values.delete = fieldname; + } + } + + delete values[fieldname]; + } + }, + + loadSSHKeyFromFile: function(file, callback) { + // ssh-keygen produces ~ 740 bytes for a 4096 bit RSA key, current max is 16 kbit, so assume: + // 740 * 8 for max. 32kbit (5920 bytes), round upwards to 8192 bytes, leaves lots of comment space + PVE.Utils.loadFile(file, callback, 8192); + }, + + loadFile: function(file, callback, maxSize) { + maxSize = maxSize || 32 * 1024; + if (file.size > maxSize) { + Ext.Msg.alert(gettext('Error'), `${gettext("Invalid file size")}: ${file.size} > ${maxSize}`); + return; + } + let reader = new FileReader(); + reader.onload = evt => callback(evt.target.result); + reader.readAsText(file); + }, + + loadTextFromFile: function(file, callback, maxBytes) { + let maxSize = maxBytes || 8192; + if (file.size > maxSize) { + Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size); + return; + } + let reader = new FileReader(); + reader.onload = evt => callback(evt.target.result); + reader.readAsText(file); + }, + + diskControllerMaxIDs: { + ide: 4, + sata: 6, + scsi: 31, + virtio: 16, + unused: 256, + }, + + // types is either undefined (all busses), an array of busses, or a single bus + forEachBus: function(types, func) { + let busses = Object.keys(PVE.Utils.diskControllerMaxIDs); + + if (Ext.isArray(types)) { + busses = types; + } else if (Ext.isDefined(types)) { + busses = [types]; + } + + // check if we only have valid busses + for (let i = 0; i < busses.length; i++) { + if (!PVE.Utils.diskControllerMaxIDs[busses[i]]) { + throw "invalid bus: '" + busses[i] + "'"; + } + } + + for (let i = 0; i < busses.length; i++) { + let count = PVE.Utils.diskControllerMaxIDs[busses[i]]; + for (let j = 0; j < count; j++) { + let cont = func(busses[i], j); + if (!cont && cont !== undefined) { + return; + } + } + } + }, + + mp_counts: { + mp: 256, + unused: 256, + }, + + forEachMP: function(func, includeUnused) { + for (let i = 0; i < PVE.Utils.mp_counts.mp; i++) { + let cont = func('mp', i); + if (!cont && cont !== undefined) { + return; + } + } + + if (!includeUnused) { + return; + } + + for (let i = 0; i < PVE.Utils.mp_counts.unused; i++) { + let cont = func('unused', i); + if (!cont && cont !== undefined) { + return; + } + } + }, + + hardware_counts: { + net: 32, + usb: 14, + usb_old: 5, + hostpci: 16, + audio: 1, + efidisk: 1, + serial: 4, + rng: 1, + tpmstate: 1, + }, + + // we can have usb6 and up only for specific machine/ostypes + get_max_usb_count: function(ostype, machine) { + if (!ostype) { + return PVE.Utils.hardware_counts.usb_old; + } + + let match = /-(\d+).(\d+)/.exec(machine ?? ''); + if (!match || PVE.Utils.qemu_min_version([match[1], match[2]], [7, 1])) { + if (ostype === 'l26') { + return PVE.Utils.hardware_counts.usb; + } + let os_match = /^win(\d+)$/.exec(ostype); + if (os_match && os_match[1] > 7) { + return PVE.Utils.hardware_counts.usb; + } + } + + return PVE.Utils.hardware_counts.usb_old; + }, + + // parameters are expected to be arrays, e.g. [7,1], [4,0,1] + // returns true if toCheck is equal or greater than minVersion + qemu_min_version: function(toCheck, minVersion) { + let i; + for (i = 0; i < toCheck.length && i < minVersion.length; i++) { + if (toCheck[i] < minVersion[i]) { + return false; + } + } + + if (minVersion.length > toCheck.length) { + for (; i < minVersion.length; i++) { + if (minVersion[i] !== 0) { + return false; + } + } + } + + return true; + }, + + cleanEmptyObjectKeys: function(obj) { + for (const propName of Object.keys(obj)) { + if (obj[propName] === null || obj[propName] === undefined) { + delete obj[propName]; + } + } + }, + + acmedomain_count: 5, + + add_domain_to_acme: function(acme, domain) { + if (acme.domains === undefined) { + acme.domains = [domain]; + } else { + acme.domains.push(domain); + acme.domains = acme.domains.filter((value, index, self) => self.indexOf(value) === index); + } + return acme; + }, + + remove_domain_from_acme: function(acme, domain) { + if (acme.domains !== undefined) { + acme.domains = acme + .domains + .filter((value, index, self) => self.indexOf(value) === index && value !== domain); + } + return acme; + }, + + handleStoreErrorOrMask: function(view, store, regex, callback) { + view.mon(store, 'load', function(proxy, response, success, operation) { + if (success) { + Proxmox.Utils.setErrorMask(view, false); + return; + } + let msg; + if (operation.error.statusText) { + if (operation.error.statusText.match(regex)) { + callback(view, operation.error); + return; + } else { + msg = operation.error.statusText + ' (' + operation.error.status + ')'; + } + } else { + msg = gettext('Connection error'); + } + Proxmox.Utils.setErrorMask(view, msg); + }); + }, + + showCephInstallOrMask: function(container, msg, nodename, callback) { + if (msg.match(/not (installed|initialized)/i)) { + if (Proxmox.UserName === 'root@pam') { + container.el.mask(); + if (!container.down('pveCephInstallWindow')) { + var isInstalled = !!msg.match(/not initialized/i); + var win = Ext.create('PVE.ceph.Install', { + nodename: nodename, + }); + win.getViewModel().set('isInstalled', isInstalled); + container.add(win); + win.on('close', () => { + container.el.unmask(); + }); + win.show(); + callback(win); + } + } else { + container.mask(Ext.String.format(gettext('{0} not installed.') + + ' ' + gettext('Log in as root to install.'), 'Ceph'), ['pve-static-mask']); + } + return true; + } else { + return false; + } + }, + + monitor_ceph_installed: function(view, rstore, nodename, maskOwnerCt) { + PVE.Utils.handleStoreErrorOrMask( + view, + rstore, + /not (installed|initialized)/i, + (_, error) => { + nodename = nodename || 'localhost'; + let maskTarget = maskOwnerCt ? view.ownerCt : view; + rstore.stopUpdate(); + PVE.Utils.showCephInstallOrMask(maskTarget, error.statusText, nodename, win => { + view.mon(win, 'cephInstallWindowClosed', () => rstore.startUpdate()); + }); + }, + ); + }, + + + propertyStringSet: function(target, source, name, value) { + if (source) { + if (value === undefined) { + target[name] = source; + } else { + target[name] = value; + } + } else { + delete target[name]; + } + }, + + forEachCorosyncLink: function(nodeinfo, cb) { + let re = /(?:ring|link)(\d+)_addr/; + Ext.iterate(nodeinfo, (prop, val) => { + let match = re.exec(prop); + if (match) { + cb(Number(match[1]), val); + } + }); + }, + + cpu_vendor_map: { + 'default': 'QEMU', + 'AuthenticAMD': 'AMD', + 'GenuineIntel': 'Intel', + }, + + cpu_vendor_order: { + "AMD": 1, + "Intel": 2, + "QEMU": 3, + "Host": 4, + "_default_": 5, // includes custom models + }, + + verify_ip64_address_list: function(value, with_suffix) { + for (let addr of value.split(/[ ,;]+/)) { + if (addr === '') { + continue; + } + + if (with_suffix) { + let parts = addr.split('%'); + addr = parts[0]; + + if (parts.length > 2) { + return false; + } + + if (parts.length > 1 && !addr.startsWith('fe80:')) { + return false; + } + } + + if (!Proxmox.Utils.IP64_match.test(addr)) { + return false; + } + } + + return true; + }, + + sortByPreviousUsage: function(vmconfig, controllerList) { + if (!controllerList) { + controllerList = ['ide', 'virtio', 'scsi', 'sata']; + } + let usedControllers = {}; + for (const type of Object.keys(PVE.Utils.diskControllerMaxIDs)) { + usedControllers[type] = 0; + } + + for (const property of Object.keys(vmconfig)) { + if (property.match(PVE.Utils.bus_match) && !vmconfig[property].match(/media=cdrom/)) { + const foundController = property.match(PVE.Utils.bus_match)[1]; + usedControllers[foundController]++; + } + } + + let sortPriority = PVE.qemu.OSDefaults.getDefaults(vmconfig.ostype).busPriority; + + let sortedList = Ext.clone(controllerList); + sortedList.sort(function(a, b) { + if (usedControllers[b] === usedControllers[a]) { + return sortPriority[b] - sortPriority[a]; + } + return usedControllers[b] - usedControllers[a]; + }); + + return sortedList; + }, + + nextFreeDisk: function(controllers, config) { + for (const controller of controllers) { + for (let i = 0; i < PVE.Utils.diskControllerMaxIDs[controller]; i++) { + let confid = controller + i.toString(); + if (!Ext.isDefined(config[confid])) { + return { + controller, + id: i, + confid, + }; + } + } + } + + return undefined; + }, + + nextFreeMP: function(type, config) { + for (let i = 0; i < PVE.Utils.mp_counts[type]; i++) { + let confid = `${type}${i}`; + if (!Ext.isDefined(config[confid])) { + return { + type, + id: i, + confid, + }; + } + } + + return undefined; + }, + + escapeNotesTemplate: function(value) { + let replace = { + '\\': '\\\\', + '\n': '\\n', + }; + return value.replace(/(\\|[\n])/g, match => replace[match]); + }, + + unEscapeNotesTemplate: function(value) { + let replace = { + '\\\\': '\\', + '\\n': '\n', + }; + return value.replace(/(\\\\|\\n)/g, match => replace[match]); + }, + + notesTemplateVars: ['cluster', 'guestname', 'node', 'vmid'], + + renderTags: function(tagstext, overrides) { + let text = ''; + if (tagstext) { + let tags = (tagstext.split(/[,; ]/) || []).filter(t => !!t); + if (PVE.UIOptions.shouldSortTags()) { + tags = tags.sort((a, b) => { + let alc = a.toLowerCase(); + let blc = b.toLowerCase(); + return alc < blc ? -1 : blc < alc ? 1 : a.localeCompare(b); + }); + } + text += ' '; + tags.forEach((tag) => { + text += Proxmox.Utils.getTagElement(tag, overrides); + }); + } + return text; + }, + + tagCharRegex: /^[a-z0-9+_.-]+$/i, + + verificationStateOrder: { + 'failed': 0, + 'none': 1, + 'ok': 2, + '__default__': 3, + }, +}, + + singleton: true, + constructor: function() { + var me = this; + Ext.apply(me, me.utilities); + + Proxmox.Utils.override_task_descriptions({ + acmedeactivate: ['ACME Account', gettext('Deactivate')], + acmenewcert: ['SRV', gettext('Order Certificate')], + acmerefresh: ['ACME Account', gettext('Refresh')], + acmeregister: ['ACME Account', gettext('Register')], + acmerenew: ['SRV', gettext('Renew Certificate')], + acmerevoke: ['SRV', gettext('Revoke Certificate')], + acmeupdate: ['ACME Account', gettext('Update')], + 'auth-realm-sync': [gettext('Realm'), gettext('Sync')], + 'auth-realm-sync-test': [gettext('Realm'), gettext('Sync Preview')], + cephcreatemds: ['Ceph Metadata Server', gettext('Create')], + cephcreatemgr: ['Ceph Manager', gettext('Create')], + cephcreatemon: ['Ceph Monitor', gettext('Create')], + cephcreateosd: ['Ceph OSD', gettext('Create')], + cephcreatepool: ['Ceph Pool', gettext('Create')], + cephdestroymds: ['Ceph Metadata Server', gettext('Destroy')], + cephdestroymgr: ['Ceph Manager', gettext('Destroy')], + cephdestroymon: ['Ceph Monitor', gettext('Destroy')], + cephdestroyosd: ['Ceph OSD', gettext('Destroy')], + cephdestroypool: ['Ceph Pool', gettext('Destroy')], + cephdestroyfs: ['CephFS', gettext('Destroy')], + cephfscreate: ['CephFS', gettext('Create')], + cephsetpool: ['Ceph Pool', gettext('Edit')], + cephsetflags: ['', gettext('Change global Ceph flags')], + clustercreate: ['', gettext('Create Cluster')], + clusterjoin: ['', gettext('Join Cluster')], + dircreate: [gettext('Directory Storage'), gettext('Create')], + dirremove: [gettext('Directory'), gettext('Remove')], + download: [gettext('File'), gettext('Download')], + hamigrate: ['HA', gettext('Migrate')], + hashutdown: ['HA', gettext('Shutdown')], + hastart: ['HA', gettext('Start')], + hastop: ['HA', gettext('Stop')], + imgcopy: ['', gettext('Copy data')], + imgdel: ['', gettext('Erase data')], + lvmcreate: [gettext('LVM Storage'), gettext('Create')], + lvmremove: ['Volume Group', gettext('Remove')], + lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')], + lvmthinremove: ['Thinpool', gettext('Remove')], + migrateall: ['', gettext('Migrate all VMs and Containers')], + 'move_volume': ['CT', gettext('Move Volume')], + 'pbs-download': ['VM/CT', gettext('File Restore Download')], + pull_file: ['CT', gettext('Pull file')], + push_file: ['CT', gettext('Push file')], + qmclone: ['VM', gettext('Clone')], + qmconfig: ['VM', gettext('Configure')], + qmcreate: ['VM', gettext('Create')], + qmdelsnapshot: ['VM', gettext('Delete Snapshot')], + qmdestroy: ['VM', gettext('Destroy')], + qmigrate: ['VM', gettext('Migrate')], + qmmove: ['VM', gettext('Move disk')], + qmpause: ['VM', gettext('Pause')], + qmreboot: ['VM', gettext('Reboot')], + qmreset: ['VM', gettext('Reset')], + qmrestore: ['VM', gettext('Restore')], + qmresume: ['VM', gettext('Resume')], + qmrollback: ['VM', gettext('Rollback')], + qmshutdown: ['VM', gettext('Shutdown')], + qmsnapshot: ['VM', gettext('Snapshot')], + qmstart: ['VM', gettext('Start')], + qmstop: ['VM', gettext('Stop')], + qmsuspend: ['VM', gettext('Hibernate')], + qmtemplate: ['VM', gettext('Convert to template')], + spiceproxy: ['VM/CT', gettext('Console') + ' (Spice)'], + spiceshell: ['', gettext('Shell') + ' (Spice)'], + startall: ['', gettext('Start all VMs and Containers')], + stopall: ['', gettext('Stop all VMs and Containers')], + unknownimgdel: ['', gettext('Destroy image from unknown guest')], + wipedisk: ['Device', gettext('Wipe Disk')], + vncproxy: ['VM/CT', gettext('Console')], + vncshell: ['', gettext('Shell')], + vzclone: ['CT', gettext('Clone')], + vzcreate: ['CT', gettext('Create')], + vzdelsnapshot: ['CT', gettext('Delete Snapshot')], + vzdestroy: ['CT', gettext('Destroy')], + vzdump: (type, id) => id ? `VM/CT ${id} - ${gettext('Backup')}` : gettext('Backup Job'), + vzmigrate: ['CT', gettext('Migrate')], + vzmount: ['CT', gettext('Mount')], + vzreboot: ['CT', gettext('Reboot')], + vzrestore: ['CT', gettext('Restore')], + vzresume: ['CT', gettext('Resume')], + vzrollback: ['CT', gettext('Rollback')], + vzshutdown: ['CT', gettext('Shutdown')], + vzsnapshot: ['CT', gettext('Snapshot')], + vzstart: ['CT', gettext('Start')], + vzstop: ['CT', gettext('Stop')], + vzsuspend: ['CT', gettext('Suspend')], + vztemplate: ['CT', gettext('Convert to template')], + vzumount: ['CT', gettext('Unmount')], + zfscreate: [gettext('ZFS Storage'), gettext('Create')], + zfsremove: ['ZFS Pool', gettext('Remove')], + }); + }, + +}); +Ext.define('PVE.UIOptions', { + singleton: true, + + options: { + 'allowed-tags': [], + }, + + update: function() { + Proxmox.Utils.API2Request({ + url: '/cluster/options', + method: 'GET', + success: function(response) { + for (const option of ['allowed-tags', 'console', 'tag-style']) { + PVE.UIOptions.options[option] = response?.result?.data?.[option]; + } + + PVE.UIOptions.updateTagList(PVE.UIOptions.options['allowed-tags']); + PVE.UIOptions.updateTagSettings(PVE.UIOptions.options['tag-style']); + PVE.UIOptions.fireUIConfigChanged(); + }, + }); + }, + + tagList: [], + + updateTagList: function(tags) { + PVE.UIOptions.tagList = [...new Set([...tags])].sort(); + }, + + parseTagOverrides: function(overrides) { + let colors = {}; + (overrides || "").split(';').forEach(color => { + if (!color) { + return; + } + let [tag, color_hex, font_hex] = color.split(':'); + let r = parseInt(color_hex.slice(0, 2), 16); + let g = parseInt(color_hex.slice(2, 4), 16); + let b = parseInt(color_hex.slice(4, 6), 16); + colors[tag] = [r, g, b]; + if (font_hex) { + colors[tag].push(parseInt(font_hex.slice(0, 2), 16)); + colors[tag].push(parseInt(font_hex.slice(2, 4), 16)); + colors[tag].push(parseInt(font_hex.slice(4, 6), 16)); + } + }); + return colors; + }, + + tagOverrides: {}, + + updateTagOverrides: function(colors) { + let sp = Ext.state.Manager.getProvider(); + let color_state = sp.get('colors', ''); + let browser_colors = PVE.UIOptions.parseTagOverrides(color_state); + PVE.UIOptions.tagOverrides = Ext.apply({}, browser_colors, colors); + }, + + updateTagSettings: function(style) { + let overrides = style?.['color-map']; + PVE.UIOptions.updateTagOverrides(PVE.UIOptions.parseTagOverrides(overrides ?? "")); + + let shape = style?.shape ?? 'circle'; + if (shape === '__default__') { + style = 'circle'; + } + + Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${shape}`); + }, + + tagTreeStyles: { + '__default__': `${Proxmox.Utils.defaultText} (${gettext('Circle')})`, + 'full': gettext('Full'), + 'circle': gettext('Circle'), + 'dense': gettext('Dense'), + 'none': Proxmox.Utils.NoneText, + }, + + tagOrderOptions: { + '__default__': `${Proxmox.Utils.defaultText} (${gettext('Alphabetical')})`, + 'config': gettext('Configuration'), + 'alphabetical': gettext('Alphabetical'), + }, + + shouldSortTags: function() { + return !(PVE.UIOptions.options['tag-style']?.ordering === 'config'); + }, + + getTreeSortingValue: function(key) { + let localStorage = Ext.state.Manager.getProvider(); + let browserValues = localStorage.get('pve-tree-sorting'); + let defaults = { + 'sort-field': 'vmid', + 'group-templates': true, + 'group-guest-types': true, + }; + + return browserValues?.[key] ?? defaults[key]; + }, + + fireUIConfigChanged: function() { + PVE.data.ResourceStore.refresh(); + Ext.GlobalEvents.fireEvent('loadedUiOptions'); + }, +}); +// ExtJS related things + +Proxmox.Utils.toolkit = 'extjs'; + +// custom PVE specific VTypes +Ext.apply(Ext.form.field.VTypes, { + + QemuStartDate: function(v) { + return (/^(now|\d{4}-\d{1,2}-\d{1,2}(T\d{1,2}:\d{1,2}:\d{1,2})?)$/).test(v); + }, + QemuStartDateText: gettext('Format') + ': "now" or "2006-06-17T16:01:21" or "2006-06-17"', + IP64AddressList: v => PVE.Utils.verify_ip64_address_list(v, false), + IP64AddressWithSuffixList: v => PVE.Utils.verify_ip64_address_list(v, true), + IP64AddressListText: gettext('Example') + ': 192.168.1.1,192.168.1.2', + IP64AddressListMask: /[A-Fa-f0-9,:.; ]/, + PciIdText: gettext('Example') + ': 0x8086', + PciId: v => /^0x[0-9a-fA-F]{4}$/.test(v), +}); + +Ext.define('PVE.form.field.Display', { + override: 'Ext.form.field.Display', + + setSubmitValue: function(value) { + // do nothing, this is only to allow generalized bindings for the: + // `me.isCreate ? 'textfield' : 'displayfield'` cases we have. + }, +}); +Ext.define('PVE.noVncConsole', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNoVncConsole', + + nodename: undefined, + vmid: undefined, + cmd: undefined, + + consoleType: undefined, // lxc, kvm, shell, cmd + xtermjs: false, + + layout: 'fit', + border: false, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.consoleType) { + throw "no console type specified"; + } + + if (!me.vmid && me.consoleType !== 'shell' && me.consoleType !== 'cmd') { + throw "no VM ID specified"; + } + + // always use same iframe, to avoid running several noVnc clients + // at same time (to avoid performance problems) + var box = Ext.create('Ext.ux.IFrame', { itemid: "vncconsole" }); + + var type = me.xtermjs ? 'xtermjs' : 'novnc'; + Ext.apply(me, { + items: box, + listeners: { + activate: function() { + let sp = Ext.state.Manager.getProvider(); + if (Ext.isFunction(me.beforeLoad)) { + me.beforeLoad(); + } + let queryDict = { + console: me.consoleType, // kvm, lxc, upgrade or shell + vmid: me.vmid, + node: me.nodename, + cmd: me.cmd, + 'cmd-opts': me.cmdOpts, + resize: sp.get('novnc-scaling', 'scale'), + }; + queryDict[type] = 1; + PVE.Utils.cleanEmptyObjectKeys(queryDict); + var url = '/?' + Ext.Object.toQueryString(queryDict); + box.load(url); + }, + }, + }); + + me.callParent(); + + me.on('afterrender', function() { + me.focus(); + }); + }, + + reload: function() { + // reload IFrame content to forcibly reconnect VNC/xterm.js to VM + var box = this.down('[itemid=vncconsole]'); + box.getWin().location.reload(); + }, +}); + +Ext.define('PVE.button.ConsoleButton', { + extend: 'Ext.button.Split', + alias: 'widget.pveConsoleButton', + + consoleType: 'shell', // one of 'shell', 'kvm', 'lxc', 'upgrade', 'cmd' + + cmd: undefined, + + consoleName: undefined, + + iconCls: 'fa fa-terminal', + + enableSpice: true, + enableXtermjs: true, + + nodename: undefined, + + vmid: 0, + + text: gettext('Console'), + + setEnableSpice: function(enable) { + var me = this; + + me.enableSpice = enable; + me.down('#spicemenu').setDisabled(!enable); + }, + + setEnableXtermJS: function(enable) { + var me = this; + + me.enableXtermjs = enable; + me.down('#xtermjs').setDisabled(!enable); + }, + + handler: function() { // main, general, handler + let me = this; + PVE.Utils.openDefaultConsoleWindow( + { + spice: me.enableSpice, + xtermjs: me.enableXtermjs, + }, + me.consoleType, + me.vmid, + me.nodename, + me.consoleName, + me.cmd, + ); + }, + + openConsole: function(types) { // used by split-menu buttons + let me = this; + PVE.Utils.openConsoleWindow( + types, + me.consoleType, + me.vmid, + me.nodename, + me.consoleName, + me.cmd, + ); + }, + + menu: [ + { + xtype: 'menuitem', + text: 'noVNC', + iconCls: 'pve-itype-icon-novnc', + type: 'html5', + handler: function(button) { + let view = this.up('button'); + view.openConsole(button.type); + }, + }, + { + xterm: 'menuitem', + itemId: 'spicemenu', + text: 'SPICE', + type: 'vv', + iconCls: 'pve-itype-icon-virt-viewer', + handler: function(button) { + let view = this.up('button'); + view.openConsole(button.type); + }, + }, + { + text: 'xterm.js', + itemId: 'xtermjs', + iconCls: 'pve-itype-icon-xtermjs', + type: 'xtermjs', + handler: function(button) { + let view = this.up('button'); + view.openConsole(button.type); + }, + }, + ], + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.callParent(); + }, +}); +Ext.define('PVE.button.PendingRevert', { + extend: 'Proxmox.button.Button', + alias: 'widget.pvePendingRevertButton', + + text: gettext('Revert'), + disabled: true, + config: { + pendingGrid: null, + apiurl: undefined, + }, + + handler: function() { + if (!this.pendingGrid) { + this.pendingGrid = this.up('proxmoxPendingObjectGrid'); + if (!this.pendingGrid) throw "revert button requires a pendingGrid"; + } + let view = this.pendingGrid; + + let rec = view.getSelectionModel().getSelection()[0]; + if (!rec) return; + + let rowdef = view.rows[rec.data.key] || {}; + let keys = rowdef.multiKey || [rec.data.key]; + + Proxmox.Utils.API2Request({ + url: this.apiurl || view.editorConfig.url, + waitMsgTarget: view, + selModel: view.getSelectionModel(), + method: 'PUT', + params: { + 'revert': keys.join(','), + }, + callback: () => view.reload(), + failure: (response) => Ext.Msg.alert('Error', response.htmlStatus), + }); + }, +}); +/* Button features: + * - observe selection changes to enable/disable the button using enableFn() + * - pop up confirmation dialog using confirmMsg() + * + * does this for the button and every menu item + */ +Ext.define('PVE.button.Split', { + extend: 'Ext.button.Split', + alias: 'widget.pveSplitButton', + + // the selection model to observe + selModel: undefined, + + // if 'false' handler will not be called (button disabled) + enableFn: function(record) { + // do nothing + }, + + // function(record) or text + confirmMsg: false, + + // take special care in confirm box (select no as default). + dangerous: false, + + handlerWrapper: function(button, event) { + var me = this; + var rec, msg; + if (me.selModel) { + rec = me.selModel.getSelection()[0]; + if (!rec || me.enableFn(rec) === false) { + return; + } + } + + if (me.confirmMsg) { + msg = me.confirmMsg; + // confirMsg can be boolean or function + if (Ext.isFunction(me.confirmMsg)) { + msg = me.confirmMsg(rec); + } + Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1; + Ext.Msg.show({ + title: gettext('Confirm'), + icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION, + msg: msg, + buttons: Ext.Msg.YESNO, + callback: function(btn) { + if (btn !== 'yes') { + return; + } + me.realHandler(button, event, rec); + }, + }); + } else { + me.realHandler(button, event, rec); + } + }, + + initComponent: function() { + var me = this; + + if (me.handler) { + me.realHandler = me.handler; + me.handler = me.handlerWrapper; + } + + if (me.menu && me.menu.items) { + me.menu.items.forEach(function(item) { + if (item.handler) { + item.realHandler = item.handler; + item.handler = me.handlerWrapper; + } + + if (item.selModel) { + me.mon(item.selModel, "selectionchange", function() { + var rec = item.selModel.getSelection()[0]; + if (!rec || item.enableFn(rec) === false) { + item.setDisabled(true); + } else { + item.setDisabled(false); + } + }); + } + }); + } + + me.callParent(); + + if (me.selModel) { + me.mon(me.selModel, "selectionchange", function() { + var rec = me.selModel.getSelection()[0]; + if (!rec || me.enableFn(rec) === false) { + me.setDisabled(true); + } else { + me.setDisabled(false); + } + }); + } + }, +}); +Ext.define('PVE.controller.StorageEdit', { + extend: 'Ext.app.ViewController', + alias: 'controller.storageEdit', + control: { + 'field[name=content]': { + change: function(field, value) { + const hasImages = Ext.Array.contains(value, 'images'); + const prealloc = field.up('form').getForm().findField('preallocation'); + if (prealloc) { + prealloc.setDisabled(!hasImages); + } + + var hasBackups = Ext.Array.contains(value, 'backup'); + var maxfiles = this.lookupReference('maxfiles'); + if (!maxfiles) { + return; + } + + if (!hasBackups) { + // clear values which will never be submitted + maxfiles.reset(); + } + maxfiles.setDisabled(!hasBackups); + }, + }, + }, +}); +Ext.define('PVE.data.PermPathStore', { + extend: 'Ext.data.Store', + alias: 'store.pvePermPath', + fields: ['value'], + autoLoad: false, + data: [ + { 'value': '/' }, + { 'value': '/access' }, + { 'value': '/access/groups' }, + { 'value': '/access/realm' }, + { 'value': '/nodes' }, + { 'value': '/pool' }, + { 'value': '/sdn/zones' }, + { 'value': '/storage' }, + { 'value': '/vms' }, + ], + + constructor: function(config) { + var me = this; + + config = config || {}; + + me.callParent([config]); + + let donePaths = {}; + me.suspendEvents(); + PVE.data.ResourceStore.each(function(record) { + let path; + switch (record.get('type')) { + case 'node': path = '/nodes/' + record.get('text'); + break; + case 'qemu': path = '/vms/' + record.get('vmid'); + break; + case 'lxc': path = '/vms/' + record.get('vmid'); + break; + case 'sdn': path = '/sdn/zones/' + record.get('sdn'); + break; + case 'storage': path = '/storage/' + record.get('storage'); + break; + case 'pool': path = '/pool/' + record.get('pool'); + break; + } + if (path !== undefined && !donePaths[path]) { + me.add({ value: path }); + donePaths[path] = 1; + } + }); + me.resumeEvents(); + + me.fireEvent('refresh', me); + me.fireEvent('datachanged', me); + + me.sort({ + property: 'value', + direction: 'ASC', + }); + }, +}); +Ext.define('PVE.data.ResourceStore', { + extend: 'Proxmox.data.UpdateStore', + singleton: true, + + findVMID: function(vmid) { + let me = this; + return me.findExact('vmid', parseInt(vmid, 10)) >= 0; + }, + + // returns the cached data from all nodes + getNodes: function() { + let me = this; + + let nodes = []; + me.each(function(record) { + if (record.get('type') === "node") { + nodes.push(record.getData()); + } + }); + + return nodes; + }, + + storageIsShared: function(storage_path) { + let me = this; + + let index = me.findExact('id', storage_path); + if (index >= 0) { + return me.getAt(index).data.shared; + } else { + return undefined; + } + }, + + guestNode: function(vmid) { + let me = this; + + let index = me.findExact('vmid', parseInt(vmid, 10)); + + return me.getAt(index).data.node; + }, + + guestName: function(vmid) { + let me = this; + let index = me.findExact('vmid', parseInt(vmid, 10)); + if (index < 0) { + return '-'; + } + let rec = me.getAt(index).data; + if ('name' in rec) { + return rec.name; + } + return ''; + }, + + refresh: function() { + let me = this; + // can only refresh if we're loaded at least once and are not currently loading + if (!me.isLoading() && me.isLoaded()) { + let records = (me.getData().getSource() || me.getData()).getRange(); + me.fireEvent('load', me, records); + } + }, + + constructor: function(config) { + let me = this; + + config = config || {}; + + let field_defaults = { + type: { + header: gettext('Type'), + type: 'string', + renderer: PVE.Utils.render_resource_type, + sortable: true, + hideable: false, + width: 100, + }, + id: { + header: 'ID', + type: 'string', + hidden: true, + sortable: true, + width: 80, + }, + running: { + header: gettext('Online'), + type: 'boolean', + renderer: Proxmox.Utils.format_boolean, + hidden: true, + convert: function(value, record) { + var info = record.data; + return Ext.isNumeric(info.uptime) && info.uptime > 0; + }, + }, + text: { + header: gettext('Description'), + type: 'string', + sortable: true, + width: 200, + convert: function(value, record) { + if (value) { + return value; + } + + let info = record.data, text; + if (Ext.isNumeric(info.vmid) && info.vmid > 0) { + text = String(info.vmid); + if (info.name) { + text += " (" + info.name + ')'; + } + } else { // node, pool, storage + text = info[info.type] || info.id; + if (info.node && info.type !== 'node') { + text += " (" + info.node + ")"; + } + } + + return text; + }, + }, + vmid: { + header: 'VMID', + type: 'integer', + hidden: true, + sortable: true, + width: 80, + }, + name: { + header: gettext('Name'), + hidden: true, + sortable: true, + type: 'string', + }, + disk: { + header: gettext('Disk usage'), + type: 'integer', + renderer: PVE.Utils.render_disk_usage, + sortable: true, + width: 100, + hidden: true, + }, + diskuse: { + header: gettext('Disk usage') + " %", + type: 'number', + sortable: true, + renderer: PVE.Utils.render_disk_usage_percent, + width: 100, + calculate: PVE.Utils.calculate_disk_usage, + sortType: 'asFloat', + }, + maxdisk: { + header: gettext('Disk size'), + type: 'integer', + renderer: Proxmox.Utils.render_size, + sortable: true, + hidden: true, + width: 100, + }, + mem: { + header: gettext('Memory usage'), + type: 'integer', + renderer: PVE.Utils.render_mem_usage, + sortable: true, + hidden: true, + width: 100, + }, + memuse: { + header: gettext('Memory usage') + " %", + type: 'number', + renderer: PVE.Utils.render_mem_usage_percent, + calculate: PVE.Utils.calculate_mem_usage, + sortType: 'asFloat', + sortable: true, + width: 100, + }, + maxmem: { + header: gettext('Memory size'), + type: 'integer', + renderer: Proxmox.Utils.render_size, + hidden: true, + sortable: true, + width: 100, + }, + cpu: { + header: gettext('CPU usage'), + type: 'float', + renderer: Proxmox.Utils.render_cpu, + sortable: true, + width: 100, + }, + maxcpu: { + header: gettext('maxcpu'), + type: 'integer', + hidden: true, + sortable: true, + width: 60, + }, + diskread: { + header: gettext('Total Disk Read'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100, + }, + diskwrite: { + header: gettext('Total Disk Write'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100, + }, + netin: { + header: gettext('Total NetIn'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100, + }, + netout: { + header: gettext('Total NetOut'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100, + }, + template: { + header: gettext('Template'), + type: 'integer', + hidden: true, + sortable: true, + width: 60, + }, + uptime: { + header: gettext('Uptime'), + type: 'integer', + renderer: Proxmox.Utils.render_uptime, + sortable: true, + width: 110, + }, + node: { + header: gettext('Node'), + type: 'string', + hidden: true, + sortable: true, + width: 110, + }, + storage: { + header: gettext('Storage'), + type: 'string', + hidden: true, + sortable: true, + width: 110, + }, + pool: { + header: gettext('Pool'), + type: 'string', + hidden: true, + sortable: true, + width: 110, + }, + hastate: { + header: gettext('HA State'), + type: 'string', + defaultValue: 'unmanaged', + hidden: true, + sortable: true, + }, + status: { + header: gettext('Status'), + type: 'string', + hidden: true, + sortable: true, + width: 110, + }, + lock: { + header: gettext('Lock'), + type: 'string', + hidden: true, + sortable: true, + width: 110, + }, + hostcpu: { + header: gettext('Host CPU usage'), + type: 'float', + renderer: PVE.Utils.render_hostcpu, + calculate: PVE.Utils.calculate_hostcpu, + sortType: 'asFloat', + sortable: true, + width: 100, + }, + hostmemuse: { + header: gettext('Host Memory usage') + " %", + type: 'number', + renderer: PVE.Utils.render_hostmem_usage_percent, + calculate: PVE.Utils.calculate_hostmem_usage, + sortType: 'asFloat', + sortable: true, + width: 100, + }, + tags: { + header: gettext('Tags'), + renderer: (value) => PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides), + type: 'string', + sortable: true, + flex: 1, + }, + // note: flex only last column to keep info closer together + }; + + let fields = []; + let fieldNames = []; + Ext.Object.each(field_defaults, function(key, value) { + var field = { name: key, type: value.type }; + if (Ext.isDefined(value.convert)) { + field.convert = value.convert; + } + + if (Ext.isDefined(value.calculate)) { + field.calculate = value.calculate; + } + + if (Ext.isDefined(value.defaultValue)) { + field.defaultValue = value.defaultValue; + } + + fields.push(field); + fieldNames.push(key); + }); + + Ext.define('PVEResources', { + extend: "Ext.data.Model", + fields: fields, + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/resources', + }, + }); + + Ext.define('PVETree', { + extend: "Ext.data.Model", + fields: fields, + proxy: { type: 'memory' }, + }); + + Ext.apply(config, { + storeid: 'PVEResources', + model: 'PVEResources', + defaultColumns: function() { + let res = []; + Ext.Object.each(field_defaults, function(field, info) { + let fieldInfo = Ext.apply({ dataIndex: field }, info); + res.push(fieldInfo); + }); + return res; + }, + fieldNames: fieldNames, + }); + + me.callParent([config]); + }, +}); +Ext.define('pve-rrd-node', { + extend: 'Ext.data.Model', + fields: [ + { + name: 'cpu', + // percentage + convert: function(value) { + return value*100; + }, + }, + { + name: 'iowait', + // percentage + convert: function(value) { + return value*100; + }, + }, + 'loadavg', + 'maxcpu', + 'memtotal', + 'memused', + 'netin', + 'netout', + 'roottotal', + 'rootused', + 'swaptotal', + 'swapused', + { type: 'date', dateFormat: 'timestamp', name: 'time' }, + ], +}); + +Ext.define('pve-rrd-guest', { + extend: 'Ext.data.Model', + fields: [ + { + name: 'cpu', + // percentage + convert: function(value) { + return value*100; + }, + }, + 'maxcpu', + 'netin', + 'netout', + 'mem', + 'maxmem', + 'disk', + 'maxdisk', + 'diskread', + 'diskwrite', + { type: 'date', dateFormat: 'timestamp', name: 'time' }, + ], +}); + +Ext.define('pve-rrd-storage', { + extend: 'Ext.data.Model', + fields: [ + 'used', + 'total', + { type: 'date', dateFormat: 'timestamp', name: 'time' }, + ], +}); +Ext.define('pve-acme-challenges', { + extend: 'Ext.data.Model', + fields: ['id', 'type', 'schema'], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/acme/challenge-schema", + }, + idProperty: 'id', +}); + +Ext.define('PVE.form.ACMEApiSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveACMEApiSelector', + + fieldLabel: gettext('DNS API'), + displayField: 'name', + valueField: 'id', + + store: { + model: 'pve-acme-challenges', + autoLoad: true, + }, + + triggerAction: 'all', + queryMode: 'local', + allowBlank: false, + editable: true, + forceSelection: true, + anyMatch: true, + selectOnFocus: true, + + getSchema: function() { + let me = this; + let val = me.getValue(); + if (val) { + let record = me.getStore().findRecord('id', val, 0, false, true, true); + if (record) { + return record.data.schema; + } + } + return {}; + }, +}); +Ext.define('PVE.form.ACMEAccountSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveACMEAccountSelector', + + displayField: 'name', + valueField: 'name', + + store: { + model: 'pve-acme-accounts', + autoLoad: true, + }, + + triggerAction: 'all', + queryMode: 'local', + allowBlank: false, + editable: false, + forceSelection: true, + + isEmpty: function() { + return this.getStore().getData().length === 0; + }, +}); +Ext.define('PVE.form.ACMEPluginSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveACMEPluginSelector', + + fieldLabel: gettext('Plugin'), + displayField: 'plugin', + valueField: 'plugin', + + store: { + model: 'pve-acme-plugins', + autoLoad: true, + filters: item => item.data.type === 'dns', + }, + + triggerAction: 'all', + queryMode: 'local', + allowBlank: false, + editable: false, +}); +Ext.define('PVE.form.AgentFeatureSelector', { + extend: 'Proxmox.panel.InputPanel', + alias: ['widget.pveAgentFeatureSelector'], + + viewModel: {}, + + items: [ + { + xtype: 'proxmoxcheckbox', + boxLabel: Ext.String.format(gettext('Use {0}'), 'QEMU Guest Agent'), + name: 'enabled', + reference: 'enabled', + uncheckedValue: 0, + }, + { + xtype: 'proxmoxcheckbox', + boxLabel: gettext('Run guest-trim after a disk move or VM migration'), + name: 'fstrim_cloned_disks', + bind: { + disabled: '{!enabled.checked}', + }, + disabled: true, + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext('Make sure the QEMU Guest Agent is installed in the VM'), + bind: { + hidden: '{!enabled.checked}', + }, + }, + ], + + advancedItems: [ + { + xtype: 'proxmoxKVComboBox', + name: 'type', + value: '__default__', + deleteEmpty: false, + fieldLabel: 'Type', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + " (VirtIO)"], + ['virtio', 'VirtIO'], + ['isa', 'ISA'], + ], + }, + ], + + onGetValues: function(values) { + var agentstr = PVE.Parser.printPropertyString(values, 'enabled'); + return { agent: agentstr }; + }, + + setValues: function(values) { + let res = PVE.Parser.parsePropertyString(values.agent, 'enabled'); + this.callParent([res]); + }, +}); +Ext.define('PVE.form.BackupModeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveBackupModeSelector'], + comboItems: [ + ['snapshot', gettext('Snapshot')], + ['suspend', gettext('Suspend')], + ['stop', gettext('Stop')], + ], +}); +Ext.define('PVE.form.SizeField', { + extend: 'Ext.form.FieldContainer', + alias: 'widget.pveSizeField', + + mixins: ['Proxmox.Mixin.CBind'], + + viewModel: { + data: { + unit: 'MiB', + unitPostfix: '', + }, + formulas: { + unitlabel: (get) => get('unit') + get('unitPostfix'), + }, + }, + + emptyText: '', + + layout: 'hbox', + defaults: { + hideLabel: true, + }, + + units: { + 'B': 1, + 'KiB': 1024, + 'MiB': 1024*1024, + 'GiB': 1024*1024*1024, + 'TiB': 1024*1024*1024*1024, + 'KB': 1000, + 'MB': 1000*1000, + 'GB': 1000*1000*1000, + 'TB': 1000*1000*1000*1000, + }, + + // display unit (TODO: make (optionally) selectable) + unit: 'MiB', + unitPostfix: '', + + // use this if the backend saves values in another unit tha bytes, e.g., + // for KiB set it to 'KiB' + backendUnit: undefined, + + // allow setting 0 and using it as a submit value + allowZero: false, + + emptyValue: null, + + items: [ + { + xtype: 'numberfield', + cbind: { + name: '{name}', + emptyText: '{emptyText}', + allowZero: '{allowZero}', + emptyValue: '{emptyValue}', + }, + minValue: 0, + step: 1, + submitLocaleSeparator: false, + fieldStyle: 'text-align: right', + flex: 1, + enableKeyEvents: true, + setValue: function(v) { + if (!this._transformed) { + let fieldContainer = this.up('fieldcontainer'); + let vm = fieldContainer.getViewModel(); + let unit = vm.get('unit'); + + v /= fieldContainer.units[unit]; + v *= fieldContainer.backendFactor; + + this._transformed = true; + } + + if (Number(v) === 0 && !this.allowZero) { + v = undefined; + } + + return Ext.form.field.Text.prototype.setValue.call(this, v); + }, + getSubmitValue: function() { + let v = this.processRawValue(this.getRawValue()); + v = v.replace(this.decimalSeparator, '.'); + + if (v === undefined || v === '') { + return this.emptyValue; + } + + if (Number(v) === 0) { + return this.allowZero ? 0 : null; + } + + let fieldContainer = this.up('fieldcontainer'); + let vm = fieldContainer.getViewModel(); + let unit = vm.get('unit'); + + v = parseFloat(v) * fieldContainer.units[unit]; + v /= fieldContainer.backendFactor; + + return String(Math.floor(v)); + }, + listeners: { + // our setValue gets only called if we have a value, avoid + // transformation of the first user-entered value + keydown: function() { this._transformed = true; }, + }, + }, + { + xtype: 'displayfield', + name: 'unit', + submitValue: false, + padding: '0 0 0 10', + bind: { + value: '{unitlabel}', + }, + listeners: { + change: (f, v) => { + f.originalValue = v; + }, + }, + width: 40, + }, + ], + + initComponent: function() { + let me = this; + + me.unit = me.unit || 'MiB'; + if (!(me.unit in me.units)) { + throw "unknown unit: " + me.unit; + } + + me.backendFactor = 1; + if (me.backendUnit !== undefined) { + if (!(me.unit in me.units)) { + throw "unknown backend unit: " + me.backendUnit; + } + me.backendFactor = me.units[me.backendUnit]; + } + + me.callParent(arguments); + + me.getViewModel().set('unit', me.unit); + me.getViewModel().set('unitPostfix', me.unitPostfix); + }, +}); + +Ext.define('PVE.form.BandwidthField', { + extend: 'PVE.form.SizeField', + alias: 'widget.pveBandwidthField', + + unitPostfix: '/s', +}); +Ext.define('PVE.form.BridgeSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.PVE.form.BridgeSelector'], + + bridgeType: 'any_bridge', // bridge, OVSBridge or any_bridge + + store: { + fields: ['iface', 'active', 'type'], + filterOnLoad: true, + sorters: [ + { + property: 'iface', + direction: 'ASC', + }, + ], + }, + valueField: 'iface', + displayField: 'iface', + listConfig: { + columns: [ + { + header: gettext('Bridge'), + dataIndex: 'iface', + hideable: false, + width: 100, + }, + { + header: gettext('Active'), + width: 60, + dataIndex: 'active', + renderer: Proxmox.Utils.format_boolean, + }, + { + header: gettext('Comment'), + dataIndex: 'comments', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + }, + + setNodename: function(nodename) { + var me = this; + + if (!nodename || me.nodename === nodename) { + return; + } + + me.nodename = nodename; + + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/network?type=' + + me.bridgeType, + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + me.nodename = undefined; + + me.callParent(); + + me.setNodename(nodename); + }, +}); + +Ext.define('PVE.form.BusTypeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: 'widget.pveBusSelector', + + withVirtIO: true, + withUnused: false, + + initComponent: function() { + var me = this; + + me.comboItems = [['ide', 'IDE'], ['sata', 'SATA']]; + + if (me.withVirtIO) { + me.comboItems.push(['virtio', 'VirtIO Block']); + } + + me.comboItems.push(['scsi', 'SCSI']); + + if (me.withUnused) { + me.comboItems.push(['unused', 'Unused']); + } + + me.callParent(); + }, +}); +Ext.define('PVE.data.CPUModel', { + extend: 'Ext.data.Model', + fields: [ + { name: 'name' }, + { name: 'vendor' }, + { name: 'custom' }, + { name: 'displayname' }, + ], +}); + +Ext.define('PVE.form.CPUModelSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.CPUModelSelector'], + + valueField: 'name', + displayField: 'displayname', + + emptyText: Proxmox.Utils.defaultText + ' (kvm64)', + allowBlank: true, + + editable: true, + anyMatch: true, + forceSelection: true, + autoSelect: false, + + deleteEmpty: true, + + listConfig: { + columns: [ + { + header: gettext('Model'), + dataIndex: 'displayname', + hideable: false, + sortable: true, + flex: 3, + }, + { + header: gettext('Vendor'), + dataIndex: 'vendor', + hideable: false, + sortable: true, + flex: 2, + }, + ], + width: 360, + }, + + store: { + autoLoad: true, + model: 'PVE.data.CPUModel', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/localhost/capabilities/qemu/cpu', + }, + sorters: [ + { + sorterFn: function(recordA, recordB) { + let a = recordA.data; + let b = recordB.data; + + let vendorOrder = PVE.Utils.cpu_vendor_order; + let orderA = vendorOrder[a.vendor] || vendorOrder._default_; + let orderB = vendorOrder[b.vendor] || vendorOrder._default_; + + if (orderA > orderB) { + return 1; + } else if (orderA < orderB) { + return -1; + } + + // Within same vendor, sort alphabetically + return a.name.localeCompare(b.name); + }, + direction: 'ASC', + }, + ], + listeners: { + load: function(store, records, success) { + if (success) { + records.forEach(rec => { + rec.data.displayname = rec.data.name.replace(/^custom-/, ''); + + let vendor = rec.data.vendor; + + if (rec.data.name === 'host') { + vendor = 'Host'; + } + + // We receive vendor names as given to QEMU as CPUID + vendor = PVE.Utils.cpu_vendor_map[vendor] || vendor; + + if (rec.data.custom) { + vendor = gettext('Custom') + ` (${vendor})`; + } + + rec.data.vendor = vendor; + }); + + store.sort(); + } + }, + }, + }, +}); +Ext.define('PVE.form.CacheTypeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.CacheTypeSelector'], + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + " (" + gettext('No cache') + ")"], + ['directsync', 'Direct sync'], + ['writethrough', 'Write through'], + ['writeback', 'Write back'], + ['unsafe', 'Write back (' + gettext('unsafe') + ')'], + ['none', gettext('No cache')], + ], +}); +Ext.define('PVE.form.CalendarEvent', { + extend: 'Ext.form.field.ComboBox', + xtype: 'pveCalendarEvent', + + editable: true, + emptyText: gettext('Editable'), // FIXME: better way to convey that to confused users? + + valueField: 'value', + queryMode: 'local', + + matchFieldWidth: false, + listConfig: { + maxWidth: 450, + }, + + store: { + field: ['value', 'text'], + data: [ + { value: '*/30', text: Ext.String.format(gettext("Every {0} minutes"), 30) }, + { value: '*/2:00', text: gettext("Every two hours") }, + { value: '21:00', text: gettext("Every day") + " 21:00" }, + { value: '2,22:30', text: gettext("Every day") + " 02:30, 22:30" }, + { value: 'mon..fri 00:00', text: gettext("Monday to Friday") + " 00:00" }, + { value: 'mon..fri */1:00', text: gettext("Monday to Friday") + ': ' + gettext("hourly") }, + { + value: 'mon..fri 7..18:00/15', + text: gettext("Monday to Friday") + ', ' + + Ext.String.format(gettext('{0} to {1}'), '07:00', '18:45') + ': ' + + Ext.String.format(gettext("Every {0} minutes"), 15), + }, + { value: 'sun 01:00', text: gettext("Sunday") + " 01:00" }, + { value: 'monthly', text: gettext("Every first day of the Month") + " 00:00" }, + { value: 'sat *-1..7 15:00', text: gettext("First Saturday each month") + " 15:00" }, + { value: 'yearly', text: gettext("First day of the year") + " 00:00" }, + ], + }, + + tpl: [ + '
    ', + '
  • {text}
  • ', + '
', + ], + + displayTpl: [ + '', + '{value}', + '', + ], + +}); +Ext.define('PVE.form.CephPoolSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCephPoolSelector', + + allowBlank: false, + valueField: 'pool_name', + displayField: 'pool_name', + editable: false, + queryMode: 'local', + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + let onlyRBDPools = ({ data }) => + !data?.application_metadata || !!data?.application_metadata?.rbd; + + var store = Ext.create('Ext.data.Store', { + fields: ['name'], + sorters: 'name', + filters: [ + onlyRBDPools, + ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/ceph/pool', + }, + }); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + + store.load({ + callback: function(rec, op, success) { + let filteredRec = rec.filter(onlyRBDPools); + + if (success && filteredRec.length > 0) { + me.select(filteredRec[0]); + } + }, + }); + }, + +}); +Ext.define('PVE.form.CephFSSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCephFSSelector', + + allowBlank: false, + valueField: 'name', + displayField: 'name', + editable: false, + queryMode: 'local', + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + var store = Ext.create('Ext.data.Store', { + fields: ['name'], + sorters: 'name', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/ceph/fs', + }, + }); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + + store.load({ + callback: function(rec, op, success) { + if (success && rec.length > 0) { + me.select(rec[0]); + } + }, + }); + }, + +}); +Ext.define('PVE.form.ComboBoxSetStoreNode', { + extend: 'Proxmox.form.ComboGrid', + config: { + apiBaseUrl: '/api2/json/nodes/', + apiSuffix: '', + }, + + showNodeSelector: false, + + setNodeName: function(value) { + let me = this; + value ||= Proxmox.NodeName; + + me.getStore().getProxy().setUrl(`${me.apiBaseUrl}${value}${me.apiSuffix}`); + me.clearValue(); + }, + + nodeChange: function(_field, value) { + let me = this; + // disable autoSelect if there is already a selection or we have the picker open + if (me.getValue() || me.isExpanded) { + let autoSelect = me.autoSelect; + me.autoSelect = false; + me.store.on('afterload', function() { + me.autoSelect = autoSelect; + }, { single: true }); + } + me.setNodeName(value); + me.fireEvent('nodechanged', value); + }, + + tbarMouseDown: function() { + this.topBarMousePress = true; + }, + + tbarMouseUp: function() { + let me = this; + delete this.topBarMousePress; + if (me.focusLeft) { + me.focus(); + delete me.focusLeft; + } + }, + + // conditionally prevent the focusLeave handler to continue, preventing collapsing of the picker + onFocusLeave: function() { + let me = this; + me.focusLeft = true; + if (!me.topBarMousePress) { + me.callParent(arguments); + } + + return undefined; + }, + + initComponent: function() { + let me = this; + + if (me.showNodeSelector && PVE.data.ResourceStore.getNodes().length > 1) { + me.errorHeight = 140; + Ext.apply(me.listConfig ?? {}, { + tbar: { + xtype: 'toolbar', + minHeight: 40, + listeners: { + mousedown: me.tbarMouseDown, + mouseup: me.tbarMouseUp, + element: 'el', + scope: me, + }, + items: [ + { + xtype: "pveStorageScanNodeSelector", + autoSelect: false, + fieldLabel: gettext('Node to scan'), + listeners: { + change: (field, value) => me.nodeChange(field, value), + }, + }, + ], + }, + emptyText: me.listConfig?.emptyText ?? gettext('Nothing found'), + }); + } + + me.callParent(); + }, +}); +Ext.define('PVE.form.CompressionSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveCompressionSelector'], + comboItems: [ + ['0', Proxmox.Utils.noneText], + ['lzo', 'LZO (' + gettext('fast') + ')'], + ['gzip', 'GZIP (' + gettext('good') + ')'], + ['zstd', 'ZSTD (' + gettext('fast and good') + ')'], + ], +}); +Ext.define('PVE.form.ContentTypeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveContentTypeSelector'], + + cts: undefined, + + initComponent: function() { + var me = this; + + me.comboItems = []; + + if (me.cts === undefined) { + me.cts = ['images', 'iso', 'vztmpl', 'backup', 'rootdir', 'snippets']; + } + + Ext.Array.each(me.cts, function(ct) { + me.comboItems.push([ct, PVE.Utils.format_content_types(ct)]); + }); + + me.callParent(); + }, +}); +Ext.define('PVE.form.ControllerSelector', { + extend: 'Ext.form.FieldContainer', + alias: 'widget.pveControllerSelector', + + withVirtIO: true, + withUnused: false, + + vmconfig: {}, // used to check for existing devices + + setToFree: function(controllers, busField, deviceIDField) { + let me = this; + let freeId = PVE.Utils.nextFreeDisk(controllers, me.vmconfig); + + if (freeId !== undefined) { + busField?.setValue(freeId.controller); + deviceIDField.setValue(freeId.id); + } + }, + + updateVMConfig: function(vmconfig) { + let me = this; + me.vmconfig = Ext.apply({}, vmconfig); + + me.down('field[name=deviceid]').validate(); + }, + + setVMConfig: function(vmconfig, autoSelect) { + let me = this; + + me.vmconfig = Ext.apply({}, vmconfig); + + let bussel = me.down('field[name=controller]'); + let deviceid = me.down('field[name=deviceid]'); + + let clist; + if (autoSelect === 'cdrom') { + if (!Ext.isDefined(me.vmconfig.ide2)) { + bussel.setValue('ide'); + deviceid.setValue(2); + return; + } + clist = ['ide', 'scsi', 'sata']; + } else { + // in most cases we want to add a disk to the same controller we previously used + clist = PVE.Utils.sortByPreviousUsage(me.vmconfig); + } + + me.setToFree(clist, bussel, deviceid); + + deviceid.validate(); + }, + + getConfId: function() { + let me = this; + let controller = me.getComponent('controller').getValue() || 'ide'; + let id = me.getComponent('deviceid').getValue() || 0; + + return `${controller}${id}`; + }, + + initComponent: function() { + let me = this; + + Ext.apply(me, { + fieldLabel: gettext('Bus/Device'), + layout: 'hbox', + defaults: { + hideLabel: true, + }, + items: [ + { + xtype: 'pveBusSelector', + name: 'controller', + itemId: 'controller', + value: PVE.qemu.OSDefaults.generic.busType, + withVirtIO: me.withVirtIO, + withUnused: me.withUnused, + allowBlank: false, + flex: 2, + listeners: { + change: function(t, value) { + if (!value) { + return; + } + let field = me.down('field[name=deviceid]'); + me.setToFree([value], undefined, field); + field.setMaxValue(PVE.Utils.diskControllerMaxIDs[value] - 1); + field.validate(); + }, + }, + }, + { + xtype: 'proxmoxintegerfield', + name: 'deviceid', + itemId: 'deviceid', + minValue: 0, + maxValue: PVE.Utils.diskControllerMaxIDs.ide - 1, + value: '0', + flex: 1, + allowBlank: false, + validator: function(value) { + if (!me.rendered) { + return undefined; + } + let controller = me.down('field[name=controller]').getValue(); + let confid = controller + value; + if (Ext.isDefined(me.vmconfig[confid])) { + return "This device is already in use."; + } + return true; + }, + }, + ], + }); + + me.callParent(); + + if (me.selectFree) { + me.setVMConfig(me.vmconfig); + } + }, +}); +Ext.define('PVE.form.DayOfWeekSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveDayOfWeekSelector'], + comboItems: [], + initComponent: function() { + var me = this; + me.comboItems = [ + ['mon', Ext.util.Format.htmlDecode(Ext.Date.dayNames[1])], + ['tue', Ext.util.Format.htmlDecode(Ext.Date.dayNames[2])], + ['wed', Ext.util.Format.htmlDecode(Ext.Date.dayNames[3])], + ['thu', Ext.util.Format.htmlDecode(Ext.Date.dayNames[4])], + ['fri', Ext.util.Format.htmlDecode(Ext.Date.dayNames[5])], + ['sat', Ext.util.Format.htmlDecode(Ext.Date.dayNames[6])], + ['sun', Ext.util.Format.htmlDecode(Ext.Date.dayNames[0])], + ]; + this.callParent(); + }, +}); +Ext.define('PVE.form.DiskFormatSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: 'widget.pveDiskFormatSelector', + comboItems: [ + ['raw', gettext('Raw disk image') + ' (raw)'], + ['qcow2', gettext('QEMU image format') + ' (qcow2)'], + ['vmdk', gettext('VMware image format') + ' (vmdk)'], + ], +}); +Ext.define('PVE.form.DiskStorageSelector', { + extend: 'Ext.container.Container', + alias: 'widget.pveDiskStorageSelector', + + layout: 'fit', + defaults: { + margin: '0 0 5 0', + }, + + // the fieldLabel for the storageselector + storageLabel: gettext('Storage'), + + // the content to show (e.g., images or rootdir) + storageContent: undefined, + + // if true, selects the first available storage + autoSelect: false, + + allowBlank: false, + emptyText: '', + + // hides the selection field + // this is always hidden on creation, + // and only shown when the storage needs a selection and + // hideSelection is not true + hideSelection: undefined, + + // hides the size field (e.g, for the efi disk dialog) + hideSize: false, + + // hides the format field (e.g. for TPM state) + hideFormat: false, + + // sets the initial size value + // string because else we get a type confusion + defaultSize: '32', + + changeStorage: function(f, value) { + var me = this; + var formatsel = me.getComponent('diskformat'); + var hdfilesel = me.getComponent('hdimage'); + var hdsizesel = me.getComponent('disksize'); + + // initial store load, and reset/deletion of the storage + if (!value) { + hdfilesel.setDisabled(true); + hdfilesel.setVisible(false); + + formatsel.setDisabled(true); + return; + } + + var rec = f.store.getById(value); + // if the storage is not defined, or valid, + // we cannot know what to enable/disable + if (!rec) { + return; + } + + let validFormats = {}; + let selectFormat = 'raw'; + if (rec.data.format) { + validFormats = rec.data.format[0]; // 0 is the formats, 1 the default in the backend + delete validFormats.subvol; // we never need subvol in the gui + if (validFormats.qcow2) { + selectFormat = 'qcow2'; + } else if (validFormats.raw) { + selectFormat = 'raw'; + } else { + selectFormat = rec.data.format[1]; + } + } + + var select = !!rec.data.select_existing && !me.hideSelection; + + formatsel.setDisabled(me.hideFormat || Ext.Object.getSize(validFormats) <= 1); + formatsel.setValue(selectFormat); + + hdfilesel.setDisabled(!select); + hdfilesel.setVisible(select); + if (select) { + hdfilesel.setStorage(value); + } + + hdsizesel.setDisabled(select || me.hideSize); + hdsizesel.setVisible(!select && !me.hideSize); + }, + + setNodename: function(nodename) { + var me = this; + var hdstorage = me.getComponent('hdstorage'); + var hdfilesel = me.getComponent('hdimage'); + + hdstorage.setNodename(nodename); + hdfilesel.setNodename(nodename); + }, + + setDisabled: function(value) { + var me = this; + var hdstorage = me.getComponent('hdstorage'); + + // reset on disable + if (value) { + hdstorage.setValue(); + } + hdstorage.setDisabled(value); + + // disabling does not always fire this event and we do not need + // the value of the validity + hdstorage.fireEvent('validitychange'); + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: 'pveStorageSelector', + itemId: 'hdstorage', + name: 'hdstorage', + reference: 'hdstorage', + fieldLabel: me.storageLabel, + nodename: me.nodename, + storageContent: me.storageContent, + disabled: me.disabled, + autoSelect: me.autoSelect, + allowBlank: me.allowBlank, + emptyText: me.emptyText, + listeners: { + change: { + fn: me.changeStorage, + scope: me, + }, + }, + }, + { + xtype: 'pveFileSelector', + name: 'hdimage', + reference: 'hdimage', + itemId: 'hdimage', + fieldLabel: gettext('Disk image'), + nodename: me.nodename, + disabled: true, + hidden: true, + }, + { + xtype: 'numberfield', + itemId: 'disksize', + reference: 'disksize', + name: 'disksize', + fieldLabel: gettext('Disk size') + ' (GiB)', + hidden: me.hideSize, + disabled: me.hideSize, + minValue: 0.001, + maxValue: 128*1024, + decimalPrecision: 3, + value: me.defaultSize, + allowBlank: false, + }, + { + xtype: 'pveDiskFormatSelector', + itemId: 'diskformat', + reference: 'diskformat', + name: 'diskformat', + fieldLabel: gettext('Format'), + nodename: me.nodename, + disabled: true, + hidden: me.hideFormat || me.storageContent === 'rootdir', + value: 'qcow2', + allowBlank: false, + }, + ]; + + // use it to disable the children but not ourself + me.disabled = false; + + me.callParent(); + }, +}); +Ext.define('PVE.form.EmailNotificationSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveEmailNotificationSelector'], + comboItems: [ + ['always', gettext('Notify always')], + ['failure', gettext('On failure only')], + ], +}); +Ext.define('PVE.form.FileSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveFileSelector', + + editable: true, + anyMatch: true, + forceSelection: true, + + listeners: { + afterrender: function() { + var me = this; + if (!me.disabled) { + me.setStorage(me.storage, me.nodename); + } + }, + }, + + setStorage: function(storage, nodename) { + var me = this; + + var change = false; + if (storage && me.storage !== storage) { + me.storage = storage; + change = true; + } + + if (nodename && me.nodename !== nodename) { + me.nodename = nodename; + change = true; + } + + if (!(me.storage && me.nodename && change)) { + return; + } + + var url = '/api2/json/nodes/' + me.nodename + '/storage/' + me.storage + '/content'; + if (me.storageContent) { + url += '?content=' + me.storageContent; + } + + me.store.setProxy({ + type: 'proxmox', + url: url, + }); + + me.store.removeAll(); + me.store.load(); + }, + + setNodename: function(nodename) { + this.setStorage(undefined, nodename); + }, + + store: { + model: 'pve-storage-content', + }, + + allowBlank: false, + autoSelect: false, + valueField: 'volid', + displayField: 'text', + + listConfig: { + width: 600, + columns: [ + { + header: gettext('Name'), + dataIndex: 'text', + hideable: false, + flex: 1, + }, + { + header: gettext('Format'), + width: 60, + dataIndex: 'format', + }, + { + header: gettext('Size'), + width: 100, + dataIndex: 'size', + renderer: Proxmox.Utils.format_size, + }, + ], + }, +}); +Ext.define('PVE.form.FirewallPolicySelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveFirewallPolicySelector'], + comboItems: [ + ['ACCEPT', 'ACCEPT'], + ['REJECT', 'REJECT'], + ['DROP', 'DROP'], + ], +}); +/* + * This is a global search field it loads the /cluster/resources on focus and displays the + * result in a floating grid. Filtering and sorting is done in the customFilter function + * + * Accepts key up/down and enter for input, and it opens to CTRL+SHIFT+F and CTRL+SPACE + */ +Ext.define('PVE.form.GlobalSearchField', { + extend: 'Ext.form.field.Text', + alias: 'widget.pveGlobalSearchField', + + emptyText: gettext('Search'), + enableKeyEvents: true, + selectOnFocus: true, + padding: '0 5 0 5', + + grid: { + xtype: 'gridpanel', + userCls: 'proxmox-tags-full', + focusOnToFront: false, + floating: true, + emptyText: Proxmox.Utils.noneText, + width: 600, + height: 400, + scrollable: { + xtype: 'scroller', + y: true, + x: true, + }, + store: { + model: 'PVEResources', + proxy: { + type: 'proxmox', + url: '/api2/extjs/cluster/resources', + }, + }, + plugins: { + ptype: 'bufferedrenderer', + trailingBufferZone: 20, + leadingBufferZone: 20, + }, + + hideMe: function() { + var me = this; + if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) { + return; + } + me.hasFocus = false; + if (!me.textfield.hasFocus) { + me.hide(); + } + }, + + setFocus: function() { + var me = this; + me.hasFocus = true; + }, + + listeners: { + rowclick: function(grid, record) { + var me = this; + me.textfield.selectAndHide(record.id); + }, + itemcontextmenu: function(v, record, item, index, event) { + var me = this; + me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event); + }, + focusleave: 'hideMe', + focusenter: 'setFocus', + }, + + columns: [ + { + text: gettext('Type'), + dataIndex: 'type', + width: 100, + renderer: PVE.Utils.render_resource_type, + }, + { + text: gettext('Description'), + flex: 1, + dataIndex: 'text', + renderer: function(value, mD, rec) { + let overrides = PVE.UIOptions.tagOverrides; + let tags = PVE.Utils.renderTags(rec.data.tags, overrides); + return `${value}${tags}`; + }, + }, + { + text: gettext('Node'), + dataIndex: 'node', + }, + { + text: gettext('Pool'), + dataIndex: 'pool', + }, + ], + }, + + customFilter: function(item) { + let me = this; + + if (me.filterVal === '') { + item.data.relevance = 0; + return true; + } + // different types have different fields to search, e.g., a node will never have a pool + const fieldMap = { + 'pool': ['type', 'pool', 'text'], + 'node': ['type', 'node', 'text'], + 'storage': ['type', 'pool', 'node', 'storage'], + 'default': ['name', 'type', 'node', 'pool', 'vmid'], + }; + let fields = fieldMap[item.data.type] || fieldMap.default; + let fieldArr = fields.map(field => item.data[field]?.toString().toLowerCase()); + if (item.data.tags) { + let tags = item.data.tags.split(/[;, ]/); + fieldArr.push(...tags); + } + + let filterWords = me.filterVal.split(/\s+/); + + // all text is case insensitive and each split-out word is searched for separately. + // a row gets 1 point for every partial match, and and additional point for every exact match + let match = 0; + for (let fieldValue of fieldArr) { + if (fieldValue === undefined || fieldValue === "") { + continue; + } + for (let filterWord of filterWords) { + if (fieldValue.indexOf(filterWord) !== -1) { + match++; // partial match + if (fieldValue === filterWord) { + match++; // exact match is worth more + } + } + } + } + item.data.relevance = match; // set the row's virtual 'relevance' value for ordering + return match > 0; + }, + + updateFilter: function(field, newValue, oldValue) { + let me = this; + // parse input and filter store, show grid + me.grid.store.filterVal = newValue.toLowerCase().trim(); + me.grid.store.clearFilter(true); + me.grid.store.filterBy(me.customFilter); + me.grid.getSelectionModel().select(0); + }, + + selectAndHide: function(id) { + var me = this; + me.tree.selectById(id); + me.grid.hide(); + me.setValue(''); + me.blur(); + }, + + onKey: function(field, e) { + var me = this; + var key = e.getKey(); + + switch (key) { + case Ext.event.Event.ENTER: + // go to first entry if there is one + if (me.grid.store.getCount() > 0) { + me.selectAndHide(me.grid.getSelection()[0].data.id); + } + break; + case Ext.event.Event.UP: + me.grid.getSelectionModel().selectPrevious(); + break; + case Ext.event.Event.DOWN: + me.grid.getSelectionModel().selectNext(); + break; + case Ext.event.Event.ESC: + me.grid.hide(); + me.blur(); + break; + } + }, + + loadValues: function(field) { + let me = this; + me.hasFocus = true; + me.grid.textfield = me; + me.grid.store.load(); + me.grid.showBy(me, 'tl-bl'); + }, + + hideGrid: function() { + let me = this; + me.hasFocus = false; + if (!me.grid.hasFocus) { + me.grid.hide(); + } + }, + + listeners: { + change: { + fn: 'updateFilter', + buffer: 250, + }, + specialkey: 'onKey', + focusenter: 'loadValues', + focusleave: { + fn: 'hideGrid', + delay: 100, + }, + }, + + toggleFocus: function() { + let me = this; + if (!me.hasFocus) { + me.focus(); + } else { + me.blur(); + } + }, + + initComponent: function() { + let me = this; + + if (!me.tree) { + throw "no tree given"; + } + + me.grid = Ext.create(me.grid); + + me.callParent(); + + // bind CTRL + SHIFT + F and CTRL + SPACE to open/close the search + me.keymap = new Ext.KeyMap({ + target: Ext.get(document), + binding: [{ + key: 'F', + ctrl: true, + shift: true, + fn: me.toggleFocus, + scope: me, + }, { + key: ' ', + ctrl: true, + fn: me.toggleFocus, + scope: me, + }], + }); + + // always select first item and sort by relevance after load + me.mon(me.grid.store, 'load', function() { + me.grid.getSelectionModel().select(0); + me.grid.store.sort({ + property: 'relevance', + direction: 'DESC', + }); + }); + }, +}); +Ext.define('pve-groups', { + extend: 'Ext.data.Model', + fields: ['groupid', 'comment', 'users'], + proxy: { + type: 'proxmox', + url: "/api2/json/access/groups", + }, + idProperty: 'groupid', +}); + +Ext.define('PVE.form.GroupSelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pveGroupSelector', + + allowBlank: false, + autoSelect: false, + valueField: 'groupid', + displayField: 'groupid', + listConfig: { + columns: [ + { + header: gettext('Group'), + sortable: true, + dataIndex: 'groupid', + flex: 1, + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + { + header: gettext('Users'), + sortable: false, + dataIndex: 'users', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + }, + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-groups', + sorters: [{ + property: 'groupid', + }], + }); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + + store.load(); + }, +}); +Ext.define('PVE.form.GuestIDSelector', { + extend: 'Ext.form.field.Number', + alias: 'widget.pveGuestIDSelector', + + allowBlank: false, + + minValue: 100, + + maxValue: 999999999, + + validateExists: undefined, + + loadNextFreeID: false, + + guestType: undefined, + + validator: function(value) { + var me = this; + + if (!Ext.isNumeric(value) || + value < me.minValue || + value > me.maxValue) { + // check is done by ExtJS + return true; + } + + if (me.validateExists === true && !me.exists) { + return me.unknownID; + } + + if (me.validateExists === false && me.exists) { + return me.inUseID; + } + + return true; + }, + + initComponent: function() { + var me = this; + var label = '{0} ID'; + var unknownID = gettext('This {0} ID does not exist'); + var inUseID = gettext('This {0} ID is already in use'); + var type = 'CT/VM'; + + if (me.guestType === 'lxc') { + type = 'CT'; + } else if (me.guestType === 'qemu') { + type = 'VM'; + } + + me.label = Ext.String.format(label, type); + me.unknownID = Ext.String.format(unknownID, type); + me.inUseID = Ext.String.format(inUseID, type); + + Ext.apply(me, { + fieldLabel: me.label, + listeners: { + 'change': function(field, newValue, oldValue) { + if (!Ext.isDefined(me.validateExists)) { + return; + } + Proxmox.Utils.API2Request({ + params: { vmid: newValue }, + url: '/cluster/nextid', + method: 'GET', + success: function(response, opts) { + me.exists = false; + me.validate(); + }, + failure: function(response, opts) { + me.exists = true; + me.validate(); + }, + }); + }, + }, + }); + + me.callParent(); + + if (me.loadNextFreeID) { + Proxmox.Utils.API2Request({ + url: '/cluster/nextid', + method: 'GET', + success: function(response, opts) { + me.setRawValue(response.result.data); + }, + }); + } + }, +}); +Ext.define('PVE.form.hashAlgorithmSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveHashAlgorithmSelector'], + config: { + deleteEmpty: false, + }, + comboItems: [ + ['__default__', 'None'], + ['md5', 'MD5'], + ['sha1', 'SHA-1'], + ['sha224', 'SHA-224'], + ['sha256', 'SHA-256'], + ['sha384', 'SHA-384'], + ['sha512', 'SHA-512'], + ], +}); +Ext.define('PVE.form.HotplugFeatureSelector', { + extend: 'Ext.form.CheckboxGroup', + alias: 'widget.pveHotplugFeatureSelector', + + columns: 1, + vertical: true, + + defaults: { + name: 'hotplugCbGroup', + submitValue: false, + }, + items: [ + { + boxLabel: gettext('Disk'), + inputValue: 'disk', + checked: true, + }, + { + boxLabel: gettext('Network'), + inputValue: 'network', + checked: true, + }, + { + boxLabel: 'USB', + inputValue: 'usb', + checked: true, + }, + { + boxLabel: gettext('Memory'), + inputValue: 'memory', + }, + { + boxLabel: gettext('CPU'), + inputValue: 'cpu', + }, + ], + + setValue: function(value) { + var me = this; + var newVal = []; + if (value === '1') { + newVal = ['disk', 'network', 'usb']; + } else if (value !== '0') { + newVal = value.split(','); + } + me.callParent([{ hotplugCbGroup: newVal }]); + }, + + // override framework function to + // assemble the hotplug value + getSubmitData: function() { + var me = this, + boxes = me.getBoxes(), + data = []; + Ext.Array.forEach(boxes, function(box) { + if (box.getValue()) { + data.push(box.inputValue); + } + }); + + /* because above is hotplug an array */ + if (data.length === 0) { + return { 'hotplug': '0' }; + } else { + return { 'hotplug': data.join(',') }; + } + }, + +}); +Ext.define('PVE.form.IPProtocolSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveIPProtocolSelector'], + valueField: 'p', + displayField: 'p', + listConfig: { + columns: [ + { + header: gettext('Protocol'), + dataIndex: 'p', + hideable: false, + sortable: false, + width: 100, + }, + { + header: gettext('Number'), + dataIndex: 'n', + hideable: false, + sortable: false, + width: 50, + }, + { + header: gettext('Description'), + dataIndex: 'd', + hideable: false, + sortable: false, + flex: 1, + }, + ], + }, + store: { + fields: ['p', 'd', 'n'], + data: [ + { p: 'tcp', n: 6, d: 'Transmission Control Protocol' }, + { p: 'udp', n: 17, d: 'User Datagram Protocol' }, + { p: 'icmp', n: 1, d: 'Internet Control Message Protocol' }, + { p: 'igmp', n: 2, d: 'Internet Group Management' }, + { p: 'ggp', n: 3, d: 'gateway-gateway protocol' }, + { p: 'ipencap', n: 4, d: 'IP encapsulated in IP' }, + { p: 'st', n: 5, d: 'ST datagram mode' }, + { p: 'egp', n: 8, d: 'exterior gateway protocol' }, + { p: 'igp', n: 9, d: 'any private interior gateway (Cisco)' }, + { p: 'pup', n: 12, d: 'PARC universal packet protocol' }, + { p: 'hmp', n: 20, d: 'host monitoring protocol' }, + { p: 'xns-idp', n: 22, d: 'Xerox NS IDP' }, + { p: 'rdp', n: 27, d: '"reliable datagram" protocol' }, + { p: 'iso-tp4', n: 29, d: 'ISO Transport Protocol class 4 [RFC905]' }, + { p: 'dccp', n: 33, d: 'Datagram Congestion Control Prot. [RFC4340]' }, + { p: 'xtp', n: 36, d: 'Xpress Transfer Protocol' }, + { p: 'ddp', n: 37, d: 'Datagram Delivery Protocol' }, + { p: 'idpr-cmtp', n: 38, d: 'IDPR Control Message Transport' }, + { p: 'ipv6', n: 41, d: 'Internet Protocol, version 6' }, + { p: 'ipv6-route', n: 43, d: 'Routing Header for IPv6' }, + { p: 'ipv6-frag', n: 44, d: 'Fragment Header for IPv6' }, + { p: 'idrp', n: 45, d: 'Inter-Domain Routing Protocol' }, + { p: 'rsvp', n: 46, d: 'Reservation Protocol' }, + { p: 'gre', n: 47, d: 'General Routing Encapsulation' }, + { p: 'esp', n: 50, d: 'Encap Security Payload [RFC2406]' }, + { p: 'ah', n: 51, d: 'Authentication Header [RFC2402]' }, + { p: 'skip', n: 57, d: 'SKIP' }, + { p: 'ipv6-icmp', n: 58, d: 'ICMP for IPv6' }, + { p: 'ipv6-nonxt', n: 59, d: 'No Next Header for IPv6' }, + { p: 'ipv6-opts', n: 60, d: 'Destination Options for IPv6' }, + { p: 'vmtp', n: 81, d: 'Versatile Message Transport' }, + { p: 'eigrp', n: 88, d: 'Enhanced Interior Routing Protocol (Cisco)' }, + { p: 'ospf', n: 89, d: 'Open Shortest Path First IGP' }, + { p: 'ax.25', n: 93, d: 'AX.25 frames' }, + { p: 'ipip', n: 94, d: 'IP-within-IP Encapsulation Protocol' }, + { p: 'etherip', n: 97, d: 'Ethernet-within-IP Encapsulation [RFC3378]' }, + { p: 'encap', n: 98, d: 'Yet Another IP encapsulation [RFC1241]' }, + { p: 'pim', n: 103, d: 'Protocol Independent Multicast' }, + { p: 'ipcomp', n: 108, d: 'IP Payload Compression Protocol' }, + { p: 'vrrp', n: 112, d: 'Virtual Router Redundancy Protocol [RFC5798]' }, + { p: 'l2tp', n: 115, d: 'Layer Two Tunneling Protocol [RFC2661]' }, + { p: 'isis', n: 124, d: 'IS-IS over IPv4' }, + { p: 'sctp', n: 132, d: 'Stream Control Transmission Protocol' }, + { p: 'fc', n: 133, d: 'Fibre Channel' }, + { p: 'mobility-header', n: 135, d: 'Mobility Support for IPv6 [RFC3775]' }, + { p: 'udplite', n: 136, d: 'UDP-Lite [RFC3828]' }, + { p: 'mpls-in-ip', n: 137, d: 'MPLS-in-IP [RFC4023]' }, + { p: 'hip', n: 139, d: 'Host Identity Protocol' }, + { p: 'shim6', n: 140, d: 'Shim6 Protocol [RFC5533]' }, + { p: 'wesp', n: 141, d: 'Wrapped Encapsulating Security Payload' }, + { p: 'rohc', n: 142, d: 'Robust Header Compression' }, + ], + }, +}); +Ext.define('PVE.form.IPRefSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveIPRefSelector'], + + base_url: undefined, + + preferredValue: '', // hack: else Form sets dirty flag? + + ref_type: undefined, // undefined = any [undefined, 'ipset' or 'alias'] + + valueField: 'ref', + displayField: 'ref', + notFoundIsValid: true, + + initComponent: function() { + var me = this; + + if (!me.base_url) { + throw "no base_url specified"; + } + + var url = "/api2/json" + me.base_url; + if (me.ref_type) { + url += "?type=" + me.ref_type; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: true, + fields: ['type', 'name', 'ref', 'comment'], + idProperty: 'ref', + proxy: { + type: 'proxmox', + url: url, + }, + sorters: { + property: 'ref', + direction: 'ASC', + }, + }); + + var disable_query_for_ips = function(f, value) { + if (value === null || + value.match(/^\d/)) { // IP address starts with \d + f.queryDelay = 9999999999; // hack: disable with long delay + } else { + f.queryDelay = 10; + } + }; + + var columns = []; + + if (!me.ref_type) { + columns.push({ + header: gettext('Type'), + dataIndex: 'type', + hideable: false, + width: 60, + }); + } + + columns.push( + { + header: gettext('Name'), + dataIndex: 'ref', + hideable: false, + width: 140, + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ); + + Ext.apply(me, { + store: store, + listConfig: { columns: columns }, + }); + + me.on('change', disable_query_for_ips); + + me.callParent(); + }, +}); + +Ext.define('PVE.form.MDevSelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pveMDevSelector', + + store: { + fields: ['type', 'available', 'description'], + filterOnLoad: true, + sorters: [ + { + property: 'type', + direction: 'ASC', + }, + ], + }, + autoSelect: false, + valueField: 'type', + displayField: 'type', + listConfig: { + width: 550, + columns: [ + { + header: gettext('Type'), + dataIndex: 'type', + renderer: function(value, md, rec) { + if (rec.data.name !== undefined) { + return `${rec.data.name} (${value})`; + } + return value; + }, + flex: 1, + }, + { + header: gettext('Avail'), + dataIndex: 'available', + width: 60, + }, + { + header: gettext('Description'), + dataIndex: 'description', + flex: 1, + cellWrap: true, + renderer: function(value) { + if (!value) { + return ''; + } + + return value.split('\n').join('
'); + }, + }, + ], + }, + + setPciID: function(pciid, force) { + var me = this; + + if (!force && (!pciid || me.pciid === pciid)) { + return; + } + + me.pciid = pciid; + me.updateProxy(); + }, + + + setNodename: function(nodename) { + var me = this; + + if (!nodename || me.nodename === nodename) { + return; + } + + me.nodename = nodename; + me.updateProxy(); + }, + + updateProxy: function() { + var me = this; + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/hardware/pci/' + me.pciid + '/mdev', + }); + me.store.load(); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw 'no node name specified'; + } + + me.callParent(); + + if (me.pciid) { + me.setPciID(me.pciid, true); + } + }, +}); + +Ext.define('PVE.form.MemoryField', { + extend: 'Ext.form.field.Number', + alias: 'widget.pveMemoryField', + + allowBlank: false, + + hotplug: false, + + minValue: 32, + + maxValue: 4178944, + + step: 32, + + value: '512', // qm backend default + + allowDecimals: false, + + allowExponential: false, + + computeUpDown: function(value) { + var me = this; + + if (!me.hotplug) { + return { up: value + me.step, down: value - me.step }; + } + + var dimm_size = 512; + var prev_dimm_size = 0; + var min_size = 1024; + var current_size = min_size; + var value_up = min_size; + var value_down = min_size; + var value_start = min_size; + + var i, j; + for (j = 0; j < 9; j++) { + for (i = 0; i < 32; i++) { + if (value >= current_size && value < current_size + dimm_size) { + value_start = current_size; + value_up = current_size + dimm_size; + value_down = current_size - (i === 0 ? prev_dimm_size : dimm_size); + } + current_size += dimm_size; + } + prev_dimm_size = dimm_size; + dimm_size = dimm_size*2; + } + + return { up: value_up, down: value_down, start: value_start }; + }, + + onSpinUp: function() { + var me = this; + if (!me.readOnly) { + var res = me.computeUpDown(me.getValue()); + me.setValue(Ext.Number.constrain(res.up, me.minValue, me.maxValue)); + } + }, + + onSpinDown: function() { + var me = this; + if (!me.readOnly) { + var res = me.computeUpDown(me.getValue()); + me.setValue(Ext.Number.constrain(res.down, me.minValue, me.maxValue)); + } + }, + + initComponent: function() { + var me = this; + + if (me.hotplug) { + me.minValue = 1024; + + me.on('blur', function(field) { + var value = me.getValue(); + var res = me.computeUpDown(value); + if (value === res.start || value === res.up || value === res.down) { + return; + } + field.setValue(res.up); + }); + } + + me.callParent(); + }, +}); +Ext.define('PVE.form.NetworkCardSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: 'widget.pveNetworkCardSelector', + comboItems: [ + ['e1000', 'Intel E1000'], + ['virtio', 'VirtIO (' + gettext('paravirtualized') + ')'], + ['rtl8139', 'Realtek RTL8139'], + ['vmxnet3', 'VMware vmxnet3'], + ], +}); +Ext.define('PVE.form.NodeSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveNodeSelector'], + + // invalidate nodes which are offline + onlineValidator: false, + + selectCurNode: false, + + // do not allow those nodes (array) + disallowedNodes: undefined, + + // only allow those nodes (array) + allowedNodes: undefined, + // set default value to empty array, else it inits it with + // null and after the store load it is an empty array, + // triggering dirtychange + value: [], + valueField: 'node', + displayField: 'node', + store: { + fields: ['node', 'cpu', 'maxcpu', 'mem', 'maxmem', 'uptime'], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes', + }, + sorters: [ + { + property: 'node', + direction: 'ASC', + }, + { + property: 'mem', + direction: 'DESC', + }, + ], + }, + + listConfig: { + columns: [ + { + header: gettext('Node'), + dataIndex: 'node', + sortable: true, + hideable: false, + flex: 1, + }, + { + header: gettext('Memory usage') + " %", + renderer: PVE.Utils.render_mem_usage_percent, + sortable: true, + width: 100, + dataIndex: 'mem', + }, + { + header: gettext('CPU usage'), + renderer: Proxmox.Utils.render_cpu, + sortable: true, + width: 100, + dataIndex: 'cpu', + }, + ], + }, + + validator: function(value) { + let me = this; + if (!me.onlineValidator || (me.allowBlank && !value)) { + return true; + } + + let offline = [], notAllowed = []; + Ext.Array.each(value.split(/\s*,\s*/), function(node) { + let rec = me.store.findRecord(me.valueField, node, 0, false, true, true); + if (!(rec && rec.data) || rec.data.status !== 'online') { + offline.push(node); + } else if (me.allowedNodes && !Ext.Array.contains(me.allowedNodes, node)) { + notAllowed.push(node); + } + }); + + if (value && notAllowed.length !== 0) { + return "Node " + notAllowed.join(', ') + " is not allowed for this action!"; + } + if (value && offline.length !== 0) { + return "Node " + offline.join(', ') + " seems to be offline!"; + } + return true; + }, + + initComponent: function() { + var me = this; + + if (me.selectCurNode && PVE.curSelectedNode && PVE.curSelectedNode.data.node) { + me.preferredValue = PVE.curSelectedNode.data.node; + } + + me.callParent(); + me.getStore().load(); + + me.getStore().addFilter(new Ext.util.Filter({ // filter out disallowed nodes + filterFn: (item) => !(me.disallowedNodes && me.disallowedNodes.includes(item.data.node)), + })); + + me.mon(me.getStore(), 'load', () => me.isValid()); + }, +}); +Ext.define('PVE.form.PCISelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pvePCISelector', + + store: { + fields: ['id', 'vendor_name', 'device_name', 'vendor', 'device', 'iommugroup', 'mdev'], + filterOnLoad: true, + sorters: [ + { + property: 'id', + direction: 'ASC', + }, + ], + }, + + autoSelect: false, + valueField: 'id', + displayField: 'id', + + // can contain a load callback for the store + // useful to determine the state of the IOMMU + onLoadCallBack: undefined, + + listConfig: { + width: 800, + columns: [ + { + header: 'ID', + dataIndex: 'id', + width: 100, + }, + { + header: gettext('IOMMU Group'), + dataIndex: 'iommugroup', + renderer: v => v === -1 ? '-' : v, + width: 75, + }, + { + header: gettext('Vendor'), + dataIndex: 'vendor_name', + flex: 2, + }, + { + header: gettext('Device'), + dataIndex: 'device_name', + flex: 6, + }, + { + header: gettext('Mediated Devices'), + dataIndex: 'mdev', + flex: 1, + renderer: function(val) { + return Proxmox.Utils.format_boolean(!!val); + }, + }, + ], + }, + + setNodename: function(nodename) { + var me = this; + + if (!nodename || me.nodename === nodename) { + return; + } + + me.nodename = nodename; + + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/hardware/pci', + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + me.nodename = undefined; + + me.callParent(); + + if (me.onLoadCallBack !== undefined) { + me.mon(me.getStore(), 'load', me.onLoadCallBack); + } + + me.setNodename(nodename); + }, +}); + +Ext.define('PVE.form.PermPathSelector', { + extend: 'Ext.form.field.ComboBox', + xtype: 'pvePermPathSelector', + + valueField: 'value', + displayField: 'value', + typeAhead: true, + queryMode: 'local', + + store: { + type: 'pvePermPath', + }, +}); +Ext.define('PVE.form.PoolSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pvePoolSelector'], + + allowBlank: false, + valueField: 'poolid', + displayField: 'poolid', + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-pools', + sorters: 'poolid', + }); + + Ext.apply(me, { + store: store, + autoSelect: false, + listConfig: { + columns: [ + { + header: gettext('Pool'), + sortable: true, + dataIndex: 'poolid', + flex: 1, + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + }, + }); + + me.callParent(); + + store.load(); + }, + +}, function() { + Ext.define('pve-pools', { + extend: 'Ext.data.Model', + fields: ['poolid', 'comment'], + proxy: { + type: 'proxmox', + url: "/api2/json/pools", + }, + idProperty: 'poolid', + }); +}); +Ext.define('PVE.form.preallocationSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pvePreallocationSelector'], + comboItems: [ + ['__default__', Proxmox.Utils.defaultText], + ['off', 'Off'], + ['metadata', 'Metadata'], + ['falloc', 'Full (posix_fallocate)'], + ['full', 'Full'], + ], +}); +Ext.define('PVE.form.PrivilegesSelector', { + extend: 'Proxmox.form.KVComboBox', + xtype: 'pvePrivilegesSelector', + + multiSelect: true, + + initComponent: function() { + let me = this; + + me.callParent(); + + Proxmox.Utils.API2Request({ + url: '/access/roles/Administrator', + method: 'GET', + success: function(response, options) { + let data = Object.keys(response.result.data).map(key => [key, key]); + + me.store.setData(data); + + me.store.sort({ + property: 'key', + direction: 'ASC', + }); + }, + failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + }, +}); +Ext.define('PVE.form.QemuBiosSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveQemuBiosSelector'], + + initComponent: function() { + var me = this; + + me.comboItems = [ + ['__default__', PVE.Utils.render_qemu_bios('')], + ['seabios', PVE.Utils.render_qemu_bios('seabios')], + ['ovmf', PVE.Utils.render_qemu_bios('ovmf')], + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.form.SDNControllerSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveSDNControllerSelector'], + + allowBlank: false, + valueField: 'controller', + displayField: 'controller', + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-sdn-controller', + sorters: { + property: 'controller', + direction: 'ASC', + }, + }); + + Ext.apply(me, { + store: store, + autoSelect: false, + listConfig: { + columns: [ + { + header: gettext('Controller'), + sortable: true, + dataIndex: 'controller', + flex: 1, + }, + ], + }, + }); + + me.callParent(); + + store.load(); + }, + +}, function() { + Ext.define('pve-sdn-controller', { + extend: 'Ext.data.Model', + fields: ['controller'], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/sdn/controllers", + }, + idProperty: 'controller', + }); +}); +Ext.define('PVE.form.SDNZoneSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveSDNZoneSelector'], + + allowBlank: false, + valueField: 'zone', + displayField: 'zone', + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-sdn-zone', + sorters: { + property: 'zone', + direction: 'ASC', + }, + }); + + Ext.apply(me, { + store: store, + autoSelect: false, + listConfig: { + columns: [ + { + header: gettext('Zone'), + sortable: true, + dataIndex: 'zone', + flex: 1, + }, + ], + }, + }); + + me.callParent(); + + store.load(); + }, + +}, function() { + Ext.define('pve-sdn-zone', { + extend: 'Ext.data.Model', + fields: ['zone'], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/sdn/zones", + }, + idProperty: 'zone', + }); +}); +Ext.define('PVE.form.SDNVnetSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveSDNVnetSelector'], + + allowBlank: false, + valueField: 'vnet', + displayField: 'vnet', + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-sdn-vnet', + sorters: { + property: 'vnet', + direction: 'ASC', + }, + }); + + Ext.apply(me, { + store: store, + autoSelect: false, + listConfig: { + columns: [ + { + header: gettext('Vnet'), + sortable: true, + dataIndex: 'vnet', + flex: 1, + }, + { + header: gettext('Alias'), + flex: 1, + dataIndex: 'alias', + }, + { + header: gettext('Tag'), + flex: 1, + dataIndex: 'tag', + }, + ], + }, + }); + + me.callParent(); + + store.load(); + }, + +}, function() { + Ext.define('pve-sdn-vnet', { + extend: 'Ext.data.Model', + fields: [ + 'alias', + 'tag', + 'type', + 'vnet', + 'zone', + ], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/sdn/vnets", + }, + idProperty: 'vnet', + }); +}); +Ext.define('PVE.form.SDNIpamSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveSDNIpamSelector'], + + allowBlank: false, + valueField: 'ipam', + displayField: 'ipam', + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-sdn-ipam', + sorters: { + property: 'ipam', + direction: 'ASC', + }, + }); + + Ext.apply(me, { + store: store, + autoSelect: false, + listConfig: { + columns: [ + { + header: gettext('Ipam'), + sortable: true, + dataIndex: 'ipam', + flex: 1, + }, + ], + }, + }); + + me.callParent(); + + store.load(); + }, + +}, function() { + Ext.define('pve-sdn-ipam', { + extend: 'Ext.data.Model', + fields: ['ipam'], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/sdn/ipams", + }, + idProperty: 'ipam', + }); +}); +Ext.define('PVE.form.SDNDnsSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveSDNDnsSelector'], + + allowBlank: false, + valueField: 'dns', + displayField: 'dns', + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-sdn-dns', + sorters: { + property: 'dns', + direction: 'ASC', + }, + }); + + Ext.apply(me, { + store: store, + autoSelect: false, + listConfig: { + columns: [ + { + header: gettext('dns'), + sortable: true, + dataIndex: 'dns', + flex: 1, + }, + ], + }, + }); + + me.callParent(); + + store.load(); + }, + +}, function() { + Ext.define('pve-sdn-dns', { + extend: 'Ext.data.Model', + fields: ['dns'], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/sdn/dns", + }, + idProperty: 'dns', + }); +}); +Ext.define('PVE.form.ScsiHwSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveScsiHwSelector'], + comboItems: [ + ['__default__', PVE.Utils.render_scsihw('')], + ['lsi', PVE.Utils.render_scsihw('lsi')], + ['lsi53c810', PVE.Utils.render_scsihw('lsi53c810')], + ['megasas', PVE.Utils.render_scsihw('megasas')], + ['virtio-scsi-pci', PVE.Utils.render_scsihw('virtio-scsi-pci')], + ['virtio-scsi-single', PVE.Utils.render_scsihw('virtio-scsi-single')], + ['pvscsi', PVE.Utils.render_scsihw('pvscsi')], + ], +}); +Ext.define('PVE.form.SecurityGroupsSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveSecurityGroupsSelector'], + + valueField: 'group', + displayField: 'group', + initComponent: function() { + var me = this; + + var store = Ext.create('Ext.data.Store', { + autoLoad: true, + fields: ['group', 'comment'], + idProperty: 'group', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/firewall/groups", + }, + sorters: { + property: 'group', + direction: 'ASC', + }, + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: gettext('Security Group'), + dataIndex: 'group', + hideable: false, + width: 100, + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + }, + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.form.SnapshotSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.PVE.form.SnapshotSelector'], + + valueField: 'name', + displayField: 'name', + + loadStore: function(nodename, vmid) { + var me = this; + + if (!nodename) { + return; + } + + me.nodename = nodename; + + if (!vmid) { + return; + } + + me.vmid = vmid; + + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid +'/snapshot', + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.guestType) { + throw "no guest type specified"; + } + + var store = Ext.create('Ext.data.Store', { + fields: ['name'], + filterOnLoad: true, + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: gettext('Snapshot'), + dataIndex: 'name', + hideable: false, + flex: 1, + }, + ], + }, + }); + + me.callParent(); + + me.loadStore(me.nodename, me.vmid); + }, +}); +Ext.define('PVE.form.SpiceEnhancementSelector', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveSpiceEnhancementSelector', + + viewModel: {}, + + items: [ + { + xtype: 'proxmoxcheckbox', + itemId: 'foldersharing', + name: 'foldersharing', + reference: 'foldersharing', + fieldLabel: 'Folder Sharing', + uncheckedValue: 0, + }, + { + xtype: 'proxmoxKVComboBox', + itemId: 'videostreaming', + name: 'videostreaming', + value: 'off', + fieldLabel: 'Video Streaming', + comboItems: [ + ['off', 'off'], + ['all', 'all'], + ['filter', 'filter'], + ], + }, + { + xtype: 'displayfield', + itemId: 'spicehint', + userCls: 'pmx-hint', + value: gettext('To use these features set the display to SPICE in the hardware settings of the VM.'), + hidden: true, + }, + { + xtype: 'displayfield', + itemId: 'spicefolderhint', + userCls: 'pmx-hint', + value: gettext('Make sure the SPICE WebDav daemon is installed in the VM.'), + bind: { + hidden: '{!foldersharing.checked}', + }, + }, + ], + + onGetValues: function(values) { + var ret = {}; + + if (values.videostreaming !== "off") { + ret.videostreaming = values.videostreaming; + } + if (values.foldersharing) { + ret.foldersharing = 1; + } + if (Ext.Object.isEmpty(ret)) { + return { 'delete': 'spice_enhancements' }; + } + var enhancements = PVE.Parser.printPropertyString(ret); + return { spice_enhancements: enhancements }; + }, + + setValues: function(values) { + var vga = PVE.Parser.parsePropertyString(values.vga, 'type'); + if (!/^qxl\d?$/.test(vga.type)) { + this.down('#spicehint').setVisible(true); + } + if (values.spice_enhancements) { + var enhancements = PVE.Parser.parsePropertyString(values.spice_enhancements); + enhancements.foldersharing = PVE.Parser.parseBoolean(enhancements.foldersharing, 0); + this.callParent([enhancements]); + } + }, +}); +Ext.define('PVE.form.StorageScanNodeSelector', { + extend: 'PVE.form.NodeSelector', + xtype: 'pveStorageScanNodeSelector', + + name: 'storageScanNode', + itemId: 'pveStorageScanNodeSelector', + fieldLabel: gettext('Scan node'), + allowBlank: true, + disallowedNodes: undefined, + autoSelect: false, + submitValue: false, + value: null, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Scan for available storages on the selected node'), + }, + triggers: { + clear: { + handler: function() { + let me = this; + me.setValue(null); + }, + }, + }, + + emptyText: Proxmox.NodeName, + + setValue: function(value) { + let me = this; + me.callParent([value]); + me.triggers.clear.setVisible(!!value); + }, +}); +Ext.define('PVE.form.StorageSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveStorageSelector', + mixins: ['Proxmox.Mixin.CBind'], + + cbindData: { + clusterView: false, + }, + + allowBlank: false, + valueField: 'storage', + displayField: 'storage', + listConfig: { + cbind: { + clusterView: '{clusterView}', + }, + width: 450, + columns: [ + { + header: gettext('Name'), + dataIndex: 'storage', + hideable: false, + flex: 1, + }, + { + header: gettext('Type'), + width: 75, + dataIndex: 'type', + }, + { + header: gettext('Avail'), + width: 90, + dataIndex: 'avail', + renderer: Proxmox.Utils.format_size, + cbind: { + hidden: '{clusterView}', + }, + }, + { + header: gettext('Capacity'), + width: 90, + dataIndex: 'total', + renderer: Proxmox.Utils.format_size, + cbind: { + hidden: '{clusterView}', + }, + }, + { + header: gettext('Nodes'), + width: 120, + dataIndex: 'nodes', + renderer: (value) => value ? value : '-- ' + gettext('All') + ' --', + cbind: { + hidden: '{!clusterView}', + }, + }, + { + header: gettext('Shared'), + width: 70, + dataIndex: 'shared', + renderer: Proxmox.Utils.format_boolean, + cbind: { + hidden: '{!clusterView}', + }, + }, + ], + }, + + reloadStorageList: function() { + let me = this; + + if (me.clusterView) { + me.getStore().setProxy({ + type: 'proxmox', + url: `/api2/json/storage`, + }); + + // filter here, back-end does not support it currently + let filters = [(storage) => !storage.data.disable]; + + if (me.storageContent) { + filters.push( + (storage) => storage.data.content.split(',').includes(me.storageContent), + ); + } + + if (me.nodename) { + filters.push( + (storage) => !storage.data.nodes || storage.data.nodes.includes(me.nodename), + ); + } + + me.getStore().clearFilter(); + me.getStore().setFilters(filters); + } else { + if (!me.nodename) { + return; + } + + let params = { + format: 1, + }; + if (me.storageContent) { + params.content = me.storageContent; + } + if (me.targetNode) { + params.target = me.targetNode; + params.enabled = 1; // skip disabled storages + } + me.store.setProxy({ + type: 'proxmox', + url: `/api2/json/nodes/${me.nodename}/storage`, + extraParams: params, + }); + } + + me.store.load(() => me.validate()); + }, + + setTargetNode: function(targetNode) { + var me = this; + + if (!targetNode || me.targetNode === targetNode) { + return; + } + + if (me.clusterView) { + throw "setting targetNode with clusterView is not implemented"; + } + + me.targetNode = targetNode; + + me.reloadStorageList(); + }, + + setNodename: function(nodename) { + var me = this; + + nodename = nodename || ''; + + if (me.nodename === nodename) { + return; + } + + me.nodename = nodename; + + me.reloadStorageList(); + }, + + initComponent: function() { + var me = this; + + let nodename = me.nodename; + me.nodename = undefined; + + var store = Ext.create('Ext.data.Store', { + model: 'pve-storage-status', + sorters: { + property: 'storage', + direction: 'ASC', + }, + }); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + + me.setNodename(nodename); + }, +}, function() { + Ext.define('pve-storage-status', { + extend: 'Ext.data.Model', + fields: ['storage', 'active', 'type', 'avail', 'total', 'nodes', 'shared'], + idProperty: 'storage', + }); +}); +Ext.define('PVE.form.TFASelector', { + extend: 'Ext.container.Container', + xtype: 'pveTFASelector', + mixins: ['Proxmox.Mixin.CBind'], + + deleteEmpty: true, + + viewModel: { + data: { + type: '__default__', + step: null, + digits: null, + id: null, + key: null, + url: null, + }, + + formulas: { + isOath: (get) => get('type') === 'oath', + isYubico: (get) => get('type') === 'yubico', + tfavalue: { + get: function(get) { + let val = { + type: get('type'), + }; + if (get('isOath')) { + let step = get('step'); + let digits = get('digits'); + if (step) { + val.step = step; + } + if (digits) { + val.digits = digits; + } + } else if (get('isYubico')) { + let id = get('id'); + let key = get('key'); + let url = get('url'); + val.id = id; + val.key = key; + if (url) { + val.url = url; + } + } else if (val.type === '__default__') { + return ""; + } + + return PVE.Parser.printPropertyString(val); + }, + set: function(value) { + let val = PVE.Parser.parseTfaConfig(value); + this.set(val); + this.notify(); + // we need to reset the original values, so that + // we can reliably track the state of the form + let form = this.getView().up('form'); + if (form.trackResetOnLoad) { + let fields = this.getView().query('field[name!="tfa"]'); + fields.forEach((field) => field.resetOriginalValue()); + } + }, + }, + }, + }, + + items: [ + { + xtype: 'proxmoxtextfield', + name: 'tfa', + hidden: true, + submitValue: true, + cbind: { + deleteEmpty: '{deleteEmpty}', + }, + bind: { + value: "{tfavalue}", + }, + }, + { + xtype: 'proxmoxKVComboBox', + value: '__default__', + deleteEmpty: false, + submitValue: false, + fieldLabel: gettext('Require TFA'), + comboItems: [ + ['__default__', Proxmox.Utils.noneText], + ['oath', 'OATH/TOTP'], + ['yubico', 'Yubico'], + ], + bind: { + value: "{type}", + }, + }, + { + xtype: 'proxmoxintegerfield', + hidden: true, + minValue: 10, + submitValue: false, + emptyText: Proxmox.Utils.defaultText + ' (30)', + fieldLabel: gettext('Time Step'), + bind: { + value: "{step}", + hidden: "{!isOath}", + disabled: "{!isOath}", + }, + }, + { + xtype: 'proxmoxintegerfield', + hidden: true, + submitValue: false, + fieldLabel: gettext('Secret Length'), + minValue: 6, + maxValue: 8, + emptyText: Proxmox.Utils.defaultText + ' (6)', + bind: { + value: "{digits}", + hidden: "{!isOath}", + disabled: "{!isOath}", + }, + }, + { + xtype: 'textfield', + hidden: true, + submitValue: false, + allowBlank: false, + fieldLabel: 'Yubico API Id', + bind: { + value: "{id}", + hidden: "{!isYubico}", + disabled: "{!isYubico}", + }, + }, + { + xtype: 'textfield', + hidden: true, + submitValue: false, + allowBlank: false, + fieldLabel: 'Yubico API Key', + bind: { + value: "{key}", + hidden: "{!isYubico}", + disabled: "{!isYubico}", + }, + }, + { + xtype: 'textfield', + hidden: true, + submitValue: false, + fieldLabel: 'Yubico URL', + bind: { + value: "{url}", + hidden: "{!isYubico}", + disabled: "{!isYubico}", + }, + }, + ], +}); +Ext.define('PVE.form.TokenSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveTokenSelector'], + + allowBlank: false, + autoSelect: false, + displayField: 'id', + + editable: true, + anyMatch: true, + forceSelection: true, + + store: { + model: 'pve-tokens', + autoLoad: true, + proxy: { + type: 'proxmox', + url: 'api2/json/access/users', + extraParams: { 'full': 1 }, + }, + sorters: 'id', + listeners: { + load: function(store, records, success) { + let tokens = []; + for (const { data: user } of records) { + if (!user.tokens || user.tokens.length === 0) { + continue; + } + for (const token of user.tokens) { + tokens.push({ + id: `${user.userid}!${token.tokenid}`, + comment: token.comment, + }); + } + } + store.loadData(tokens); + }, + }, + }, + + listConfig: { + columns: [ + { + header: gettext('API Token'), + sortable: true, + dataIndex: 'id', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + }, +}, function() { + Ext.define('pve-tokens', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'userid', 'tokenid', 'comment', + { type: 'boolean', name: 'privsep' }, + { type: 'date', dateFormat: 'timestamp', name: 'expire' }, + ], + idProperty: 'id', + }); +}); +Ext.define('PVE.form.USBSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveUSBSelector'], + + allowBlank: false, + autoSelect: false, + anyMatch: true, + displayField: 'product_and_id', + valueField: 'usbid', + editable: true, + + validator: function(value) { + var me = this; + if (!value) { + return true; // handled later by allowEmpty in the getErrors call chain + } + value = me.getValue(); // as the valueField is not the displayfield + if (me.type === 'device') { + return (/^[a-f0-9]{4}:[a-f0-9]{4}$/i).test(value); + } else if (me.type === 'port') { + return (/^[0-9]+-[0-9]+(\.[0-9]+)*$/).test(value); + } + return gettext("Invalid Value"); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + + if (!nodename) { + throw "no nodename specified"; + } + + if (me.type !== 'device' && me.type !== 'port') { + throw "no valid type specified"; + } + + let store = new Ext.data.Store({ + model: `pve-usb-${me.type}`, + proxy: { + type: 'proxmox', + url: `/api2/json/nodes/${nodename}/hardware/usb`, + }, + filters: [ + ({ data }) => !!data.usbpath && !!data.prodid && String(data.class) !== "9", + ], + }); + let emptyText = ''; + if (me.type === 'device') { + emptyText = gettext('Passthrough a specific device'); + } else { + emptyText = gettext('Passthrough a full port'); + } + + Ext.apply(me, { + store: store, + emptyText: emptyText, + listConfig: { + width: 520, + columns: [ + { + header: me.type === 'device'?gettext('Device'):gettext('Port'), + sortable: true, + dataIndex: 'usbid', + width: 80, + }, + { + header: gettext('Manufacturer'), + sortable: true, + dataIndex: 'manufacturer', + width: 150, + }, + { + header: gettext('Product'), + sortable: true, + dataIndex: 'product', + flex: 1, + }, + { + header: gettext('Speed'), + width: 75, + sortable: true, + dataIndex: 'speed', + renderer: function(value) { + let speed2Class = { + "10000": "USB 3.1", + "5000": "USB 3.0", + "480": "USB 2.0", + "12": "USB 1.x", + "1.5": "USB 1.x", + }; + return speed2Class[value] || value + " Mbps"; + }, + }, + ], + }, + }); + + me.callParent(); + + store.load(); + }, + +}, function() { + Ext.define('pve-usb-device', { + extend: 'Ext.data.Model', + fields: [ + { + name: 'usbid', + convert: function(val, data) { + if (val) { + return val; + } + return data.get('vendid') + ':' + data.get('prodid'); + }, + }, + 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath', + { name: 'port', type: 'number' }, + { name: 'level', type: 'number' }, + { name: 'class', type: 'number' }, + { name: 'devnum', type: 'number' }, + { name: 'busnum', type: 'number' }, + { + name: 'product_and_id', + type: 'string', + convert: (v, rec) => { + let res = rec.data.product || gettext('Unkown'); + res += " (" + rec.data.usbid + ")"; + return res; + }, + }, + ], + }); + + Ext.define('pve-usb-port', { + extend: 'Ext.data.Model', + fields: [ + { + name: 'usbid', + convert: function(val, data) { + if (val) { + return val; + } + return data.get('busnum') + '-' + data.get('usbpath'); + }, + }, + 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath', + { name: 'port', type: 'number' }, + { name: 'level', type: 'number' }, + { name: 'class', type: 'number' }, + { name: 'devnum', type: 'number' }, + { name: 'busnum', type: 'number' }, + { + name: 'product_and_id', + type: 'string', + convert: (v, rec) => { + let res = rec.data.product || gettext('Unplugged'); + res += " (" + rec.data.usbid + ")"; + return res; + }, + }, + ], + }); +}); +Ext.define('pmx-users', { + extend: 'Ext.data.Model', + fields: [ + 'userid', 'firstname', 'lastname', 'email', 'comment', + { type: 'boolean', name: 'enable' }, + { type: 'date', dateFormat: 'timestamp', name: 'expire' }, + ], + proxy: { + type: 'proxmox', + url: "/api2/json/access/users", + }, + idProperty: 'userid', +}); +Ext.define('PVE.form.VlanField', { + extend: 'Ext.form.field.Number', + alias: ['widget.pveVlanField'], + + deleteEmpty: false, + + emptyText: 'no VLAN', + + fieldLabel: gettext('VLAN Tag'), + + allowBlank: true, + + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue) { + val = me.getSubmitValue(); + if (val) { + data = {}; + data[me.getName()] = val; + } else if (me.deleteEmpty) { + data = {}; + data.delete = me.getName(); + } + } + return data; + }, + + initComponent: function() { + var me = this; + + Ext.apply(me, { + minValue: 1, + maxValue: 4094, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.form.VMCPUFlagSelector', { + extend: 'Ext.grid.Panel', + alias: 'widget.vmcpuflagselector', + + mixins: { + field: 'Ext.form.field.Field', + }, + + disableSelection: true, + columnLines: false, + selectable: false, + hideHeaders: true, + + scrollable: 'y', + height: 200, + + unkownFlags: [], + + store: { + type: 'store', + fields: ['flag', { name: 'state', defaultValue: '=' }, 'desc'], + data: [ + // FIXME: let qemu-server host this and autogenerate or get from API call?? + { flag: 'md-clear', desc: 'Required to let the guest OS know if MDS is mitigated correctly' }, + { flag: 'pcid', desc: 'Meltdown fix cost reduction on Westmere, Sandy-, and IvyBridge Intel CPUs' }, + { flag: 'spec-ctrl', desc: 'Allows improved Spectre mitigation with Intel CPUs' }, + { flag: 'ssbd', desc: 'Protection for "Speculative Store Bypass" for Intel models' }, + { flag: 'ibpb', desc: 'Allows improved Spectre mitigation with AMD CPUs' }, + { flag: 'virt-ssbd', desc: 'Basis for "Speculative Store Bypass" protection for AMD models' }, + { flag: 'amd-ssbd', desc: 'Improves Spectre mitigation performance with AMD CPUs, best used with "virt-ssbd"' }, + { flag: 'amd-no-ssb', desc: 'Notifies guest OS that host is not vulnerable for Spectre on AMD CPUs' }, + { flag: 'pdpe1gb', desc: 'Allow guest OS to use 1GB size pages, if host HW supports it' }, + { flag: 'hv-tlbflush', desc: 'Improve performance in overcommitted Windows guests. May lead to guest bluescreens on old CPUs.' }, + { flag: 'hv-evmcs', desc: 'Improve performance for nested virtualization. Only supported on Intel CPUs.' }, + { flag: 'aes', desc: 'Activate AES instruction set for HW acceleration.' }, + ], + listeners: { + update: function() { + this.commitChanges(); + }, + }, + }, + + getValue: function() { + var me = this; + var store = me.getStore(); + var flags = ''; + + // ExtJS does not has a nice getAllRecords interface for stores :/ + store.queryBy(Ext.returnTrue).each(function(rec) { + var s = rec.get('state'); + if (s && s !== '=') { + var f = rec.get('flag'); + if (flags === '') { + flags = s + f; + } else { + flags += ';' + s + f; + } + } + }); + + flags += me.unkownFlags.join(';'); + + return flags; + }, + + setValue: function(value) { + var me = this; + var store = me.getStore(); + + me.value = value || ''; + + me.unkownFlags = []; + + me.getStore().queryBy(Ext.returnTrue).each(function(rec) { + rec.set('state', '='); + }); + + var flags = value ? value.split(';') : []; + flags.forEach(function(flag) { + var sign = flag.substr(0, 1); + flag = flag.substr(1); + + var rec = store.findRecord('flag', flag, 0, false, true, true); + if (rec !== null) { + rec.set('state', sign); + } else { + me.unkownFlags.push(flag); + } + }); + store.reload(); + + var res = me.mixins.field.setValue.call(me, value); + + return res; + }, + columns: [ + { + dataIndex: 'state', + renderer: function(v) { + switch (v) { + case '=': return 'Default'; + case '-': return 'Off'; + case '+': return 'On'; + default: return 'Unknown'; + } + }, + width: 65, + }, + { + xtype: 'widgetcolumn', + dataIndex: 'state', + width: 95, + onWidgetAttach: function(column, widget, record) { + var val = record.get('state') || '='; + widget.down('[inputValue=' + val + ']').setValue(true); + // TODO: disable if selected CPU model and flag are incompatible + }, + widget: { + xtype: 'radiogroup', + hideLabel: true, + layout: 'hbox', + validateOnChange: false, + value: '=', + listeners: { + change: function(f, value) { + var v = Object.values(value)[0]; + f.getWidgetRecord().set('state', v); + + var view = this.up('grid'); + view.dirty = view.getValue() !== view.originalValue; + view.checkDirty(); + //view.checkChange(); + }, + }, + items: [ + { + boxLabel: '-', + boxLabelAlign: 'before', + inputValue: '-', + isFormField: false, + }, + { + checked: true, + inputValue: '=', + isFormField: false, + }, + { + boxLabel: '+', + inputValue: '+', + isFormField: false, + }, + ], + }, + }, + { + dataIndex: 'flag', + width: 100, + }, + { + dataIndex: 'desc', + cellWrap: true, + flex: 1, + }, + ], + + initComponent: function() { + var me = this; + + // static class store, thus gets not recreated, so ensure defaults are set! + me.getStore().data.forEach(function(v) { + v.state = '='; + }); + + me.value = me.originalValue = ''; + + me.callParent(arguments); + }, +}); +/* filter is a javascript builtin, but extjs calls it also filter */ +Ext.define('PVE.form.VMSelector', { + extend: 'Ext.grid.Panel', + alias: 'widget.vmselector', + + mixins: { + field: 'Ext.form.field.Field', + }, + + allowBlank: true, + selectAll: false, + isFormField: true, + + plugins: 'gridfilters', + + store: { + model: 'PVEResources', + sorters: 'vmid', + }, + + columnsDeclaration: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 80, + filter: { + type: 'number', + }, + }, + { + header: gettext('Node'), + dataIndex: 'node', + }, + { + header: gettext('Status'), + dataIndex: 'status', + filter: { + type: 'list', + }, + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1, + filter: { + type: 'string', + }, + }, + { + header: gettext('Pool'), + dataIndex: 'pool', + filter: { + type: 'list', + }, + }, + { + header: gettext('Type'), + dataIndex: 'type', + width: 120, + renderer: function(value) { + if (value === 'qemu') { + return gettext('Virtual Machine'); + } else if (value === 'lxc') { + return gettext('LXC Container'); + } + + return ''; + }, + filter: { + type: 'list', + store: { + data: [ + { id: 'qemu', text: gettext('Virtual Machine') }, + { id: 'lxc', text: gettext('LXC Container') }, + ], + un: function() { + // Due to EXTJS-18711. we have to do a static list via a store but to avoid + // creating an object, we have to have an empty pseudo un function + }, + }, + }, + }, + { + header: 'HA ' + gettext('Status'), + dataIndex: 'hastate', + flex: 1, + filter: { + type: 'list', + }, + }, + ], + + // should be a list of 'dataIndex' values, if 'undefined' all declared columns will be included + columnSelection: undefined, + + selModel: { + selType: 'checkboxmodel', + mode: 'SIMPLE', + }, + + checkChangeEvents: [ + 'selectionchange', + 'change', + ], + + listeners: { + selectionchange: function() { + // to trigger validity and error checks + this.checkChange(); + }, + }, + + getValue: function() { + var me = this; + if (me.savedValue !== undefined) { + return me.savedValue; + } + var sm = me.getSelectionModel(); + var selection = sm.getSelection(); + var values = []; + var store = me.getStore(); + selection.forEach(function(item) { + // only add if not filtered + if (store.findExact('vmid', item.data.vmid) !== -1) { + values.push(item.data.vmid); + } + }); + return values; + }, + + setValueSelection: function(value) { + let me = this; + + let store = me.getStore(); + let notFound = []; + let selection = value.map(item => { + let found = store.findRecord('vmid', item, 0, false, true, true); + if (!found) { + notFound.push(item); + } + return found; + }).filter(r => r); + + for (const vmid of notFound) { + let rec = store.add({ + vmid, + node: 'unknown', + }); + selection.push(rec[0]); + } + + let sm = me.getSelectionModel(); + if (selection.length) { + sm.select(selection); + } else { + sm.deselectAll(); + } + // to correctly trigger invalid class + me.getErrors(); + }, + + setValue: function(value) { + let me = this; + if (!Ext.isArray(value)) { + value = value.split(','); + } + + let store = me.getStore(); + if (!store.isLoaded()) { + me.savedValue = value; + store.on('load', function() { + me.setValueSelection(value); + delete me.savedValue; + }, { single: true }); + } else { + me.setValueSelection(value); + } + return me.mixins.field.setValue.call(me, value); + }, + + getErrors: function(value) { + let me = this; + if (!me.isDisabled() && me.allowBlank === false && + me.getSelectionModel().getCount() === 0) { + me.addBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']); + return [gettext('No VM selected')]; + } + + me.removeBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']); + return []; + }, + + setDisabled: function(disabled) { + let me = this; + let res = me.callParent([disabled]); + me.getErrors(); + return res; + }, + + initComponent: function() { + let me = this; + + let columns = me.columnsDeclaration.filter((column) => + me.columnSelection ? me.columnSelection.indexOf(column.dataIndex) !== -1 : true, + ).map((x) => x); + + me.columns = columns; + + me.callParent(); + + me.getStore().load({ params: { type: 'vm' } }); + + if (me.nodename) { + me.getStore().addFilter({ + property: 'node', + exactMatch: true, + value: me.nodename, + }); + } + + // only show the relevant guests by default + if (me.action) { + var statusfilter = ''; + switch (me.action) { + case 'startall': + statusfilter = 'stopped'; + break; + case 'stopall': + statusfilter = 'running'; + break; + } + if (statusfilter !== '') { + me.getStore().addFilter([{ + property: 'template', + value: 0, + }, { + id: 'x-gridfilter-status', + operator: 'in', + property: 'status', + value: [statusfilter], + }]); + } + } + + if (me.selectAll) { + me.mon(me.getStore(), 'load', function() { + me.getSelectionModel().selectAll(false); + }); + } + }, +}); + + +Ext.define('PVE.form.VMComboSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.vmComboSelector', + + valueField: 'vmid', + displayField: 'vmid', + + autoSelect: false, + editable: true, + anyMatch: true, + forceSelection: true, + + store: { + model: 'PVEResources', + autoLoad: true, + sorters: 'vmid', + filters: [{ + property: 'type', + value: /lxc|qemu/, + }], + }, + + listConfig: { + width: 600, + plugins: 'gridfilters', + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 80, + filter: { + type: 'number', + }, + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1, + filter: { + type: 'string', + }, + }, + { + header: gettext('Node'), + dataIndex: 'node', + }, + { + header: gettext('Status'), + dataIndex: 'status', + filter: { + type: 'list', + }, + }, + { + header: gettext('Pool'), + dataIndex: 'pool', + hidden: true, + filter: { + type: 'list', + }, + }, + { + header: gettext('Type'), + dataIndex: 'type', + width: 120, + renderer: function(value) { + if (value === 'qemu') { + return gettext('Virtual Machine'); + } else if (value === 'lxc') { + return gettext('LXC Container'); + } + + return ''; + }, + filter: { + type: 'list', + store: { + data: [ + { id: 'qemu', text: gettext('Virtual Machine') }, + { id: 'lxc', text: gettext('LXC Container') }, + ], + un: function() { /* due to EXTJS-18711 */ }, + }, + }, + }, + { + header: 'HA ' + gettext('Status'), + dataIndex: 'hastate', + hidden: true, + flex: 1, + filter: { + type: 'list', + }, + }, + ], + }, +}); +Ext.define('PVE.form.VNCKeyboardSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.VNCKeyboardSelector'], + comboItems: Object.entries(PVE.Utils.kvm_keymaps), +}); +/* + * Top left combobox, used to select a view of the underneath RessourceTree + */ +Ext.define('PVE.form.ViewSelector', { + extend: 'Ext.form.field.ComboBox', + alias: ['widget.pveViewSelector'], + + editable: false, + allowBlank: false, + forceSelection: true, + autoSelect: false, + valueField: 'key', + displayField: 'value', + hideLabel: true, + queryMode: 'local', + + initComponent: function() { + let me = this; + + let default_views = { + server: { + text: gettext('Server View'), + groups: ['node'], + }, + folder: { + text: gettext('Folder View'), + groups: ['type'], + }, + pool: { + text: gettext('Pool View'), + groups: ['pool'], + // Pool View only lists VMs and Containers + filterfn: ({ data }) => data.type === 'qemu' || data.type === 'lxc' || data.type === 'pool', + }, + }; + let groupdef = Object.entries(default_views).map(([name, config]) => [name, config.text]); + + let store = Ext.create('Ext.data.Store', { + model: 'KeyValue', + proxy: { + type: 'memory', + reader: 'array', + }, + data: groupdef, + autoload: true, + }); + + Ext.apply(me, { + store: store, + value: groupdef[0][0], + getViewFilter: function() { + let view = me.getValue(); + return Ext.apply({ id: view }, default_views[view] || default_views.server); + }, + getState: function() { + return { value: me.getValue() }; + }, + applyState: function(state, doSelect) { + let view = me.getValue(); + if (state && state.value && view !== state.value) { + let record = store.findRecord('key', state.value, 0, false, true, true); + if (record) { + me.setValue(state.value, true); + if (doSelect) { + me.fireEvent('select', me, [record]); + } + } + } + }, + stateEvents: ['select'], + stateful: true, + stateId: 'pveview', + id: 'view', + }); + + me.callParent(); + + let statechange = function(sp, key, value) { + if (key === me.id) { + me.applyState(value, true); + } + }; + let sp = Ext.state.Manager.getProvider(); + me.mon(sp, 'statechange', statechange, me); + }, +}); +Ext.define('PVE.form.iScsiProviderSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveiScsiProviderSelector'], + comboItems: [ + ['comstar', 'Comstar'], + ['freenas', 'FreeNAS/TrueNAS API'], + ['istgt', 'istgt'], + ['iet', 'IET'], + ['LIO', 'LIO'], + ], +}); +Ext.define('PVE.form.ColorPicker', { + extend: 'Ext.form.FieldContainer', + alias: 'widget.pveColorPicker', + + defaultBindProperty: 'value', + + config: { + value: null, + }, + + height: 24, + + layout: { + type: 'hbox', + align: 'stretch', + }, + + getValue: function() { + return this.realvalue.slice(1); + }, + + setValue: function(value) { + let me = this; + me.setColor(value); + if (value && value.length === 6) { + me.picker.value = value[0] !== '#' ? `#${value}` : value; + } + }, + + setColor: function(value) { + let me = this; + let oldValue = me.realvalue; + me.realvalue = value; + let color = value.length === 6 ? `#${value}` : undefined; + me.down('#picker').setStyle('background-color', color); + me.down('#text').setValue(value ?? ""); + me.fireEvent('change', me, me.realvalue, oldValue); + }, + + initComponent: function() { + let me = this; + me.picker = document.createElement('input'); + me.picker.type = 'color'; + me.picker.style = `opacity: 0; border: 0px; width: 100%; height: ${me.height}px`; + me.picker.value = `${me.value}`; + + me.items = [ + { + xtype: 'textfield', + itemId: 'text', + minLength: !me.allowBlank ? 6 : undefined, + maxLength: 6, + enforceMaxLength: true, + allowBlank: me.allowBlank, + emptyText: me.allowBlank ? gettext('Automatic') : undefined, + maskRe: /[a-f0-9]/i, + regex: /^[a-f0-9]{6}$/i, + flex: 1, + listeners: { + change: function(field, value) { + me.setValue(value); + }, + }, + }, + { + xtype: 'box', + style: { + 'margin-left': '1px', + border: '1px solid #cfcfcf', + }, + itemId: 'picker', + width: 24, + contentEl: me.picker, + }, + ]; + + me.callParent(); + me.picker.oninput = function() { + me.setColor(me.picker.value.slice(1)); + }; + }, +}); + +Ext.define('PVE.form.TagColorGrid', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveTagColorGrid', + + mixins: [ + 'Ext.form.field.Field', + ], + + allowBlank: true, + selectAll: false, + isFormField: true, + deleteEmpty: false, + selModel: 'checkboxmodel', + + config: { + deleteEmpty: false, + }, + + emptyText: gettext('No Overrides'), + viewConfig: { + deferEmptyText: false, + }, + + setValue: function(value) { + let me = this; + let colors; + if (Ext.isObject(value)) { + colors = value.colors; + } else { + colors = value; + } + if (!colors) { + me.getStore().removeAll(); + me.checkChange(); + return me; + } + let entries = (colors.split(';') || []).map((entry) => { + let [tag, bg, fg] = entry.split(':'); + fg = fg || ""; + return { + tag, + color: bg, + text: fg, + }; + }); + me.getStore().setData(entries); + me.checkChange(); + return me; + }, + + getValue: function() { + let me = this; + let values = []; + me.getStore().each((rec) => { + if (rec.data.tag) { + let val = `${rec.data.tag}:${rec.data.color}`; + if (rec.data.text) { + val += `:${rec.data.text}`; + } + values.push(val); + } + }); + return values.join(';'); + }, + + getErrors: function(value) { + let me = this; + let emptyTag = false; + let notValidColor = false; + let colorRegex = new RegExp(/^[0-9a-f]{6}$/i); + me.getStore().each((rec) => { + if (!rec.data.tag) { + emptyTag = true; + } + if (!rec.data.color?.match(colorRegex)) { + notValidColor = true; + } + if (rec.data.text && !rec.data.text?.match(colorRegex)) { + notValidColor = true; + } + }); + let errors = []; + if (emptyTag) { + errors.push(gettext('Tag must not be empty.')); + } + if (notValidColor) { + errors.push(gettext('Not a valid color.')); + } + return errors; + }, + + // override framework function to implement deleteEmpty behaviour + getSubmitData: function() { + let me = this, + data = null, + val; + if (!me.disabled && me.submitValue) { + val = me.getValue(); + if (val !== null && val !== '') { + data = {}; + data[me.getName()] = val; + } else if (me.getDeleteEmpty()) { + data = {}; + data.delete = me.getName(); + } + } + return data; + }, + + + controller: { + xclass: 'Ext.app.ViewController', + + addLine: function() { + let me = this; + me.getView().getStore().add({ + tag: '', + color: '', + text: '', + }); + }, + + removeSelection: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (selection === undefined) { + return; + } + + selection.forEach((sel) => { + view.getStore().remove(sel); + }); + view.checkChange(); + }, + + tagChange: function(field, newValue, oldValue) { + let me = this; + let rec = field.getWidgetRecord(); + if (!rec) { + return; + } + if (newValue && newValue !== oldValue) { + let newrgb = Proxmox.Utils.stringToRGB(newValue); + let newvalue = Proxmox.Utils.rgbToHex(newrgb); + if (!rec.get('color')) { + rec.set('color', newvalue); + } else if (oldValue) { + let oldrgb = Proxmox.Utils.stringToRGB(oldValue); + let oldvalue = Proxmox.Utils.rgbToHex(oldrgb); + if (rec.get('color') === oldvalue) { + rec.set('color', newvalue); + } + } + } + me.fieldChange(field, newValue, oldValue); + }, + + backgroundChange: function(field, newValue, oldValue) { + let me = this; + let rec = field.getWidgetRecord(); + if (!rec) { + return; + } + if (newValue && newValue !== oldValue) { + let newrgb = Proxmox.Utils.hexToRGB(newValue); + let newcls = Proxmox.Utils.getTextContrastClass(newrgb); + let hexvalue = newcls === 'dark' ? '000000' : 'FFFFFF'; + if (!rec.get('text')) { + rec.set('text', hexvalue); + } else if (oldValue) { + let oldrgb = Proxmox.Utils.hexToRGB(oldValue); + let oldcls = Proxmox.Utils.getTextContrastClass(oldrgb); + let oldvalue = oldcls === 'dark' ? '000000' : 'FFFFFF'; + if (rec.get('text') === oldvalue) { + rec.set('text', hexvalue); + } + } + } + me.fieldChange(field, newValue, oldValue); + }, + + fieldChange: function(field, newValue, oldValue) { + let me = this; + let view = me.getView(); + let rec = field.getWidgetRecord(); + if (!rec) { + return; + } + let column = field.getWidgetColumn(); + rec.set(column.dataIndex, newValue); + view.checkChange(); + }, + }, + + tbar: [ + { + text: gettext('Add'), + handler: 'addLine', + }, + { + xtype: 'proxmoxButton', + text: gettext('Remove'), + handler: 'removeSelection', + disabled: true, + }, + ], + + columns: [ + { + header: 'Tag', + dataIndex: 'tag', + xtype: 'widgetcolumn', + onWidgetAttach: function(col, widget, rec) { + widget.getStore().setData(PVE.UIOptions.tagList.map(v => ({ tag: v }))); + }, + widget: { + xtype: 'combobox', + isFormField: false, + maskRe: PVE.Utils.tagCharRegex, + allowBlank: false, + queryMode: 'local', + displayField: 'tag', + valueField: 'tag', + store: {}, + listeners: { + change: 'tagChange', + }, + }, + flex: 1, + }, + { + header: gettext('Background'), + xtype: 'widgetcolumn', + flex: 1, + dataIndex: 'color', + widget: { + xtype: 'pveColorPicker', + isFormField: false, + listeners: { + change: 'backgroundChange', + }, + }, + }, + { + header: gettext('Text'), + xtype: 'widgetcolumn', + flex: 1, + dataIndex: 'text', + widget: { + xtype: 'pveColorPicker', + allowBlank: true, + isFormField: false, + listeners: { + change: 'fieldChange', + }, + }, + }, + ], + + store: { + listeners: { + update: function() { + this.commitChanges(); + }, + }, + }, + + initComponent: function() { + let me = this; + me.callParent(); + me.initField(); + }, +}); +Ext.define('PVE.form.ListField', { + extend: 'Ext.container.Container', + alias: 'widget.pveListField', + + mixins: [ + 'Ext.form.field.Field', + ], + + // override for column header + fieldTitle: gettext('Item'), + + // will be applied to the textfields + maskRe: undefined, + + allowBlank: true, + selectAll: false, + isFormField: true, + deleteEmpty: false, + config: { + deleteEmpty: false, + }, + + setValue: function(list) { + let me = this; + list = Ext.isArray(list) ? list : (list ?? '').split(';').filter(t => t !== ''); + + let store = me.lookup('grid').getStore(); + if (list.length > 0) { + store.setData(list.map(item => ({ item }))); + } else { + store.removeAll(); + } + me.checkChange(); + return me; + }, + + getValue: function() { + let me = this; + let values = []; + me.lookup('grid').getStore().each((rec) => { + if (rec.data.item) { + values.push(rec.data.item); + } + }); + return values.join(';'); + }, + + getErrors: function(value) { + let me = this; + let empty = false; + me.lookup('grid').getStore().each((rec) => { + if (!rec.data.item) { + empty = true; + } + }); + if (empty) { + return [gettext('Tag must not be empty.')]; + } + return []; + }, + + // override framework function to implement deleteEmpty behaviour + getSubmitData: function() { + let me = this, + data = null, + val; + if (!me.disabled && me.submitValue) { + val = me.getValue(); + if (val !== null && val !== '') { + data = {}; + data[me.getName()] = val; + } else if (me.getDeleteEmpty()) { + data = {}; + data.delete = me.getName(); + } + } + return data; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + addLine: function() { + let me = this; + me.lookup('grid').getStore().add({ + item: '', + }); + }, + + removeSelection: function(field) { + let me = this; + let view = me.getView(); + let grid = me.lookup('grid'); + + let record = field.getWidgetRecord(); + if (record === undefined) { + // this is sometimes called before a record/column is initialized + return; + } + + grid.getStore().remove(record); + view.checkChange(); + view.validate(); + }, + + itemChange: function(field, newValue) { + let rec = field.getWidgetRecord(); + if (!rec) { + return; + } + let column = field.getWidgetColumn(); + rec.set(column.dataIndex, newValue); + let list = field.up('pveListField'); + list.checkChange(); + list.validate(); + }, + + control: { + 'grid button': { + click: 'removeSelection', + }, + }, + }, + + items: [ + { + xtype: 'grid', + reference: 'grid', + + viewConfig: { + deferEmptyText: false, + }, + + store: { + listeners: { + update: function() { + this.commitChanges(); + }, + }, + }, + }, + { + xtype: 'button', + text: gettext('Add'), + iconCls: 'fa fa-plus-circle', + handler: 'addLine', + }, + ], + + initComponent: function() { + let me = this; + + for (const [key, value] of Object.entries(me.gridConfig ?? {})) { + me.items[0][key] = value; + } + + me.items[0].columns = [ + { + header: me.fieldTtitle, + dataIndex: 'item', + xtype: 'widgetcolumn', + widget: { + xtype: 'textfield', + isFormField: false, + maskRe: me.maskRe, + allowBlank: false, + queryMode: 'local', + listeners: { + change: 'itemChange', + }, + }, + flex: 1, + }, + { + xtype: 'widgetcolumn', + width: 40, + widget: { + xtype: 'button', + iconCls: 'fa fa-trash-o', + }, + }, + ]; + + me.callParent(); + me.initField(); + }, +}); +Ext.define('Proxmox.form.Tag', { + extend: 'Ext.Component', + alias: 'widget.pveTag', + + mode: 'editable', + + tag: '', + cls: 'pve-edit-tag', + + tpl: [ + '', + '{tag}', + '', + ], + + // contains tags not to show in the picker and not allowing to set + filter: [], + + updateFilter: function(tags) { + this.filter = tags; + }, + + onClick: function(event) { + let me = this; + if (event.target.tagName === 'I' && !event.target.classList.contains('handle')) { + if (me.mode === 'editable') { + me.destroy(); + return; + } + } else if (event.target.tagName !== 'SPAN' || me.mode !== 'editable') { + return; + } + me.selectText(); + }, + + selectText: function(collapseToEnd) { + let me = this; + let tagEl = me.tagEl(); + tagEl.contentEditable = true; + let range = document.createRange(); + range.selectNodeContents(tagEl); + if (collapseToEnd) { + range.collapse(false); + } + let sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + + me.showPicker(); + }, + + showPicker: function() { + let me = this; + if (!me.picker) { + me.picker = Ext.widget({ + xtype: 'boundlist', + minWidth: 70, + scrollable: true, + floating: true, + hidden: true, + userCls: 'proxmox-tags-full', + displayField: 'tag', + itemTpl: [ + '{[Proxmox.Utils.getTagElement(values.tag, PVE.UIOptions.tagOverrides)]}', + ], + store: [], + listeners: { + select: function(picker, rec) { + me.tagEl().innerHTML = rec.data.tag; + me.setTag(rec.data.tag, true); + me.selectText(true); + me.setColor(rec.data.tag); + me.picker.hide(); + }, + }, + }); + } + me.picker.getStore()?.clearFilter(); + let taglist = PVE.UIOptions.tagList.filter(v => !me.filter.includes(v)).map(v => ({ tag: v })); + if (taglist.length < 1) { + return; + } + me.picker.getStore().setData(taglist); + me.picker.showBy(me, 'tl-bl'); + me.picker.setMaxHeight(200); + }, + + setMode: function(mode) { + let me = this; + let tagEl = me.tagEl(); + if (tagEl) { + tagEl.contentEditable = mode === 'editable'; + } + me.removeCls(me.mode); + me.addCls(mode); + me.mode = mode; + if (me.mode !== 'editable') { + me.picker?.hide(); + } + }, + + onKeyPress: function(event) { + let me = this; + let key = event.browserEvent.key; + switch (key) { + case 'Enter': + case 'Escape': + me.fireEvent('keypress', key); + break; + case 'ArrowLeft': + case 'ArrowRight': + case 'Backspace': + case 'Delete': + return; + default: + if (key.match(PVE.Utils.tagCharRegex)) { + return; + } + me.setTag(me.tagEl().innerHTML); + } + event.browserEvent.preventDefault(); + event.browserEvent.stopPropagation(); + }, + + // for pasting text + beforeInput: function(event) { + let me = this; + me.updateLayout(); + let tag = event.event.data ?? event.event.dataTransfer?.getData('text/plain'); + if (!tag) { + return; + } + if (tag.match(PVE.Utils.tagCharRegex) === null) { + event.event.preventDefault(); + event.event.stopPropagation(); + } + }, + + onInput: function(event) { + let me = this; + me.picker.getStore().filter({ + property: 'tag', + value: me.tagEl().innerHTML, + anyMatch: true, + }); + me.setTag(me.tagEl().innerHTML); + }, + + lostFocus: function(list, event) { + let me = this; + me.picker?.hide(); + window.getSelection().removeAllRanges(); + }, + + setColor: function(tag) { + let me = this; + let rgb = PVE.UIOptions.tagOverrides[tag] ?? Proxmox.Utils.stringToRGB(tag); + + let cls = Proxmox.Utils.getTextContrastClass(rgb); + let color = Proxmox.Utils.rgbToCss(rgb); + me.setUserCls(`proxmox-tag-${cls}`); + me.setStyle('background-color', color); + if (rgb.length > 3) { + let fgcolor = Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]]); + + me.setStyle('color', fgcolor); + } else { + me.setStyle('color'); + } + }, + + setTag: function(tag) { + let me = this; + let oldtag = me.tag; + me.tag = tag; + + clearTimeout(me.colorTimeout); + me.colorTimeout = setTimeout(() => me.setColor(tag), 200); + + me.updateLayout(); + if (oldtag !== tag) { + me.fireEvent('change', me, tag, oldtag); + } + }, + + tagEl: function() { + return this.el?.dom?.getElementsByTagName('span')?.[0]; + }, + + listeners: { + click: 'onClick', + focusleave: 'lostFocus', + keydown: 'onKeyPress', + beforeInput: 'beforeInput', + input: 'onInput', + element: 'el', + scope: 'this', + }, + + initComponent: function() { + let me = this; + + me.data = { + tag: me.tag, + }; + + me.setTag(me.tag); + me.setColor(me.tag); + me.setMode(me.mode ?? 'normal'); + me.callParent(); + }, + + destroy: function() { + let me = this; + if (me.picker) { + Ext.destroy(me.picker); + } + clearTimeout(me.colorTimeout); + me.callParent(); + }, +}); +Ext.define('PVE.panel.TagEditContainer', { + extend: 'Ext.container.Container', + alias: 'widget.pveTagEditContainer', + + layout: { + type: 'hbox', + align: 'middle', + }, + + // set to false to hide the 'no tags' field and the edit button + canEdit: true, + + controller: { + xclass: 'Ext.app.ViewController', + + loadTags: function(tagstring = '', force = false) { + let me = this; + let view = me.getView(); + + if (me.oldTags === tagstring && !force) { + return; + } + + view.suspendLayout = true; + me.forEachTag((tag) => { + view.remove(tag); + }); + me.getViewModel().set('tagCount', 0); + let newtags = tagstring.split(/[;, ]/).filter((t) => !!t) || []; + newtags.forEach((tag) => { + me.addTag(tag); + }); + view.suspendLayout = false; + view.updateLayout(); + if (!force) { + me.oldTags = tagstring; + } + me.tagsChanged(); + }, + + onRender: function(v) { + let me = this; + let view = me.getView(); + view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags()); + + view.dragzone = Ext.create('Ext.dd.DragZone', v.getEl(), { + getDragData: function(e) { + let source = e.getTarget('.handle'); + if (!source) { + return undefined; + } + let sourceId = source.parentNode.id; + let cmp = Ext.getCmp(sourceId); + let ddel = document.createElement('div'); + ddel.classList.add('proxmox-tags-full'); + ddel.innerHTML = Proxmox.Utils.getTagElement(cmp.tag, PVE.UIOptions.tagOverrides); + let repairXY = Ext.fly(source).getXY(); + cmp.setDisabled(true); + ddel.id = Ext.id(); + return { + ddel, + repairXY, + sourceId, + }; + }, + onMouseUp: function(target, e, id) { + let cmp = Ext.getCmp(this.dragData.sourceId); + if (cmp && !cmp.isDestroyed) { + cmp.setDisabled(false); + } + }, + getRepairXY: function() { + return this.dragData.repairXY; + }, + beforeInvalidDrop: function(target, e, id) { + let cmp = Ext.getCmp(this.dragData.sourceId); + if (cmp && !cmp.isDestroyed) { + cmp.setDisabled(false); + } + }, + }); + view.dropzone = Ext.create('Ext.dd.DropZone', v.getEl(), { + getTargetFromEvent: function(e) { + return e.getTarget('.proxmox-tag-dark,.proxmox-tag-light'); + }, + getIndicator: function() { + if (!view.indicator) { + view.indicator = Ext.create('Ext.Component', { + floating: true, + html: '', + hidden: true, + shadow: false, + }); + } + return view.indicator; + }, + onContainerOver: function() { + this.getIndicator().setVisible(false); + }, + notifyOut: function() { + this.getIndicator().setVisible(false); + }, + onNodeOver: function(target, dd, e, data) { + let indicator = this.getIndicator(); + indicator.setVisible(true); + indicator.alignTo(Ext.getCmp(target.id), 't50-bl', [-1, -2]); + return this.dropAllowed; + }, + onNodeDrop: function(target, dd, e, data) { + this.getIndicator().setVisible(false); + let sourceCmp = Ext.getCmp(data.sourceId); + if (!sourceCmp) { + return; + } + sourceCmp.setDisabled(false); + let targetCmp = Ext.getCmp(target.id); + view.remove(sourceCmp, { destroy: false }); + view.insert(view.items.indexOf(targetCmp), sourceCmp); + me.tagsChanged(); + }, + }); + }, + + forEachTag: function(func) { + let me = this; + let view = me.getView(); + view.items.each((field) => { + if (field.getXType() === 'pveTag') { + func(field); + } + return true; + }); + }, + + toggleEdit: function(cancel) { + let me = this; + let vm = me.getViewModel(); + let view = me.getView(); + let editMode = !vm.get('editMode'); + vm.set('editMode', editMode); + + // get a current tag list for editing + if (editMode) { + PVE.UIOptions.update(); + } + + me.forEachTag((tag) => { + tag.setMode(editMode ? 'editable' : 'normal'); + }); + + if (!vm.get('editMode')) { + let tags = []; + if (cancel) { + me.loadTags(me.oldTags, true); + } else { + let toRemove = []; + me.forEachTag((cmp) => { + if (cmp.isVisible() && cmp.tag) { + tags.push(cmp.tag); + } else { + toRemove.push(cmp); + } + }); + toRemove.forEach(cmp => view.remove(cmp)); + tags = tags.join(','); + if (me.oldTags !== tags) { + me.oldTags = tags; + me.loadTags(tags, true); + me.getView().fireEvent('change', tags); + } + } + } + me.getView().updateLayout(); + }, + + tagsChanged: function() { + let me = this; + let tags = []; + me.forEachTag(cmp => { + if (cmp.tag) { + tags.push(cmp.tag); + } + }); + me.getViewModel().set('isDirty', me.oldTags !== tags.join(',')); + me.forEachTag(cmp => { + cmp.updateFilter(tags); + }); + }, + + addTag: function(tag, isNew) { + let me = this; + let view = me.getView(); + let vm = me.getViewModel(); + let index = view.items.length - 5; + if (PVE.UIOptions.shouldSortTags() && !isNew) { + index = view.items.findIndexBy(tagField => { + if (tagField.reference === 'noTagsField') { + return false; + } + if (tagField.xtype !== 'pveTag') { + return true; + } + let a = tagField.tag.toLowerCase(); + let b = tag.toLowerCase(); + return a > b ? true : a < b ? false : tagField.tag.localeCompare(tag) > 0; + }, 1); + } + let tagField = view.insert(index, { + xtype: 'pveTag', + tag, + mode: vm.get('editMode') ? 'editable' : 'normal', + listeners: { + change: 'tagsChanged', + destroy: function() { + vm.set('tagCount', vm.get('tagCount') - 1); + me.tagsChanged(); + }, + keypress: function(key) { + if (key === 'Enter') { + me.editClick(); + } else if (key === 'Escape') { + me.cancelClick(); + } + }, + }, + }); + + if (isNew) { + me.tagsChanged(); + tagField.selectText(); + } + + vm.set('tagCount', vm.get('tagCount') + 1); + }, + + addTagClick: function(event) { + let me = this; + me.lookup('noTagsField').setVisible(false); + me.addTag('', true); + }, + + cancelClick: function() { + this.toggleEdit(true); + }, + + editClick: function() { + this.toggleEdit(false); + }, + + init: function(view) { + let me = this; + if (view.tags) { + me.loadTags(view.tags); + } + me.getViewModel().set('canEdit', view.canEdit); + + me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => { + view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags()); + me.loadTags(me.oldTags, true); // refresh tag colors and order + }); + }, + }, + + viewModel: { + data: { + tagCount: 0, + editMode: false, + canEdit: true, + isDirty: false, + }, + + formulas: { + hideNoTags: function(get) { + return get('tagCount') !== 0 || !get('canEdit'); + }, + hideEditBtn: function(get) { + return get('editMode') || !get('canEdit'); + }, + }, + }, + + loadTags: function() { + return this.getController().loadTags(...arguments); + }, + + items: [ + { + xtype: 'box', + reference: 'noTagsField', + bind: { + hidden: '{hideNoTags}', + }, + html: gettext('No Tags'), + style: { + opacity: 0.5, + }, + }, + { + xtype: 'button', + iconCls: 'fa fa-plus', + tooltip: gettext('Add Tag'), + bind: { + hidden: '{!editMode}', + }, + hidden: true, + margin: '0 8 0 5', + ui: 'default-toolbar', + handler: 'addTagClick', + }, + { + xtype: 'tbseparator', + ui: 'horizontal', + bind: { + hidden: '{!editMode}', + }, + hidden: true, + }, + { + xtype: 'button', + iconCls: 'fa fa-times', + tooltip: gettext('Cancel Edit'), + bind: { + hidden: '{!editMode}', + }, + hidden: true, + margin: '0 5 0 0', + ui: 'default-toolbar', + handler: 'cancelClick', + }, + { + xtype: 'button', + iconCls: 'fa fa-check', + tooltip: gettext('Finish Edit'), + bind: { + hidden: '{!editMode}', + disabled: '{!isDirty}', + }, + hidden: true, + handler: 'editClick', + }, + { + xtype: 'box', + cls: 'pve-tag-inline-button', + html: ``, + bind: { + hidden: '{hideEditBtn}', + }, + listeners: { + click: 'editClick', + element: 'el', + }, + }, + ], + + listeners: { + render: 'onRender', + }, + + destroy: function() { + let me = this; + Ext.destroy(me.dragzone); + Ext.destroy(me.dropzone); + Ext.destroy(me.indicator); + me.callParent(); + }, +}); +Ext.define('PVE.grid.BackupView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveBackupView'], + + onlineHelp: 'chapter_vzdump', + + stateful: true, + stateId: 'grid-guest-backup', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var vmtype = me.pveSelNode.data.type; + if (!vmtype) { + throw "no VM type specified"; + } + + var vmtypeFilter; + if (vmtype === 'lxc' || vmtype === 'openvz') { + vmtypeFilter = function(item) { + return PVE.Utils.volume_is_lxc_backup(item.data.volid, item.data.format); + }; + } else if (vmtype === 'qemu') { + vmtypeFilter = function(item) { + return PVE.Utils.volume_is_qemu_backup(item.data.volid, item.data.format); + }; + } else { + throw "unsupported VM type '" + vmtype + "'"; + } + + var searchFilter = { + property: 'volid', + value: '', + anyMatch: true, + caseSensitive: false, + }; + + var vmidFilter = { + property: 'vmid', + value: vmid, + exactMatch: true, + }; + + me.store = Ext.create('Ext.data.Store', { + model: 'pve-storage-content', + sorters: [ + { + property: 'vmid', + direction: 'ASC', + }, + { + property: 'vdate', + direction: 'DESC', + }, + ], + filters: [ + vmtypeFilter, + searchFilter, + vmidFilter, + ], + }); + + let updateFilter = function() { + me.store.filter([ + vmtypeFilter, + searchFilter, + vmidFilter, + ]); + }; + + const reload = Ext.Function.createBuffered((options) => { + if (me.store) { + me.store.load(options); + } + }, 100); + + let isPBS = false; + var setStorage = function(storage) { + var url = '/api2/json/nodes/' + nodename + '/storage/' + storage + '/content'; + url += '?content=backup'; + + me.store.setProxy({ + type: 'proxmox', + url: url, + }); + + Proxmox.Utils.monStoreErrors(me.view, me.store, true); + + reload(); + }; + + let file_restore_btn; + + var storagesel = Ext.create('PVE.form.StorageSelector', { + nodename: nodename, + fieldLabel: gettext('Storage'), + labelAlign: 'right', + storageContent: 'backup', + allowBlank: false, + listeners: { + change: function(f, value) { + let storage = f.getStore().findRecord('storage', value, 0, false, true, true); + if (storage) { + isPBS = storage.data.type === 'pbs'; + me.getColumns().forEach((column) => { + let id = column.dataIndex; + if (id === 'verification' || id === 'encrypted') { + column.setHidden(!isPBS); + } + }); + } else { + isPBS = false; + } + setStorage(value); + if (file_restore_btn) { + file_restore_btn.setHidden(!isPBS); + } + }, + }, + }); + + var storagefilter = Ext.create('Ext.form.field.Text', { + fieldLabel: gettext('Search'), + labelWidth: 50, + labelAlign: 'right', + enableKeyEvents: true, + value: searchFilter.value, + listeners: { + buffer: 500, + keyup: function(field) { + me.store.clearFilter(true); + searchFilter.value = field.getValue(); + updateFilter(); + }, + }, + }); + + var vmidfilterCB = Ext.create('Ext.form.field.Checkbox', { + boxLabel: gettext('Filter VMID'), + value: '1', + listeners: { + change: function(cb, value) { + vmidFilter.value = value ? vmid : ''; + vmidFilter.exactMatch = !!value; + updateFilter(); + }, + }, + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var backup_btn = Ext.create('Ext.button.Button', { + text: gettext('Backup now'), + handler: function() { + var win = Ext.create('PVE.window.Backup', { + nodename: nodename, + vmid: vmid, + vmtype: vmtype, + storage: storagesel.getValue(), + listeners: { + close: function() { + reload(); + }, + }, + }); + win.show(); + }, + }); + + var restore_btn = Ext.create('Proxmox.button.Button', { + text: gettext('Restore'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return !!rec; + }, + handler: function(b, e, rec) { + let win = Ext.create('PVE.window.Restore', { + nodename: nodename, + vmid: vmid, + volid: rec.data.volid, + volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec), + vmtype: vmtype, + isPBS: isPBS, + }); + win.show(); + win.on('destroy', reload); + }, + }); + + let delete_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + dangerous: true, + delay: 5, + enableFn: rec => !rec?.data?.protected, + confirmMsg: ({ data }) => { + let msg = Ext.String.format( + gettext('Are you sure you want to remove entry {0}'), `'${data.volid}'`); + return msg + " " + gettext('This will permanently erase all data.'); + }, + getUrl: ({ data }) => `/nodes/${nodename}/storage/${storagesel.getValue()}/content/${data.volid}`, + callback: () => reload(), + }); + + let config_btn = Ext.create('Proxmox.button.Button', { + text: gettext('Show Configuration'), + disabled: true, + selModel: sm, + enableFn: rec => !!rec, + handler: function(b, e, rec) { + let storage = storagesel.getValue(); + if (!storage) { + return; + } + Ext.create('PVE.window.BackupConfig', { + volume: rec.data.volid, + pveSelNode: me.pveSelNode, + autoShow: true, + }); + }, + }); + + // declared above so that the storage selector can change this buttons hidden state + file_restore_btn = Ext.create('Proxmox.button.Button', { + text: gettext('File Restore'), + disabled: true, + selModel: sm, + enableFn: rec => !!rec && isPBS, + hidden: !isPBS, + handler: function(b, e, rec) { + let storage = storagesel.getValue(); + let isVMArchive = PVE.Utils.volume_is_qemu_backup(rec.data.volid, rec.data.format); + Ext.create('Proxmox.window.FileBrowser', { + title: gettext('File Restore') + " - " + rec.data.text, + listURL: `/api2/json/nodes/localhost/storage/${storage}/file-restore/list`, + downloadURL: `/api2/json/nodes/localhost/storage/${storage}/file-restore/download`, + extraParams: { + volume: rec.data.volid, + }, + archive: isVMArchive ? 'all' : undefined, + autoShow: true, + }); + }, + }); + + Ext.apply(me, { + selModel: sm, + tbar: { + overflowHandler: 'scroller', + items: [ + backup_btn, + '-', + restore_btn, + file_restore_btn, + config_btn, + { + xtype: 'proxmoxButton', + text: gettext('Edit Notes'), + disabled: true, + handler: function() { + let volid = sm.getSelection()[0].data.volid; + var storage = storagesel.getValue(); + Ext.create('Proxmox.window.Edit', { + autoLoad: true, + width: 600, + height: 400, + resizable: true, + title: gettext('Notes'), + url: `/api2/extjs/nodes/${nodename}/storage/${storage}/content/${volid}`, + layout: 'fit', + items: [ + { + xtype: 'textarea', + layout: 'fit', + name: 'notes', + height: '100%', + }, + ], + listeners: { + destroy: () => reload(), + }, + }).show(); + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Change Protection'), + disabled: true, + handler: function(button, event, record) { + let volid = record.data.volid, storage = storagesel.getValue(); + let url = `/api2/extjs/nodes/${nodename}/storage/${storage}/content/${volid}`; + Proxmox.Utils.API2Request({ + url: url, + method: 'PUT', + waitMsgTarget: me, + params: { + 'protected': record.data.protected ? 0 : 1, + }, + failure: (response) => Ext.Msg.alert('Error', response.htmlStatus), + success: () => { + reload({ + callback: () => sm.fireEvent('selectionchange', sm, [record]), + }); + }, + }); + }, + }, + '-', + delete_btn, + '->', + storagesel, + '-', + vmidfilterCB, + storagefilter, + ], + }, + columns: [ + { + header: gettext('Name'), + flex: 2, + sortable: true, + renderer: PVE.Utils.render_storage_content, + dataIndex: 'volid', + }, + { + header: gettext('Notes'), + dataIndex: 'notes', + flex: 1, + renderer: Ext.htmlEncode, + }, + { + header: ``, + tooltip: gettext('Protected'), + width: 30, + renderer: v => v ? `` : '', + sorter: (a, b) => (b.data.protected || 0) - (a.data.protected || 0), + dataIndex: 'protected', + }, + { + header: gettext('Date'), + width: 150, + dataIndex: 'vdate', + }, + { + header: gettext('Format'), + width: 100, + dataIndex: 'format', + }, + { + header: gettext('Size'), + width: 100, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size', + }, + { + header: gettext('VMID'), + dataIndex: 'vmid', + hidden: true, + }, + { + header: gettext('Encrypted'), + dataIndex: 'encrypted', + renderer: PVE.Utils.render_backup_encryption, + }, + { + header: gettext('Verify State'), + dataIndex: 'verification', + renderer: PVE.Utils.render_backup_verification, + }, + ], + }); + + me.callParent(); + }, +}); +Ext.define('PVE.FirewallAliasEdit', { + extend: 'Proxmox.window.Edit', + + base_url: undefined, + + alias_name: undefined, + + width: 400, + + initComponent: function() { + let me = this; + + me.isCreate = me.alias_name === undefined; + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.alias_name; + me.method = 'PUT'; + } + + let ipanel = Ext.create('Proxmox.panel.InputPanel', { + isCreate: me.isCreate, + items: [ + { + xtype: 'textfield', + name: me.isCreate ? 'name' : 'rename', + fieldLabel: gettext('Name'), + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'cidr', + fieldLabel: gettext('IP/CIDR'), + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment'), + }, + ], + }); + + Ext.apply(me, { + subject: gettext('Alias'), + isAdd: true, + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + let values = response.result.data; + values.rename = values.name; + ipanel.setValues(values); + }, + }); + } + }, +}); + +Ext.define('pve-fw-aliases', { + extend: 'Ext.data.Model', + + fields: ['name', 'cidr', 'comment', 'digest'], + idProperty: 'name', +}); + +Ext.define('PVE.FirewallAliases', { + extend: 'Ext.grid.Panel', + alias: ['widget.pveFirewallAliases'], + + onlineHelp: 'pve_firewall_ip_aliases', + + stateful: true, + stateId: 'grid-firewall-aliases', + + base_url: undefined, + + title: gettext('Alias'), + + initComponent: function() { + let me = this; + + if (!me.base_url) { + throw "missing base_url configuration"; + } + + let store = new Ext.data.Store({ + model: 'pve-fw-aliases', + proxy: { + type: 'proxmox', + url: "/api2/json" + me.base_url, + }, + sorters: { + property: 'name', + direction: 'ASC', + }, + }); + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let reload = function() { + let oldrec = sm.getSelection()[0]; + store.load(function(records, operation, success) { + if (oldrec) { + var rec = store.findRecord('name', oldrec.data.name, 0, false, true, true); + if (rec) { + sm.select(rec); + } + } + }); + }; + + let run_editor = function() { + let rec = me.getSelectionModel().getSelection()[0]; + if (!rec) { + return; + } + let win = Ext.create('PVE.FirewallAliasEdit', { + base_url: me.base_url, + alias_name: rec.data.name, + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + me.addBtn = Ext.create('Ext.Button', { + text: gettext('Add'), + handler: function() { + var win = Ext.create('PVE.FirewallAliasEdit', { + base_url: me.base_url, + }); + win.on('destroy', reload); + win.show(); + }, + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: reload, + }); + + + Ext.apply(me, { + store: store, + tbar: [me.addBtn, me.removeBtn, me.editBtn], + selModel: sm, + columns: [ + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1, + }, + { + header: gettext('IP/CIDR'), + dataIndex: 'cidr', + flex: 1, + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 3, + }, + ], + listeners: { + itemdblclick: run_editor, + }, + }); + + me.callParent(); + me.on('activate', reload); + }, +}); +Ext.define('PVE.FirewallOptions', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveFirewallOptions'], + + fwtype: undefined, // 'dc', 'node' or 'vm' + + base_url: undefined, + + initComponent: function() { + var me = this; + + if (!me.base_url) { + throw "missing base_url configuration"; + } + + if (me.fwtype === 'dc' || me.fwtype === 'node' || me.fwtype === 'vm') { + if (me.fwtype === 'node') { + me.cwidth1 = 250; + } + } else { + throw "unknown firewall option type"; + } + + me.rows = {}; + + var add_boolean_row = function(name, text, defaultValue) { + me.add_boolean_row(name, text, { defaultValue: defaultValue }); + }; + var add_integer_row = function(name, text, minValue, labelWidth) { + me.add_integer_row(name, text, { + minValue: minValue, + deleteEmpty: true, + labelWidth: labelWidth, + renderer: function(value) { + if (value === undefined) { + return Proxmox.Utils.defaultText; + } + + return value; + }, + }); + }; + + var add_log_row = function(name, labelWidth) { + me.rows[name] = { + header: name, + required: true, + defaultValue: 'nolog', + editor: { + xtype: 'proxmoxWindowEdit', + subject: name, + fieldDefaults: { labelWidth: labelWidth || 100 }, + items: { + xtype: 'pveFirewallLogLevels', + name: name, + fieldLabel: name, + }, + }, + }; + }; + + if (me.fwtype === 'node') { + me.rows.enable = { + required: true, + defaultValue: 1, + header: gettext('Firewall'), + renderer: Proxmox.Utils.format_boolean, + editor: { + xtype: 'pveFirewallEnableEdit', + defaultValue: 1, + }, + }; + add_boolean_row('nosmurfs', gettext('SMURFS filter'), 1); + add_boolean_row('tcpflags', gettext('TCP flags filter'), 0); + add_boolean_row('ndp', 'NDP', 1); + add_integer_row('nf_conntrack_max', 'nf_conntrack_max', 32768, 120); + add_integer_row('nf_conntrack_tcp_timeout_established', + 'nf_conntrack_tcp_timeout_established', 7875, 250); + add_log_row('log_level_in'); + add_log_row('log_level_out'); + add_log_row('tcp_flags_log_level', 120); + add_log_row('smurf_log_level'); + } else if (me.fwtype === 'vm') { + me.rows.enable = { + required: true, + defaultValue: 0, + header: gettext('Firewall'), + renderer: Proxmox.Utils.format_boolean, + editor: { + xtype: 'pveFirewallEnableEdit', + defaultValue: 0, + }, + }; + add_boolean_row('dhcp', 'DHCP', 1); + add_boolean_row('ndp', 'NDP', 1); + add_boolean_row('radv', gettext('Router Advertisement'), 0); + add_boolean_row('macfilter', gettext('MAC filter'), 1); + add_boolean_row('ipfilter', gettext('IP filter'), 0); + add_log_row('log_level_in'); + add_log_row('log_level_out'); + } else if (me.fwtype === 'dc') { + add_boolean_row('enable', gettext('Firewall'), 0); + add_boolean_row('ebtables', 'ebtables', 1); + me.rows.log_ratelimit = { + header: gettext('Log rate limit'), + required: true, + defaultValue: gettext('Default') + ' (enable=1,rate1/second,burst=5)', + editor: { + xtype: 'pveFirewallLograteEdit', + defaultValue: 'enable=1', + }, + }; + } + + if (me.fwtype === 'dc' || me.fwtype === 'vm') { + me.rows.policy_in = { + header: gettext('Input Policy'), + required: true, + defaultValue: 'DROP', + editor: { + xtype: 'proxmoxWindowEdit', + subject: gettext('Input Policy'), + items: { + xtype: 'pveFirewallPolicySelector', + name: 'policy_in', + value: 'DROP', + fieldLabel: gettext('Input Policy'), + }, + }, + }; + + me.rows.policy_out = { + header: gettext('Output Policy'), + required: true, + defaultValue: 'ACCEPT', + editor: { + xtype: 'proxmoxWindowEdit', + subject: gettext('Output Policy'), + items: { + xtype: 'pveFirewallPolicySelector', + name: 'policy_out', + value: 'ACCEPT', + fieldLabel: gettext('Output Policy'), + }, + }, + }; + } + + var edit_btn = new Ext.Button({ + text: gettext('Edit'), + disabled: true, + handler: function() { me.run_editor(); }, + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + var rowdef = me.rows[rec.data.key]; + edit_btn.setDisabled(!rowdef.editor); + }; + + Ext.apply(me, { + url: "/api2/json" + me.base_url, + tbar: [edit_btn], + editorConfig: { + url: '/api2/extjs/' + me.base_url, + }, + listeners: { + itemdblclick: me.run_editor, + selectionchange: set_button_status, + }, + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + }, +}); + + +Ext.define('PVE.FirewallLogLevels', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveFirewallLogLevels'], + + name: 'log', + fieldLabel: gettext('Log level'), + value: 'nolog', + comboItems: [['nolog', 'nolog'], ['emerg', 'emerg'], ['alert', 'alert'], + ['crit', 'crit'], ['err', 'err'], ['warning', 'warning'], + ['notice', 'notice'], ['info', 'info'], ['debug', 'debug']], +}); +Ext.define('PVE.form.FWMacroSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveFWMacroSelector', + allowBlank: true, + autoSelect: false, + valueField: 'macro', + displayField: 'macro', + listConfig: { + columns: [ + { + header: gettext('Macro'), + dataIndex: 'macro', + hideable: false, + width: 100, + }, + { + header: gettext('Description'), + renderer: Ext.String.htmlEncode, + flex: 1, + dataIndex: 'descr', + }, + ], + }, + initComponent: function() { + var me = this; + + var store = Ext.create('Ext.data.Store', { + autoLoad: true, + fields: ['macro', 'descr'], + idProperty: 'macro', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/firewall/macros", + }, + sorters: { + property: 'macro', + direction: 'ASC', + }, + }); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.form.ICMPTypeSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveICMPTypeSelector', + allowBlank: true, + autoSelect: false, + valueField: 'name', + displayField: 'name', + listConfig: { + columns: [ + { + header: gettext('Type'), + dataIndex: 'type', + hideable: false, + sortable: false, + width: 50, + }, + { + header: gettext('Name'), + dataIndex: 'name', + hideable: false, + sortable: false, + flex: 1, + }, + ], + }, + setName: function(value) { + this.name = value; + }, +}); + +let ICMP_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', { + field: ['type', 'name'], + data: [ + { type: 'any', name: 'any' }, + { type: '0', name: 'echo-reply' }, + { type: '3', name: 'destination-unreachable' }, + { type: '3/0', name: 'network-unreachable' }, + { type: '3/1', name: 'host-unreachable' }, + { type: '3/2', name: 'protocol-unreachable' }, + { type: '3/3', name: 'port-unreachable' }, + { type: '3/4', name: 'fragmentation-needed' }, + { type: '3/5', name: 'source-route-failed' }, + { type: '3/6', name: 'network-unknown' }, + { type: '3/7', name: 'host-unknown' }, + { type: '3/9', name: 'network-prohibited' }, + { type: '3/10', name: 'host-prohibited' }, + { type: '3/11', name: 'TOS-network-unreachable' }, + { type: '3/12', name: 'TOS-host-unreachable' }, + { type: '3/13', name: 'communication-prohibited' }, + { type: '3/14', name: 'host-precedence-violation' }, + { type: '3/15', name: 'precedence-cutoff' }, + { type: '4', name: 'source-quench' }, + { type: '5', name: 'redirect' }, + { type: '5/0', name: 'network-redirect' }, + { type: '5/1', name: 'host-redirect' }, + { type: '5/2', name: 'TOS-network-redirect' }, + { type: '5/3', name: 'TOS-host-redirect' }, + { type: '8', name: 'echo-request' }, + { type: '9', name: 'router-advertisement' }, + { type: '10', name: 'router-solicitation' }, + { type: '11', name: 'time-exceeded' }, + { type: '11/0', name: 'ttl-zero-during-transit' }, + { type: '11/1', name: 'ttl-zero-during-reassembly' }, + { type: '12', name: 'parameter-problem' }, + { type: '12/0', name: 'ip-header-bad' }, + { type: '12/1', name: 'required-option-missing' }, + { type: '13', name: 'timestamp-request' }, + { type: '14', name: 'timestamp-reply' }, + { type: '17', name: 'address-mask-request' }, + { type: '18', name: 'address-mask-reply' }, + ], +}); +let ICMPV6_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', { + field: ['type', 'name'], + data: [ + { type: '1', name: 'destination-unreachable' }, + { type: '1/0', name: 'no-route' }, + { type: '1/1', name: 'communication-prohibited' }, + { type: '1/2', name: 'beyond-scope' }, + { type: '1/3', name: 'address-unreachable' }, + { type: '1/4', name: 'port-unreachable' }, + { type: '1/5', name: 'failed-policy' }, + { type: '1/6', name: 'reject-route' }, + { type: '2', name: 'packet-too-big' }, + { type: '3', name: 'time-exceeded' }, + { type: '3/0', name: 'ttl-zero-during-transit' }, + { type: '3/1', name: 'ttl-zero-during-reassembly' }, + { type: '4', name: 'parameter-problem' }, + { type: '4/0', name: 'bad-header' }, + { type: '4/1', name: 'unknown-header-type' }, + { type: '4/2', name: 'unknown-option' }, + { type: '128', name: 'echo-request' }, + { type: '129', name: 'echo-reply' }, + { type: '133', name: 'router-solicitation' }, + { type: '134', name: 'router-advertisement' }, + { type: '135', name: 'neighbour-solicitation' }, + { type: '136', name: 'neighbour-advertisement' }, + { type: '137', name: 'redirect' }, + ], +}); + +Ext.define('PVE.FirewallRulePanel', { + extend: 'Proxmox.panel.InputPanel', + + allow_iface: false, + + list_refs_url: undefined, + + onGetValues: function(values) { + var me = this; + + // hack: editable ComboGrid returns nothing when empty, so we need to set '' + // Also, disabled text fields return nothing, so we need to set '' + + Ext.Array.each(['source', 'dest', 'macro', 'proto', 'sport', 'dport', 'icmp-type', 'log'], function(key) { + if (values[key] === undefined) { + values[key] = ''; + } + }); + + delete values.modified_marker; + + return values; + }, + + initComponent: function() { + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + me.column1 = [ + { + // hack: we use this field to mark the form 'dirty' when the + // record has errors- so that the user can safe the unmodified + // form again. + xtype: 'hiddenfield', + name: 'modified_marker', + value: '', + }, + { + xtype: 'proxmoxKVComboBox', + name: 'type', + value: 'in', + comboItems: [['in', 'in'], ['out', 'out']], + fieldLabel: gettext('Direction'), + allowBlank: false, + }, + { + xtype: 'proxmoxKVComboBox', + name: 'action', + value: 'ACCEPT', + comboItems: [['ACCEPT', 'ACCEPT'], ['DROP', 'DROP'], ['REJECT', 'REJECT']], + fieldLabel: gettext('Action'), + allowBlank: false, + }, + ]; + + if (me.allow_iface) { + me.column1.push({ + xtype: 'proxmoxtextfield', + name: 'iface', + deleteEmpty: !me.isCreate, + value: '', + fieldLabel: gettext('Interface'), + }); + } else { + me.column1.push({ + xtype: 'displayfield', + fieldLabel: '', + value: '', + }); + } + + me.column1.push( + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '', + }, + { + xtype: 'pveIPRefSelector', + name: 'source', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('Source'), + maxLength: 512, + maxLengthText: gettext('Too long, consider using IP sets.'), + }, + { + xtype: 'pveIPRefSelector', + name: 'dest', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('Destination'), + maxLength: 512, + maxLengthText: gettext('Too long, consider using IP sets.'), + }, + ); + + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Enable'), + }, + { + xtype: 'pveFWMacroSelector', + name: 'macro', + fieldLabel: gettext('Macro'), + editable: true, + allowBlank: true, + listeners: { + change: function(f, value) { + if (value === null) { + me.down('field[name=proto]').setDisabled(false); + me.down('field[name=sport]').setDisabled(false); + me.down('field[name=dport]').setDisabled(false); + } else { + me.down('field[name=proto]').setDisabled(true); + me.down('field[name=proto]').setValue(''); + me.down('field[name=sport]').setDisabled(true); + me.down('field[name=sport]').setValue(''); + me.down('field[name=dport]').setDisabled(true); + me.down('field[name=dport]').setValue(''); + } + }, + }, + }, + { + xtype: 'pveIPProtocolSelector', + name: 'proto', + autoSelect: false, + editable: true, + value: '', + fieldLabel: gettext('Protocol'), + listeners: { + change: function(f, value) { + if (value === 'icmp' || value === 'icmpv6' || value === 'ipv6-icmp') { + me.down('field[name=dport]').setHidden(true); + me.down('field[name=dport]').setDisabled(true); + if (value === 'icmp') { + me.down('#icmpv4-type').setHidden(false); + me.down('#icmpv4-type').setDisabled(false); + me.down('#icmpv6-type').setHidden(true); + me.down('#icmpv6-type').setDisabled(true); + } else { + me.down('#icmpv6-type').setHidden(false); + me.down('#icmpv6-type').setDisabled(false); + me.down('#icmpv4-type').setHidden(true); + me.down('#icmpv4-type').setDisabled(true); + } + } else { + me.down('#icmpv4-type').setHidden(true); + me.down('#icmpv4-type').setDisabled(true); + me.down('#icmpv6-type').setHidden(true); + me.down('#icmpv6-type').setDisabled(true); + me.down('field[name=dport]').setHidden(false); + me.down('field[name=dport]').setDisabled(false); + } + }, + }, + }, + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '', + }, + { + xtype: 'textfield', + name: 'sport', + value: '', + fieldLabel: gettext('Source port'), + }, + { + xtype: 'textfield', + name: 'dport', + value: '', + fieldLabel: gettext('Dest. port'), + }, + { + xtype: 'pveICMPTypeSelector', + name: 'icmp-type', + id: 'icmpv4-type', + autoSelect: false, + editable: true, + hidden: true, + disabled: true, + value: '', + fieldLabel: gettext('ICMP type'), + store: ICMP_TYPE_NAMES_STORE, + }, + { + xtype: 'pveICMPTypeSelector', + name: 'icmp-type', + id: 'icmpv6-type', + autoSelect: false, + editable: true, + hidden: true, + disabled: true, + value: '', + fieldLabel: gettext('ICMP type'), + store: ICMPV6_TYPE_NAMES_STORE, + }, + ]; + + me.advancedColumn1 = [ + { + xtype: 'pveFirewallLogLevels', + }, + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment'), + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.FirewallRuleEdit', { + extend: 'Proxmox.window.Edit', + + base_url: undefined, + list_refs_url: undefined, + + allow_iface: false, + + initComponent: function() { + var me = this; + + if (!me.base_url) { + throw "no base_url specified"; + } + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + me.isCreate = me.rule_pos === undefined; + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString(); + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.FirewallRulePanel', { + isCreate: me.isCreate, + list_refs_url: me.list_refs_url, + allow_iface: me.allow_iface, + rule_pos: me.rule_pos, + }); + + Ext.apply(me, { + subject: gettext('Rule'), + isAdd: true, + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + // set icmp-type again after protocol has been set + if (values["icmp-type"] !== undefined) { + ipanel.setValues({ "icmp-type": values["icmp-type"] }); + } + if (values.errors) { + var field = me.query('[isFormField][name=modified_marker]')[0]; + field.setValue(1); + Ext.Function.defer(function() { + var form = ipanel.up('form').getForm(); + form.markInvalid(values.errors); + }, 100); + } + }, + }); + } else if (me.rec) { + ipanel.setValues(me.rec.data); + } + }, +}); + +Ext.define('PVE.FirewallGroupRuleEdit', { + extend: 'Proxmox.window.Edit', + + base_url: undefined, + + allow_iface: false, + + initComponent: function() { + var me = this; + + me.isCreate = me.rule_pos === undefined; + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString(); + me.method = 'PUT'; + } + + var column1 = [ + { + xtype: 'hiddenfield', + name: 'type', + value: 'group', + }, + { + xtype: 'pveSecurityGroupsSelector', + name: 'action', + value: '', + fieldLabel: gettext('Security Group'), + allowBlank: false, + }, + ]; + + if (me.allow_iface) { + column1.push({ + xtype: 'proxmoxtextfield', + name: 'iface', + deleteEmpty: !me.isCreate, + value: '', + fieldLabel: gettext('Interface'), + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + isCreate: me.isCreate, + column1: column1, + column2: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Enable'), + }, + ], + columnB: [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment'), + }, + ], + }); + + Ext.apply(me, { + subject: gettext('Rule'), + isAdd: true, + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + }, + }); + } + }, +}); + +Ext.define('PVE.FirewallRules', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveFirewallRules', + + onlineHelp: 'chapter_pve_firewall', + + stateful: true, + stateId: 'grid-firewall-rules', + + base_url: undefined, + list_refs_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + groupBtn: undefined, + + tbar_prefix: undefined, + + allow_groups: true, + allow_iface: false, + + setBaseUrl: function(url) { + var me = this; + + me.base_url = url; + + if (url === undefined) { + me.addBtn.setDisabled(true); + if (me.groupBtn) { + me.groupBtn.setDisabled(true); + } + me.store.removeAll(); + } else { + me.addBtn.setDisabled(false); + me.removeBtn.baseurl = url + '/'; + if (me.groupBtn) { + me.groupBtn.setDisabled(false); + } + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json' + url, + }); + + me.store.load(); + } + }, + + moveRule: function(from, to) { + var me = this; + + if (!me.base_url) { + return; + } + + Proxmox.Utils.API2Request({ + url: me.base_url + "/" + from, + method: 'PUT', + params: { moveto: to }, + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: function() { + me.store.load(); + }, + }); + }, + + updateRule: function(rule) { + var me = this; + + if (!me.base_url) { + return; + } + + rule.enable = rule.enable ? 1 : 0; + + var pos = rule.pos; + delete rule.pos; + delete rule.errors; + + Proxmox.Utils.API2Request({ + url: me.base_url + '/' + pos.toString(), + method: 'PUT', + params: rule, + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: function() { + me.store.load(); + }, + }); + }, + + + initComponent: function() { + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + var store = Ext.create('Ext.data.Store', { + model: 'pve-fw-rule', + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var type = rec.data.type; + + var editor; + if (type === 'in' || type === 'out') { + editor = 'PVE.FirewallRuleEdit'; + } else if (type === 'group') { + editor = 'PVE.FirewallGroupRuleEdit'; + } else { + return; + } + + var win = Ext.create(editor, { + digest: rec.data.digest, + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url, + rule_pos: rec.data.pos, + }); + + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + me.addBtn = Ext.create('Ext.Button', { + text: gettext('Add'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.FirewallRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url, + }); + win.on('destroy', reload); + win.show(); + }, + }); + + var run_copy_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + let type = rec.data.type; + if (!(type === 'in' || type === 'out')) { + return; + } + + let win = Ext.create('PVE.FirewallRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url, + rec: rec, + }); + win.show(); + win.on('destroy', reload); + }; + + me.copyBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Copy'), + selModel: sm, + enableFn: ({ data }) => data.type === 'in' || data.type === 'out', + disabled: true, + handler: run_copy_editor, + }); + + if (me.allow_groups) { + me.groupBtn = Ext.create('Ext.Button', { + text: gettext('Insert') + ': ' + + gettext('Security Group'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.FirewallGroupRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url, + }); + win.on('destroy', reload); + win.show(); + }, + }); + } + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + confirmMsg: false, + getRecordName: function(rec) { + var rule = rec.data; + return rule.pos.toString() + + '?digest=' + encodeURIComponent(rule.digest); + }, + callback: function() { + me.store.load(); + }, + }); + + let tbar = me.tbar_prefix ? [me.tbar_prefix] : []; + tbar.push(me.addBtn, me.copyBtn); + if (me.groupBtn) { + tbar.push(me.groupBtn); + } + tbar.push(me.removeBtn, me.editBtn); + + let render_errors = function(name, value, metaData, record) { + let errors = record.data.errors; + if (errors && errors[name]) { + metaData.tdCls = 'proxmox-invalid-row'; + let html = '

' + Ext.htmlEncode(errors[name]) + '

'; + metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + html + '"'; + } + return value; + }; + + let columns = [ + { + // similar to xtype: 'rownumberer', + dataIndex: 'pos', + resizable: false, + minWidth: 65, + maxWidth: 83, + flex: 1, + sortable: false, + hideable: false, + menuDisabled: true, + renderer: function(value, metaData, record, rowIdx, colIdx) { + metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special'; + let dragHandle = ""; + if (value >= 0) { + return dragHandle + value; + } + return dragHandle; + }, + }, + { + xtype: 'checkcolumn', + header: gettext('On'), + dataIndex: 'enable', + listeners: { + checkchange: function(column, recordIndex, checked) { + var record = me.getStore().getData().items[recordIndex]; + record.commit(); + var data = {}; + Ext.Array.forEach(record.getFields(), function(field) { + data[field.name] = record.get(field.name); + }); + if (!me.allow_iface || !data.iface) { + delete data.iface; + } + me.updateRule(data); + }, + }, + width: 40, + }, + { + header: gettext('Type'), + dataIndex: 'type', + renderer: function(value, metaData, record) { + return render_errors('type', value, metaData, record); + }, + minWidth: 60, + maxWidth: 80, + flex: 2, + }, + { + header: gettext('Action'), + dataIndex: 'action', + renderer: function(value, metaData, record) { + return render_errors('action', value, metaData, record); + }, + minWidth: 80, + maxWidth: 200, + flex: 2, + }, + { + header: gettext('Macro'), + dataIndex: 'macro', + renderer: function(value, metaData, record) { + return render_errors('macro', value, metaData, record); + }, + minWidth: 80, + flex: 2, + }, + ]; + + if (me.allow_iface) { + columns.push({ + header: gettext('Interface'), + dataIndex: 'iface', + renderer: function(value, metaData, record) { + return render_errors('iface', value, metaData, record); + }, + minWidth: 80, + flex: 2, + }); + } + + columns.push( + { + header: gettext('Protocol'), + dataIndex: 'proto', + renderer: function(value, metaData, record) { + return render_errors('proto', value, metaData, record); + }, + width: 75, + }, + { + header: gettext('Source'), + dataIndex: 'source', + renderer: function(value, metaData, record) { + return render_errors('source', value, metaData, record); + }, + minWidth: 100, + flex: 2, + }, + { + header: gettext('S.Port'), + dataIndex: 'sport', + renderer: function(value, metaData, record) { + return render_errors('sport', value, metaData, record); + }, + width: 75, + }, + { + header: gettext('Destination'), + dataIndex: 'dest', + renderer: function(value, metaData, record) { + return render_errors('dest', value, metaData, record); + }, + minWidth: 100, + flex: 2, + }, + { + header: gettext('D.Port'), + dataIndex: 'dport', + renderer: function(value, metaData, record) { + return render_errors('dport', value, metaData, record); + }, + width: 75, + }, + { + header: gettext('Log level'), + dataIndex: 'log', + renderer: function(value, metaData, record) { + return render_errors('log', value, metaData, record); + }, + width: 100, + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + flex: 10, + minWidth: 75, + renderer: function(value, metaData, record) { + let comment = render_errors('comment', Ext.util.Format.htmlEncode(value), metaData, record) || ''; + if (comment.length * 12 > metaData.column.cellWidth) { + comment = `${comment}`; + } + return comment; + }, + }, + ); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + plugins: [ + { + ptype: 'gridviewdragdrop', + dragGroup: 'FWRuleDDGroup', + dropGroup: 'FWRuleDDGroup', + }, + ], + listeners: { + beforedrop: function(node, data, dropRec, dropPosition) { + if (!dropRec) { + return false; // empty view + } + let moveto = dropRec.get('pos'); + if (dropPosition === 'after') { + moveto++; + } + let pos = data.records[0].get('pos'); + me.moveRule(pos, moveto); + return 0; + }, + itemdblclick: run_editor, + }, + }, + sortableColumns: false, + columns: columns, + }); + + me.callParent(); + + if (me.base_url) { + me.setBaseUrl(me.base_url); // load + } + }, +}, function() { + Ext.define('pve-fw-rule', { + extend: 'Ext.data.Model', + fields: [ + { name: 'enable', type: 'boolean' }, + 'type', + 'action', + 'macro', + 'source', + 'dest', + 'proto', + 'iface', + 'dport', + 'sport', + 'comment', + 'pos', + 'digest', + 'errors', + ], + idProperty: 'pos', + }); +}); +Ext.define('PVE.pool.AddVM', { + extend: 'Proxmox.window.Edit', + width: 600, + height: 400, + isAdd: true, + isCreate: true, + initComponent: function() { + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + me.url = "/pools/" + me.pool; + me.method = 'PUT'; + + var vmsField = Ext.create('Ext.form.field.Text', { + name: 'vms', + hidden: true, + allowBlank: false, + }); + + var vmStore = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: [ + { + property: 'vmid', + direction: 'ASC', + }, + ], + filters: [ + function(item) { + return (item.data.type === 'lxc' || item.data.type === 'qemu') && item.data.pool === ''; + }, + ], + }); + + var vmGrid = Ext.create('widget.grid', { + store: vmStore, + border: true, + height: 300, + scrollable: true, + selModel: { + selType: 'checkboxmodel', + mode: 'SIMPLE', + listeners: { + selectionchange: function(model, selected, opts) { + var selectedVms = []; + selected.forEach(function(vm) { + selectedVms.push(vm.data.vmid); + }); + vmsField.setValue(selectedVms); + }, + }, + }, + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 60, + }, + { + header: gettext('Node'), + dataIndex: 'node', + }, + { + header: gettext('Status'), + dataIndex: 'uptime', + renderer: function(value) { + if (value) { + return Proxmox.Utils.runningText; + } else { + return Proxmox.Utils.stoppedText; + } + }, + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1, + }, + { + header: gettext('Type'), + dataIndex: 'type', + }, + ], + }); + Ext.apply(me, { + subject: gettext('Virtual Machine'), + items: [vmsField, vmGrid], + }); + + me.callParent(); + vmStore.load(); + }, +}); + +Ext.define('PVE.pool.AddStorage', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + me.isCreate = true; + me.isAdd = true; + me.url = "/pools/" + me.pool; + me.method = 'PUT'; + + Ext.apply(me, { + subject: gettext('Storage'), + width: 350, + items: [ + { + xtype: 'pveStorageSelector', + name: 'storage', + nodename: 'localhost', + autoSelect: false, + value: '', + fieldLabel: gettext("Storage"), + }, + ], + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.grid.PoolMembers', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pvePoolMembers'], + + // fixme: dynamic status update ? + + stateful: true, + stateId: 'grid-pool-members', + + initComponent: function() { + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + var store = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: [ + { + property: 'type', + direction: 'ASC', + }, + ], + proxy: { + type: 'proxmox', + root: 'data.members', + url: "/api2/json/pools/" + me.pool, + }, + }); + + var coldef = PVE.data.ResourceStore.defaultColumns().filter((c) => + c.dataIndex !== 'tags' && c.dataIndex !== 'lock', + ); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: function(rec) { + return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.id + "'"); + }, + handler: function(btn, event, rec) { + var params = { 'delete': 1 }; + if (rec.data.type === 'storage') { + params.storage = rec.data.storage; + } else if (rec.data.type === 'qemu' || rec.data.type === 'lxc' || rec.data.type === 'openvz') { + params.vms = rec.data.vmid; + } else { + throw "unknown resource type"; + } + + Proxmox.Utils.API2Request({ + url: '/pools/' + me.pool, + method: 'PUT', + params: params, + waitMsgTarget: me, + callback: function() { + reload(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Virtual Machine'), + iconCls: 'pve-itype-icon-qemu', + handler: function() { + var win = Ext.create('PVE.pool.AddVM', { pool: me.pool }); + win.on('destroy', reload); + win.show(); + }, + }, + { + text: gettext('Storage'), + iconCls: 'pve-itype-icon-storage', + handler: function() { + var win = Ext.create('PVE.pool.AddStorage', { pool: me.pool }); + win.on('destroy', reload); + win.show(); + }, + }, + ], + }), + }, + remove_btn, + ], + viewConfig: { + stripeRows: true, + }, + columns: coldef, + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + itemdblclick: function(v, record) { + var ws = me.up('pveStdWorkspace'); + ws.selectById(record.data.id); + }, + activate: reload, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.window.ReplicaEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveReplicaEdit', + + subject: gettext('Replication Job'), + + + url: '/cluster/replication', + method: 'POST', + + initComponent: function() { + var me = this; + + var vmid = me.pveSelNode.data.vmid; + var nodename = me.pveSelNode.data.node; + + var items = []; + + items.push({ + xtype: me.isCreate && !vmid?'pveGuestIDSelector':'displayfield', + name: 'guest', + fieldLabel: 'CT/VM ID', + value: vmid || '', + }); + + items.push( + { + xtype: me.isCreate ? 'pveNodeSelector':'displayfield', + name: 'target', + disallowedNodes: [nodename], + allowBlank: false, + onlineValidator: true, + fieldLabel: gettext("Target"), + }, + { + xtype: 'pveCalendarEvent', + fieldLabel: gettext('Schedule'), + emptyText: '*/15 - ' + Ext.String.format(gettext('Every {0} minutes'), 15), + name: 'schedule', + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Rate limit') + ' (MB/s)', + step: 1, + minValue: 1, + emptyText: gettext('unlimited'), + name: 'rate', + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment', + }, + { + xtype: 'proxmoxcheckbox', + name: 'enabled', + defaultValue: 'on', + checked: true, + fieldLabel: gettext('Enabled'), + }, + ); + + me.items = [ + { + xtype: 'inputpanel', + itemId: 'ipanel', + onlineHelp: 'pvesr_schedule_time_format', + + onGetValues: function(values) { + let win = this.up('window'); + + values.disable = values.enabled ? 0 : 1; + delete values.enabled; + + PVE.Utils.delete_if_default(values, 'rate', '', win.isCreate); + PVE.Utils.delete_if_default(values, 'disable', 0, win.isCreate); + PVE.Utils.delete_if_default(values, 'schedule', '*/15', win.isCreate); + PVE.Utils.delete_if_default(values, 'comment', '', win.isCreate); + + if (win.isCreate) { + values.type = 'local'; + let vm = vmid || values.guest; + let id = -1; + if (win.highestids[vm] !== undefined) { + id = win.highestids[vm]; + } + id++; + values.id = vm + '-' + id.toString(); + delete values.guest; + } + return values; + }, + items: items, + }, + ]; + + me.callParent(); + + if (me.isCreate) { + me.load({ + success: function(response) { + var jobs = response.result.data; + var highestids = {}; + Ext.Array.forEach(jobs, function(job) { + var match = /^([0-9]+)-([0-9]+)$/.exec(job.id); + if (match) { + let jobVMID = parseInt(match[1], 10); + let id = parseInt(match[2], 10); + if (highestids[jobVMID] === undefined || highestids[jobVMID] < id) { + highestids[jobVMID] = id; + } + } + }); + me.highestids = highestids; + }, + }); + } else { + me.load({ + success: function(response, options) { + response.result.data.enabled = !response.result.data.disable; + me.setValues(response.result.data); + me.digest = response.result.data.digest; + }, + }); + } + }, +}); + +/* callback is a function and string */ +Ext.define('PVE.grid.ReplicaView', { + extend: 'Ext.grid.Panel', + xtype: 'pveReplicaView', + + onlineHelp: 'chapter_pvesr', + + stateful: true, + stateId: 'grid-pve-replication-status', + + controller: { + xclass: 'Ext.app.ViewController', + + addJob: function(button, event, rec) { + let me = this; + let view = me.getView(); + Ext.create('PVE.window.ReplicaEdit', { + isCreate: true, + method: 'POST', + pveSelNode: view.pveSelNode, + listeners: { + destroy: () => me.reload(), + }, + autoShow: true, + }); + }, + + editJob: function(button, event, { data }) { + let me = this; + let view = me.getView(); + Ext.create('PVE.window.ReplicaEdit', { + url: `/cluster/replication/${data.id}`, + method: 'PUT', + pveSelNode: view.pveSelNode, + listeners: { + destroy: () => me.reload(), + }, + autoShow: true, + }); + }, + + scheduleJobNow: function(button, event, rec) { + let me = this; + let view = me.getView(); + Proxmox.Utils.API2Request({ + url: `/api2/extjs/nodes/${view.nodename}/replication/${rec.data.id}/schedule_now`, + method: 'POST', + waitMsgTarget: view, + callback: () => me.reload(), + failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + }, + + showLog: function(button, event, rec) { + let me = this; + let view = this.getView(); + + let logView = Ext.create('Proxmox.panel.LogView', { + border: false, + url: `/api2/extjs/nodes/${view.nodename}/replication/${rec.data.id}/log`, + }); + let task = Ext.TaskManager.newTask({ + run: () => logView.requestUpdate(), + interval: 1000, + }); + let win = Ext.create('Ext.window.Window', { + items: [logView], + layout: 'fit', + width: 800, + height: 400, + modal: true, + title: gettext("Replication Log"), + listeners: { + destroy: function() { + task.stop(); + me.reload(); + }, + }, + }); + task.start(); + win.show(); + }, + + reload: function() { + this.getView().rstore.load(); + }, + + dblClick: function(grid, record, item) { + this.editJob(undefined, undefined, record); + }, + + // currently replication is for cluster only, so disable the whole component for non-cluster + checkPrerequisites: function() { + let view = this.getView(); + if (PVE.data.ResourceStore.getNodes().length < 2) { + view.mask(gettext("Replication needs at least two nodes"), ['pve-static-mask']); + } + }, + + control: { + '#': { + itemdblclick: 'dblClick', + afterlayout: 'checkPrerequisites', + }, + }, + }, + + tbar: [ + { + text: gettext('Add'), + itemId: 'addButton', + handler: 'addJob', + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + itemId: 'editButton', + handler: 'editJob', + disabled: true, + }, + { + xtype: 'proxmoxStdRemoveButton', + itemId: 'removeButton', + baseurl: '/api2/extjs/cluster/replication/', + dangerous: true, + callback: 'reload', + }, + { + xtype: 'proxmoxButton', + text: gettext('Log'), + itemId: 'logButton', + handler: 'showLog', + disabled: true, + }, + { + xtype: 'proxmoxButton', + text: gettext('Schedule now'), + itemId: 'scheduleNowButton', + handler: 'scheduleJobNow', + disabled: true, + }, + ], + + initComponent: function() { + var me = this; + var mode = ''; + var url = '/cluster/replication'; + + me.nodename = me.pveSelNode.data.node; + me.vmid = me.pveSelNode.data.vmid; + + me.columns = [ + { + header: gettext('Enabled'), + width: 80, + dataIndex: 'enabled', + align: 'center', + // TODO: switch to Proxmox.Utils.renderEnabledIcon once available + renderer: enabled => ``, + sortable: true, + }, + { + text: 'ID', + dataIndex: 'id', + width: 60, + hidden: true, + }, + { + text: gettext('Guest'), + dataIndex: 'guest', + width: 75, + }, + { + text: gettext('Job'), + dataIndex: 'jobnum', + width: 60, + }, + { + text: gettext('Target'), + dataIndex: 'target', + }, + ]; + + if (!me.nodename) { + mode = 'dc'; + me.stateId = 'grid-pve-replication-dc'; + } else if (!me.vmid) { + mode = 'node'; + url = `/nodes/${me.nodename}/replication`; + } else { + mode = 'vm'; + url = `/nodes/${me.nodename}/replication?guest=${me.vmid}`; + } + + if (mode !== 'dc') { + me.columns.push( + { + text: gettext('Status'), + dataIndex: 'state', + minWidth: 160, + flex: 1, + renderer: function(value, metadata, record) { + if (record.data.pid) { + metadata.tdCls = 'x-grid-row-loading'; + return ''; + } + + let icons = [], states = []; + + if (record.data.remove_job) { + icons.push(''); + states.push(gettext("Removal Scheduled")); + } + if (record.data.error) { + icons.push(''); + states.push(record.data.error); + } + if (icons.length === 0) { + icons.push(''); + states.push(gettext('OK')); + } + + return icons.join(',') + ' ' + states.join(','); + }, + }, + { + text: gettext('Last Sync'), + dataIndex: 'last_sync', + width: 150, + renderer: function(value, metadata, record) { + if (!value) { + return '-'; + } + if (record.data.pid) { + return gettext('syncing'); + } + return Proxmox.Utils.render_timestamp(value); + }, + }, + { + text: gettext('Duration'), + dataIndex: 'duration', + width: 60, + renderer: Proxmox.Utils.render_duration, + }, + { + text: gettext('Next Sync'), + dataIndex: 'next_sync', + width: 150, + renderer: function(value) { + if (!value) { + return '-'; + } + + let now = new Date(), next = new Date(value * 1000); + if (next < now) { + return gettext('pending'); + } + return Proxmox.Utils.render_timestamp(value); + }, + }, + ); + } + + me.columns.push( + { + text: gettext('Schedule'), + width: 75, + dataIndex: 'schedule', + }, + { + text: gettext('Rate limit'), + dataIndex: 'rate', + renderer: function(value) { + if (!value) { + return gettext('unlimited'); + } + + return value.toString() + ' MB/s'; + }, + hidden: true, + }, + { + text: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.htmlEncode, + }, + ); + + me.rstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'pve-replica-' + me.nodename + me.vmid, + model: mode === 'dc'? 'pve-replication' : 'pve-replication-state', + interval: 3000, + proxy: { + type: 'proxmox', + url: "/api2/json" + url, + }, + }); + + me.store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + sorters: [ + { + property: 'guest', + }, + { + property: 'jobnum', + }, + ], + }); + + me.callParent(); + + // we cannot access the log and scheduleNow button + // in the datacenter, because + // we do not know where/if the jobs runs + if (mode === 'dc') { + me.down('#logButton').setHidden(true); + me.down('#scheduleNowButton').setHidden(true); + } + + // if we set the warning mask, we do not want to load + // or set the mask on store errors + if (PVE.data.ResourceStore.getNodes().length < 2) { + return; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + me.on('destroy', me.rstore.stopUpdate); + me.rstore.startUpdate(); + }, +}, function() { + Ext.define('pve-replication', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'target', 'comment', 'rate', 'type', + { name: 'guest', type: 'integer' }, + { name: 'jobnum', type: 'integer' }, + { name: 'schedule', defaultValue: '*/15' }, + { name: 'disable', defaultValue: '' }, + { name: 'enabled', calculate: function(data) { return !data.disable; } }, + ], + }); + + Ext.define('pve-replication-state', { + extend: 'pve-replication', + fields: [ + 'last_sync', 'next_sync', 'error', 'duration', 'state', + 'fail_count', 'remove_job', 'pid', + ], + }); +}); +Ext.define('PVE.grid.ResourceGrid', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveResourceGrid'], + + border: false, + defaultSorter: { + property: 'type', + direction: 'ASC', + }, + userCls: 'proxmox-tags-full', + initComponent: function() { + let me = this; + + let rstore = PVE.data.ResourceStore; + + let store = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: me.defaultSorter, + proxy: { + type: 'memory', + }, + }); + + let textfilter = ''; + let textfilterMatch = function(item) { + for (const field of ['name', 'storage', 'node', 'type', 'text']) { + let v = item.data[field]; + if (v && v.toLowerCase().indexOf(textfilter) >= 0) { + return true; + } + } + return false; + }; + + let updateGrid = function() { + var filterfn = me.viewFilter ? me.viewFilter.filterfn : null; + + store.suspendEvents(); + + let nodeidx = {}; + let gather_child_nodes; + gather_child_nodes = function(node) { + if (!node || !node.childNodes) { + return; + } + for (let child of node.childNodes) { + let orgNode = rstore.data.get(child.data.id); + if (orgNode) { + if ((!filterfn || filterfn(child)) && (!textfilter || textfilterMatch(child))) { + nodeidx[child.data.id] = orgNode; + } + } + gather_child_nodes(child); + } + }; + gather_child_nodes(me.pveSelNode); + + // remove vanished items + let rmlist = []; + store.each(olditem => { + if (!nodeidx[olditem.data.id]) { + rmlist.push(olditem); + } + }); + if (rmlist.length) { + store.remove(rmlist); + } + + // add new items + let addlist = []; + for (const [_key, item] of Object.entries(nodeidx)) { + // getById() use find(), which is slow (ExtJS4 DP5) + let olditem = store.data.get(item.data.id); + if (!olditem) { + addlist.push(item); + continue; + } + let changes = false; + for (let field of PVE.data.ResourceStore.fieldNames) { + if (field !== 'id' && item.data[field] !== olditem.data[field]) { + changes = true; + olditem.beginEdit(); + olditem.set(field, item.data[field]); + } + } + if (changes) { + olditem.endEdit(true); + olditem.commit(true); + } + } + if (addlist.length) { + store.add(addlist); + } + store.sort(); + store.resumeEvents(); + store.fireEvent('refresh', store); + }; + + Ext.apply(me, { + store: store, + stateful: true, + stateId: 'grid-resource', + tbar: [ + '->', + gettext('Search') + ':', ' ', + { + xtype: 'textfield', + width: 200, + value: textfilter, + enableKeyEvents: true, + listeners: { + buffer: 500, + keyup: function(field, e) { + textfilter = field.getValue().toLowerCase(); + updateGrid(); + }, + }, + }, + ], + viewConfig: { + stripeRows: true, + }, + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + itemdblclick: function(v, record) { + var ws = me.up('pveStdWorkspace'); + ws.selectById(record.data.id); + }, + afterrender: function() { + updateGrid(); + }, + }, + columns: rstore.defaultColumns(), + }); + me.callParent(); + me.mon(rstore, 'load', () => updateGrid()); + }, +}); +/* + * Base class for all the multitab config panels + * + * How to use this: + * + * You create a subclass of this, and then define your wanted tabs + * as items like this: + * + * items: [{ + * title: "myTitle", + * xytpe: "somextype", + * iconCls: 'fa fa-icon', + * groups: ['somegroup'], + * expandedOnInit: true, + * itemId: 'someId' + * }] + * + * this has to be in the declarative syntax, else we + * cannot save them for later + * (so no Ext.create or Ext.apply of an item in the subclass) + * + * the groups array expects the itemids of the items + * which are the parents, which have to come before they + * are used + * + * if you want following the tree: + * + * Option1 + * Option2 + * -> SubOption1 + * -> SubSubOption1 + * + * the suboption1 group array has to look like this: + * groups: ['itemid-of-option2'] + * + * and of subsuboption1: + * groups: ['itemid-of-option2', 'itemid-of-suboption1'] + * + * setting the expandedOnInit determines if the item/group is expanded + * initially (false by default) + */ +Ext.define('PVE.panel.Config', { + extend: 'Ext.panel.Panel', + alias: 'widget.pvePanelConfig', + + showSearch: true, // add a resource grid with a search button as first tab + viewFilter: undefined, // a filter to pass to that resource grid + + tbarSpacing: true, // if true, adds a spacer after the title in tbar + + dockedItems: [{ + // this is needed for the overflow handler + xtype: 'toolbar', + overflowHandler: 'scroller', + dock: 'left', + style: { + padding: 0, + margin: 0, + }, + cls: 'pve-toolbar-bg', + items: { + xtype: 'treelist', + itemId: 'menu', + ui: 'pve-nav', + expanderOnly: true, + expanderFirst: false, + animation: false, + singleExpand: false, + listeners: { + selectionchange: function(treeList, selection) { + if (!selection) { + return; + } + let view = this.up('panel'); + view.suspendLayout = true; + view.activateCard(selection.data.id); + view.suspendLayout = false; + view.updateLayout(); + }, + itemclick: function(treelist, info) { + var olditem = treelist.getSelection(); + var newitem = info.node; + + // when clicking on the expand arrow, we don't select items, but still want the original behaviour + if (info.select === false) { + return; + } + + // click on a different, open item then leave it open, else toggle the clicked item + if (olditem.data.id !== newitem.data.id && + newitem.data.expanded === true) { + info.toggle = false; + } else { + info.toggle = true; + } + }, + }, + }, + }, + { + xtype: 'toolbar', + itemId: 'toolbar', + dock: 'top', + height: 36, + overflowHandler: 'scroller', + }], + + firstItem: '', + layout: 'card', + border: 0, + + // used for automated test + selectById: function(cardid) { + var me = this; + + var root = me.store.getRoot(); + var selection = root.findChild('id', cardid, true); + + if (selection) { + selection.expand(); + var menu = me.down('#menu'); + menu.setSelection(selection); + return cardid; + } + return ''; + }, + + activateCard: function(cardid) { + var me = this; + if (me.savedItems[cardid]) { + var curcard = me.getLayout().getActiveItem(); + var newcard = me.add(me.savedItems[cardid]); + me.helpButton.setOnlineHelp(newcard.onlineHelp || me.onlineHelp); + if (curcard) { + me.setActiveItem(cardid); + me.remove(curcard, true); + + // trigger state change + + var ncard = cardid; + // Note: '' is alias for first tab. + // First tab can be 'search' or something else + if (cardid === me.firstItem) { + ncard = ''; + } + if (me.hstateid) { + me.sp.set(me.hstateid, { value: ncard }); + } + } + } + }, + + initComponent: function() { + var me = this; + + var stateid = me.hstateid; + + me.sp = Ext.state.Manager.getProvider(); + + var activeTab; // leaving this undefined means items[0] will be the default tab + + if (stateid) { + let state = me.sp.get(stateid); + if (state && state.value) { + // if this tab does not exist, it chooses the first + activeTab = state.value; + } + } + + // get title + var title = me.title || me.pveSelNode.data.text; + me.title = undefined; + + // create toolbar + var tbar = me.tbar || []; + me.tbar = undefined; + + if (!me.onlineHelp) { + // use the onlineHelp property indirection to enforce checking reference validity + let typeToOnlineHelp = { + 'type/lxc': { onlineHelp: 'chapter_pct' }, + 'type/node': { onlineHelp: 'chapter_system_administration' }, + 'type/pool': { onlineHelp: 'pveum_pools' }, + 'type/qemu': { onlineHelp: 'chapter_virtual_machines' }, + 'type/sdn': { onlineHelp: 'chapter_pvesdn' }, + 'type/storage': { onlineHelp: 'chapter_storage' }, + }; + me.onlineHelp = typeToOnlineHelp[me.pveSelNode.data.id]?.onlineHelp; + } + + if (me.tbarSpacing) { + tbar.unshift('->'); + } + tbar.unshift({ + xtype: 'tbtext', + text: title, + baseCls: 'x-panel-header-text', + }); + + me.helpButton = Ext.create('Proxmox.button.Help', { + hidden: false, + listenToGlobalEvent: false, + onlineHelp: me.onlineHelp || undefined, + }); + + tbar.push(me.helpButton); + + me.dockedItems[1].items = tbar; + + // include search tab + me.items = me.items || []; + if (me.showSearch) { + me.items.unshift({ + xtype: 'pveResourceGrid', + itemId: 'search', + title: gettext('Search'), + iconCls: 'fa fa-search', + pveSelNode: me.pveSelNode, + }); + } + + me.savedItems = {}; + if (me.items[0]) { + me.firstItem = me.items[0].itemId; + } + + me.store = Ext.create('Ext.data.TreeStore', { + root: { + expanded: true, + }, + }); + var root = me.store.getRoot(); + me.insertNodes(me.items); + + delete me.items; + me.defaults = me.defaults || {}; + Ext.apply(me.defaults, { + pveSelNode: me.pveSelNode, + viewFilter: me.viewFilter, + workspace: me.workspace, + border: 0, + }); + + me.callParent(); + + var menu = me.down('#menu'); + var selection = root.findChild('id', activeTab, true) || root.firstChild; + var node = selection; + while (node !== root) { + node.expand(); + node = node.parentNode; + } + menu.setStore(me.store); + menu.setSelection(selection); + + // on a state change, + // select the new item + var statechange = function(sp, key, state) { + // it the state change is for this panel + if (stateid && key === stateid && state) { + // get active item + var acard = me.getLayout().getActiveItem().itemId; + // get the itemid of the new value + var ncard = state.value || me.firstItem; + if (ncard && acard !== ncard) { + // select the chosen item + menu.setSelection(root.findChild('id', ncard, true) || root.firstChild); + } + } + }; + + if (stateid) { + me.mon(me.sp, 'statechange', statechange); + } + }, + + insertNodes: function(items) { + var me = this; + var root = me.store.getRoot(); + + items.forEach(function(item) { + var treeitem = Ext.create('Ext.data.TreeModel', { + id: item.itemId, + text: item.title, + iconCls: item.iconCls, + leaf: true, + expanded: item.expandedOnInit, + }); + item.header = false; + if (me.savedItems[item.itemId] !== undefined) { + throw "itemId already exists, please use another"; + } + me.savedItems[item.itemId] = item; + + var group; + var curnode = root; + + // get/create the group items + while (Ext.isArray(item.groups) && item.groups.length > 0) { + group = item.groups.shift(); + + var child = curnode.findChild('id', group); + if (child === null) { + // did not find the group item + // so add it where we are + break; + } + curnode = child; + } + + // insert the item + + // lets see if it already exists + var node = curnode.findChild('id', item.itemId); + + if (node === null) { + curnode.appendChild(treeitem); + } else { + // should not happen! + throw "id already exists"; + } + }); + }, +}); +/* + * Input panel for prune settings with a keep-all option intended to be used as + * part of an edit/create window. + */ +Ext.define('PVE.panel.BackupJobPrune', { + extend: 'Proxmox.panel.PruneInputPanel', + xtype: 'pveBackupJobPrunePanel', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'vzdump_retention', + + onGetValues: function(formValues) { + if (this.needMask) { // isMasked() may not yet be true if not rendered once + return {}; + } else if (this.isCreate && !this.rendered) { + return this.keepAllDefaultForCreate ? { 'prune-backups': 'keep-all=1' } : {}; + } + + let options = { 'delete': [] }; + + if ('max-protected-backups' in formValues) { + options['max-protected-backups'] = formValues['max-protected-backups']; + } else if (this.hasMaxProtected) { + options.delete.push('max-protected-backups'); + } + + delete formValues['max-protected-backups']; + delete formValues.delete; + + let retention = PVE.Parser.printPropertyString(formValues); + if (retention === '') { + options.delete.push('prune-backups'); + } else { + options['prune-backups'] = retention; + } + + if (!this.isCreate) { + // always delete old 'maxfiles' on edit, we map it to keep-last on window load + options.delete.push('maxfiles'); + } else { + delete options.delete; + } + + return options; + }, + + updateComponents: function() { + let me = this; + + let keepAll = me.down('proxmoxcheckbox[name=keep-all]').getValue(); + let anyValue = false; + me.query('pmxPruneKeepField').forEach(field => { + anyValue = anyValue || field.getValue() !== null; + field.setDisabled(keepAll); + }); + me.down('component[name=no-keeps-hint]').setHidden(anyValue || keepAll); + }, + + listeners: { + afterrender: function(panel) { + if (panel.needMask) { + panel.down('component[name=no-keeps-hint]').setHtml(''); + panel.mask( + gettext('Backup content type not available for this storage.'), + ); + } else if (panel.isCreate && panel.keepAllDefaultForCreate) { + panel.down('proxmoxcheckbox[name=keep-all]').setValue(true); + } + panel.down('component[name=pbs-hint]').setHidden(!panel.showPBSHint); + + let maxProtected = panel.down('proxmoxintegerfield[name=max-protected-backups]'); + maxProtected.setDisabled(!panel.hasMaxProtected); + maxProtected.setHidden(!panel.hasMaxProtected); + + panel.query('pmxPruneKeepField').forEach(field => { + field.on('change', panel.updateComponents, panel); + }); + panel.updateComponents(); + }, + }, + + columnT: { + xtype: 'proxmoxcheckbox', + name: 'keep-all', + boxLabel: gettext('Keep all backups'), + listeners: { + change: function(field, newValue) { + let panel = field.up('pveBackupJobPrunePanel'); + panel.updateComponents(); + }, + }, + }, + + columnB: [ + { + xtype: 'component', + userCls: 'pmx-hint', + name: 'no-keeps-hint', + hidden: true, + padding: '5 1', + cbind: { + html: '{fallbackHintHtml}', + }, + }, + { + xtype: 'component', + userCls: 'pmx-hint', + name: 'pbs-hint', + hidden: true, + padding: '5 1', + html: gettext("It's preferred to configure backup retention directly on the Proxmox Backup Server."), + }, + { + xtype: 'proxmoxintegerfield', + name: 'max-protected-backups', + fieldLabel: gettext('Maximum Protected'), + minValue: -1, + hidden: true, + disabled: true, + emptyText: 'unlimited with Datastore.Allocate privilege, 5 otherwise', + deleteEmpty: true, + autoEl: { + tag: 'div', + 'data-qtip': Ext.String.format(gettext('Use {0} for unlimited'), -1), + }, + }, + ], +}); +Ext.define('PVE.widget.HealthWidget', { + extend: 'Ext.Component', + alias: 'widget.pveHealthWidget', + + data: { + iconCls: PVE.Utils.get_health_icon(undefined, true), + text: '', + title: '', + }, + + style: { + 'text-align': 'center', + }, + + tpl: [ + '

{title}

', + '', + '

', + '{text}', + ], + + updateHealth: function(data) { + var me = this; + me.update(Ext.apply(me.data, data)); + }, + + initComponent: function() { + var me = this; + + if (me.title) { + me.config.data.title = me.title; + } + + me.callParent(); + }, + +}); +Ext.define('pve-fw-ipsets', { + extend: 'Ext.data.Model', + fields: ['name', 'comment', 'digest'], + idProperty: 'name', +}); + +Ext.define('PVE.IPSetList', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveIPSetList', + + stateful: true, + stateId: 'grid-firewall-ipsetlist', + + ipset_panel: undefined, + + base_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + initComponent: function() { + var me = this; + + if (typeof me.ipset_panel === 'undefined') { + throw "no rule panel specified"; + } + + if (typeof me.ipset_panel === 'undefined') { + throw "no base_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-fw-ipsets', + proxy: { + type: 'proxmox', + url: "/api2/json" + me.base_url, + }, + sorters: { + property: 'name', + direction: 'ASC', + }, + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + var oldrec = sm.getSelection()[0]; + store.load(function(records, operation, success) { + if (oldrec) { + var rec = store.findRecord('name', oldrec.data.name, 0, false, true, true); + if (rec) { + sm.select(rec); + } + } + }); + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('Proxmox.window.Edit', { + subject: "IPSet '" + rec.data.name + "'", + url: me.base_url, + method: 'POST', + digest: rec.data.digest, + items: [ + { + xtype: 'hiddenfield', + name: 'rename', + value: rec.data.name, + }, + { + xtype: 'textfield', + name: 'name', + value: rec.data.name, + fieldLabel: gettext('Name'), + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'comment', + value: rec.data.comment, + fieldLabel: gettext('Comment'), + }, + ], + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + me.addBtn = new Proxmox.button.Button({ + text: gettext('Create'), + handler: function() { + sm.deselectAll(); + var win = Ext.create('Proxmox.window.Edit', { + subject: 'IPSet', + url: me.base_url, + method: 'POST', + items: [ + { + xtype: 'textfield', + name: 'name', + value: '', + fieldLabel: gettext('Name'), + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment'), + }, + ], + }); + win.show(); + win.on('destroy', reload); + }, + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: reload, + }); + + Ext.apply(me, { + store: store, + tbar: ['IPSet:', me.addBtn, me.removeBtn, me.editBtn], + selModel: sm, + columns: [ + { header: 'IPSet', dataIndex: 'name', width: '100' }, + { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 }, + ], + listeners: { + itemdblclick: run_editor, + select: function(_, rec) { + var url = me.base_url + '/' + rec.data.name; + me.ipset_panel.setBaseUrl(url); + }, + deselect: function() { + me.ipset_panel.setBaseUrl(undefined); + }, + show: reload, + }, + }); + + me.callParent(); + + store.load(); + }, +}); + +Ext.define('PVE.IPSetCidrEdit', { + extend: 'Proxmox.window.Edit', + + cidr: undefined, + + initComponent: function() { + var me = this; + + me.isCreate = me.cidr === undefined; + + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.cidr; + me.method = 'PUT'; + } + + var column1 = []; + + if (me.isCreate) { + if (!me.list_refs_url) { + throw "no alias_base_url specified"; + } + + column1.push({ + xtype: 'pveIPRefSelector', + name: 'cidr', + ref_type: 'alias', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('IP/CIDR'), + }); + } else { + column1.push({ + xtype: 'displayfield', + name: 'cidr', + value: '', + fieldLabel: gettext('IP/CIDR'), + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + isCreate: me.isCreate, + column1: column1, + column2: [ + { + xtype: 'proxmoxcheckbox', + name: 'nomatch', + checked: false, + uncheckedValue: 0, + fieldLabel: 'nomatch', + }, + ], + columnB: [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment'), + }, + ], + }); + + Ext.apply(me, { + subject: gettext('IP/CIDR'), + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + }, + }); + } + }, +}); + +Ext.define('PVE.IPSetGrid', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveIPSetGrid', + + stateful: true, + stateId: 'grid-firewall-ipsets', + + base_url: undefined, + list_refs_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + setBaseUrl: function(url) { + var me = this; + + me.base_url = url; + + if (url === undefined) { + me.addBtn.setDisabled(true); + me.store.removeAll(); + } else { + me.addBtn.setDisabled(false); + me.removeBtn.baseurl = url + '/'; + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json' + url, + }); + + me.store.load(); + } + }, + + initComponent: function() { + var me = this; + + if (!me.list_refs_url) { + throw "no1 list_refs_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-ipset', + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('PVE.IPSetCidrEdit', { + base_url: me.base_url, + cidr: rec.data.cidr, + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + me.addBtn = new Proxmox.button.Button({ + text: gettext('Add'), + disabled: true, + handler: function() { + if (!me.base_url) { + return; + } + var win = Ext.create('PVE.IPSetCidrEdit', { + base_url: me.base_url, + list_refs_url: me.list_refs_url, + }); + win.show(); + win.on('destroy', reload); + }, + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: reload, + }); + + var render_errors = function(value, metaData, record) { + var errors = record.data.errors; + if (errors) { + var msg = errors.cidr || errors.nomatch; + if (msg) { + metaData.tdCls = 'proxmox-invalid-row'; + var html = '

' + Ext.htmlEncode(msg) + '

'; + metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + + html.replace(/"/g, '"') + '"'; + } + } + return value; + }; + + Ext.apply(me, { + tbar: ['IP/CIDR:', me.addBtn, me.removeBtn, me.editBtn], + store: store, + selModel: sm, + listeners: { + itemdblclick: run_editor, + }, + columns: [ + { + xtype: 'rownumberer', + }, + { + header: gettext('IP/CIDR'), + dataIndex: 'cidr', + width: 150, + renderer: function(value, metaData, record) { + value = render_errors(value, metaData, record); + if (record.data.nomatch) { + return '! ' + value; + } + return value; + }, + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + flex: 1, + renderer: function(value) { + return Ext.util.Format.htmlEncode(value); + }, + }, + ], + }); + + me.callParent(); + + if (me.base_url) { + me.setBaseUrl(me.base_url); // load + } + }, +}, function() { + Ext.define('pve-ipset', { + extend: 'Ext.data.Model', + fields: [{ name: 'nomatch', type: 'boolean' }, + 'cidr', 'comment', 'errors'], + idProperty: 'cidr', + }); +}); + +Ext.define('PVE.IPSet', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveIPSet', + + title: 'IPSet', + + onlineHelp: 'pve_firewall_ip_sets', + + list_refs_url: undefined, + + initComponent: function() { + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + var ipset_panel = Ext.createWidget('pveIPSetGrid', { + region: 'center', + list_refs_url: me.list_refs_url, + border: false, + }); + + var ipset_list = Ext.createWidget('pveIPSetList', { + region: 'west', + ipset_panel: ipset_panel, + base_url: me.base_url, + width: '50%', + border: false, + split: true, + }); + + Ext.apply(me, { + layout: 'border', + items: [ipset_list, ipset_panel], + listeners: { + show: function() { + ipset_list.fireEvent('show', ipset_list); + }, + }, + }); + + me.callParent(); + }, +}); +/* + * This is a running chart widget you add time datapoints to it, and we only + * show the last x of it used for ceph performance charts + */ +Ext.define('PVE.widget.RunningChart', { + extend: 'Ext.container.Container', + alias: 'widget.pveRunningChart', + + layout: { + type: 'hbox', + align: 'center', + }, + items: [ + { + width: 80, + xtype: 'box', + itemId: 'title', + data: { + title: '', + }, + tpl: '

{title}:

', + }, + { + flex: 1, + xtype: 'cartesian', + height: '100%', + itemId: 'chart', + border: false, + axes: [ + { + type: 'numeric', + position: 'left', + hidden: true, + minimum: 0, + }, + { + type: 'numeric', + position: 'bottom', + hidden: true, + }, + ], + + store: { + trackRemoved: false, + data: {}, + }, + + sprites: [{ + id: 'valueSprite', + type: 'text', + text: '0 B/s', + textAlign: 'end', + textBaseline: 'middle', + fontSize: 14, + }], + + series: [{ + type: 'line', + xField: 'time', + yField: 'val', + fill: 'true', + colors: ['#cfcfcf'], + tooltip: { + trackMouse: true, + renderer: function(tooltip, record, ctx) { + if (!record || !record.data) return; + const view = this.getChart(); + const date = new Date(record.data.time); + const value = view.up().renderer(record.data.val); + const line1 = `${view.up().title}: ${value}`; + const line2 = Ext.Date.format(date, 'H:i:s'); + tooltip.setHtml(`${line1}
${line2}`); + }, + }, + style: { + lineWidth: 1.5, + opacity: 0.60, + }, + marker: { + opacity: 0, + scaling: 0.01, + fx: { + duration: 200, + easing: 'easeOut', + }, + }, + highlightCfg: { + opacity: 1, + scaling: 1.5, + }, + }], + }, + ], + + // the renderer for the tooltip and last value, default just the value + renderer: Ext.identityFn, + + // show the last x seconds default is 5 minutes + timeFrame: 5*60, + + checkThemeColors: function() { + let me = this; + let rootStyle = getComputedStyle(document.documentElement); + + // get color + let background = rootStyle.getPropertyValue("--pwt-panel-background").trim() || "#ffffff"; + let text = rootStyle.getPropertyValue("--pwt-text-color").trim() || "#000000"; + + // set the colors + me.chart.setBackground(background); + me.chart.valuesprite.setAttributes({ fillStyle: text }, true); + me.chart.redraw(); + }, + + addDataPoint: function(value, time) { + let view = this.chart; + let panel = view.up(); + let now = new Date().getTime(); + let begin = new Date(now - 1000 * panel.timeFrame).getTime(); + + view.store.add({ + time: time || now, + val: value || 0, + }); + + // delete all old records when we have 20 times more datapoints + // than seconds in our timeframe (so even a subsecond graph does + // not trigger this often) + // + // records in the store do not take much space, but like this, + // we prevent a memory leak when someone has the site open for a long time + // with minimal graphical glitches + if (view.store.count() > panel.timeFrame * 20) { + var oldData = view.store.getData().createFiltered(function(item) { + return item.data.time < begin; + }); + + view.store.remove(oldData.getRange()); + } + + view.timeaxis.setMinimum(begin); + view.timeaxis.setMaximum(now); + view.valuesprite.setText(panel.renderer(value || 0).toString()); + view.valuesprite.setAttributes({ + x: view.getWidth() - 15, + y: view.getHeight()/2, + }, true); + view.redraw(); + }, + + setTitle: function(title) { + this.title = title; + let titlebox = this.getComponent('title'); + titlebox.update({ title: title }); + }, + + initComponent: function() { + var me = this; + me.callParent(); + + if (me.title) { + me.getComponent('title').update({ title: me.title }); + } + me.chart = me.getComponent('chart'); + me.chart.timeaxis = me.chart.getAxes()[1]; + me.chart.valuesprite = me.chart.getSurface('chart').get('valueSprite'); + if (me.color) { + me.chart.series[0].setStyle({ + fill: me.color, + stroke: me.color, + }); + } + + me.checkThemeColors(); + + // switch colors on media query changes + me.mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)"); + me.themeListener = (e) => { me.checkThemeColors(); }; + me.mediaQueryList.addEventListener("change", me.themeListener); + }, + + doDestroy: function() { + let me = this; + + me.mediaQueryList.removeEventListener("change", me.themeListener); + + me.callParent(); + }, +}); +/* + * This class describes the bottom panel + */ +Ext.define('PVE.panel.StatusPanel', { + extend: 'Ext.tab.Panel', + alias: 'widget.pveStatusPanel', + + + //title: "Logs", + //tabPosition: 'bottom', + + initComponent: function() { + var me = this; + + var stateid = 'ltab'; + var sp = Ext.state.Manager.getProvider(); + + var state = sp.get(stateid); + if (state && state.value) { + me.activeTab = state.value; + } + + Ext.apply(me, { + listeners: { + tabchange: function() { + var atab = me.getActiveTab().itemId; + let tabstate = { value: atab }; + sp.set(stateid, tabstate); + }, + }, + items: [ + { + itemId: 'tasks', + title: gettext('Tasks'), + xtype: 'pveClusterTasks', + }, + { + itemId: 'clog', + title: gettext('Cluster log'), + xtype: 'pveClusterLog', + }, + ], + }); + + me.callParent(); + + me.items.get(0).fireEvent('show', me.items.get(0)); + + var statechange = function(_, key, newstate) { + if (key === stateid) { + var atab = me.getActiveTab().itemId; + let ntab = newstate.value; + if (newstate && ntab && atab !== ntab) { + me.setActiveTab(ntab); + } + } + }; + + sp.on('statechange', statechange); + me.on('destroy', function() { + sp.un('statechange', statechange); + }); + }, +}); +Ext.define('PVE.panel.GuestStatusView', { + extend: 'Proxmox.panel.StatusView', + alias: 'widget.pveGuestStatusView', + mixins: ['Proxmox.Mixin.CBind'], + + cbindData: function(initialConfig) { + var me = this; + return { + isQemu: me.pveSelNode.data.type === 'qemu', + isLxc: me.pveSelNode.data.type === 'lxc', + }; + }, + + layout: { + type: 'vbox', + align: 'stretch', + }, + + defaults: { + xtype: 'pmxInfoWidget', + padding: '2 25', + }, + items: [ + { + xtype: 'box', + height: 20, + }, + { + itemId: 'status', + title: gettext('Status'), + iconCls: 'fa fa-info fa-fw', + printBar: false, + multiField: true, + renderer: function(record) { + var me = this; + var text = record.data.status; + var qmpstatus = record.data.qmpstatus; + if (qmpstatus && qmpstatus !== record.data.status) { + text += ' (' + qmpstatus + ')'; + } + return text; + }, + }, + { + itemId: 'hamanaged', + iconCls: 'fa fa-heartbeat fa-fw', + title: gettext('HA State'), + printBar: false, + textField: 'ha', + renderer: PVE.Utils.format_ha, + }, + { + itemId: 'node', + iconCls: 'fa fa-building fa-fw', + title: gettext('Node'), + cbind: { + text: '{pveSelNode.data.node}', + }, + printBar: false, + }, + { + xtype: 'box', + height: 15, + }, + { + itemId: 'cpu', + iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon', + title: gettext('CPU usage'), + valueField: 'cpu', + maxField: 'cpus', + renderer: Proxmox.Utils.render_cpu_usage, + // in this specific api call + // we already have the correct value for the usage + calculate: Ext.identityFn, + }, + { + itemId: 'memory', + iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon', + title: gettext('Memory usage'), + valueField: 'mem', + maxField: 'maxmem', + }, + { + itemId: 'swap', + iconCls: 'fa fa-refresh fa-fw', + title: gettext('SWAP usage'), + valueField: 'swap', + maxField: 'maxswap', + cbind: { + hidden: '{isQemu}', + disabled: '{isQemu}', + }, + }, + { + itemId: 'rootfs', + iconCls: 'fa fa-hdd-o fa-fw', + title: gettext('Bootdisk size'), + valueField: 'disk', + maxField: 'maxdisk', + printBar: false, + renderer: function(used, max) { + var me = this; + me.setPrintBar(used > 0); + if (used === 0) { + return Proxmox.Utils.render_size(max); + } else { + return Proxmox.Utils.render_size_usage(used, max); + } + }, + }, + { + xtype: 'box', + height: 15, + }, + { + itemId: 'ips', + xtype: 'pveAgentIPView', + cbind: { + rstore: '{rstore}', + pveSelNode: '{pveSelNode}', + hidden: '{isLxc}', + disabled: '{isLxc}', + }, + }, + ], + + updateTitle: function() { + var me = this; + var uptime = me.getRecordValue('uptime'); + + var text = ""; + if (Number(uptime) > 0) { + text = " (" + gettext('Uptime') + ': ' + Proxmox.Utils.format_duration_long(uptime) + + ')'; + } + + me.setTitle(me.getRecordValue('name') + text); + }, +}); +Ext.define('PVE.guest.Summary', { + extend: 'Ext.panel.Panel', + xtype: 'pveGuestSummary', + + scrollable: true, + bodyPadding: 5, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + if (!me.workspace) { + throw "no workspace specified"; + } + + if (!me.statusStore) { + throw "no status storage specified"; + } + + var type = me.pveSelNode.data.type; + var template = !!me.pveSelNode.data.template; + var rstore = me.statusStore; + + var items = [ + { + xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView', + flex: 1, + padding: template ? '5' : '0 5 0 0', + itemId: 'gueststatus', + pveSelNode: me.pveSelNode, + rstore: rstore, + }, + { + xtype: 'pmxNotesView', + flex: 1, + padding: template ? '5' : '0 0 0 5', + itemId: 'notesview', + pveSelNode: me.pveSelNode, + }, + ]; + + var rrdstore; + if (!template) { + // in non-template mode put the two panels always together + items = [ + { + xtype: 'container', + height: 300, + layout: { + type: 'hbox', + align: 'stretch', + }, + items: items, + }, + ]; + + rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: `/api2/json/nodes/${nodename}/${type}/${vmid}/rrddata`, + model: 'pve-rrd-guest', + }); + + items.push( + { + xtype: 'proxmoxRRDChart', + title: gettext('CPU usage'), + pveSelNode: me.pveSelNode, + fields: ['cpu'], + fieldTitles: [gettext('CPU usage')], + unit: 'percent', + store: rrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Memory usage'), + pveSelNode: me.pveSelNode, + fields: ['maxmem', 'mem'], + fieldTitles: [gettext('Total'), gettext('RAM usage')], + unit: 'bytes', + powerOfTwo: true, + store: rrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Network traffic'), + pveSelNode: me.pveSelNode, + fields: ['netin', 'netout'], + store: rrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Disk IO'), + pveSelNode: me.pveSelNode, + fields: ['diskread', 'diskwrite'], + store: rrdstore, + }, + ); + } + + Ext.apply(me, { + tbar: ['->', { xtype: 'proxmoxRRDTypeSelector' }], + items: [ + { + xtype: 'container', + itemId: 'itemcontainer', + layout: { + type: 'column', + }, + minWidth: 700, + defaults: { + minHeight: 330, + padding: 5, + }, + items: items, + listeners: { + resize: function(container) { + Proxmox.Utils.updateColumns(container); + }, + }, + }, + ], + }); + + me.callParent(); + if (!template) { + rrdstore.startUpdate(); + me.on('destroy', rrdstore.stopUpdate); + } + let sp = Ext.state.Manager.getProvider(); + me.mon(sp, 'statechange', function(provider, key, value) { + if (key !== 'summarycolumns') { + return; + } + Proxmox.Utils.updateColumns(me.getComponent('itemcontainer')); + }); + }, +}); +Ext.define('PVE.panel.TemplateStatusView', { + extend: 'Proxmox.panel.StatusView', + alias: 'widget.pveTemplateStatusView', + + layout: { + type: 'vbox', + align: 'stretch', + }, + + defaults: { + xtype: 'pmxInfoWidget', + printBar: false, + padding: '2 25', + }, + items: [ + { + xtype: 'box', + height: 20, + }, + { + itemId: 'hamanaged', + iconCls: 'fa fa-heartbeat fa-fw', + title: gettext('HA State'), + printBar: false, + textField: 'ha', + renderer: PVE.Utils.format_ha, + }, + { + itemId: 'node', + iconCls: 'fa fa-fw fa-building', + title: gettext('Node'), + }, + { + xtype: 'box', + height: 20, + }, + { + itemId: 'cpus', + iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon', + title: gettext('Processors'), + textField: 'cpus', + }, + { + itemId: 'memory', + iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon', + title: gettext('Memory'), + textField: 'maxmem', + renderer: Proxmox.Utils.render_size, + }, + { + itemId: 'swap', + iconCls: 'fa fa-refresh fa-fw', + title: gettext('Swap'), + textField: 'maxswap', + renderer: Proxmox.Utils.render_size, + }, + { + itemId: 'disk', + iconCls: 'fa fa-hdd-o fa-fw', + title: gettext('Bootdisk size'), + textField: 'maxdisk', + renderer: Proxmox.Utils.render_size, + }, + { + xtype: 'box', + height: 20, + }, + ], + + initComponent: function() { + var me = this; + + var name = me.pveSelNode.data.name; + if (!name) { + throw "no name specified"; + } + + me.title = name; + + me.callParent(); + if (me.pveSelNode.data.type !== 'lxc') { + me.remove(me.getComponent('swap')); + } + me.getComponent('node').updateValue(me.pveSelNode.data.node); + }, +}); +Ext.define('PVE.panel.MultiDiskPanel', { + extend: 'Ext.panel.Panel', + + setNodename: function(nodename) { + this.items.each((panel) => panel.setNodename(nodename)); + }, + + border: false, + bodyBorder: false, + + layout: 'card', + + controller: { + xclass: 'Ext.app.ViewController', + + vmconfig: {}, + + onAdd: function() { + let me = this; + me.lookup('addButton').setDisabled(true); + me.addDisk(); + let count = me.lookup('grid').getStore().getCount() + 1; // +1 is from ide2 + me.lookup('addButton').setDisabled(count >= me.maxCount); + }, + + getNextFreeDisk: function(vmconfig) { + throw "implement in subclass"; + }, + + addPanel: function(itemId, vmconfig, nextFreeDisk) { + throw "implement in subclass"; + }, + + // define in subclass + diskSorter: undefined, + + addDisk: function() { + let me = this; + let grid = me.lookup('grid'); + let store = grid.getStore(); + + // get free disk id + let vmconfig = me.getVMConfig(true); + let nextFreeDisk = me.getNextFreeDisk(vmconfig); + if (!nextFreeDisk) { + return; + } + + // add store entry + panel + let itemId = 'disk-card-' + ++Ext.idSeed; + let rec = store.add({ + name: nextFreeDisk.confid, + itemId, + })[0]; + + let panel = me.addPanel(itemId, vmconfig, nextFreeDisk); + panel.updateVMConfig(vmconfig); + + // we need to setup a validitychange handler, so that we can show + // that a disk has invalid fields + let fields = panel.query('field'); + fields.forEach((el) => el.on('validitychange', () => { + let valid = fields.every((field) => field.isValid()); + rec.set('valid', valid); + me.checkValidity(); + })); + + store.sort(me.diskSorter); + + // select if the panel added is the only one + if (store.getCount() === 1) { + grid.getSelectionModel().select(0, false); + } + }, + + getBaseVMConfig: function() { + throw "implement in subclass"; + }, + + getVMConfig: function(all) { + let me = this; + + let vmconfig = me.getBaseVMConfig(); + + me.lookup('grid').getStore().each((rec) => { + if (all || rec.get('valid')) { + vmconfig[rec.get('name')] = rec.get('itemId'); + } + }); + + return vmconfig; + }, + + checkValidity: function() { + let me = this; + let valid = me.lookup('grid').getStore().findExact('valid', false) === -1; + me.lookup('validationfield').setValue(valid); + }, + + updateVMConfig: function() { + let me = this; + let view = me.getView(); + let grid = me.lookup('grid'); + let store = grid.getStore(); + + let vmconfig = me.getVMConfig(); + + let valid = true; + + store.each((rec) => { + let itemId = rec.get('itemId'); + let name = rec.get('name'); + let panel = view.getComponent(itemId); + if (!panel) { + throw "unexpected missing panel"; + } + + // copy config for each panel and remote its own id + let panel_vmconfig = Ext.apply({}, vmconfig); + if (panel_vmconfig[name] === itemId) { + delete panel_vmconfig[name]; + } + + if (!rec.get('valid')) { + valid = false; + } + + panel.updateVMConfig(panel_vmconfig); + }); + + me.lookup('validationfield').setValue(valid); + + return vmconfig; + }, + + onChange: function(panel, newVal) { + let me = this; + let store = me.lookup('grid').getStore(); + + let el = store.findRecord('itemId', panel.itemId, 0, false, true, true); + if (el.get('name') === newVal) { + // do not update if there was no change + return; + } + + el.set('name', newVal); + el.commit(); + + store.sort(me.diskSorter); + + // so that it happens after the layouting + setTimeout(function() { + me.updateVMConfig(); + }, 10); + }, + + onRemove: function(tableview, rowIndex, colIndex, item, event, record) { + let me = this; + let grid = me.lookup('grid'); + let store = grid.getStore(); + let removed_idx = store.indexOf(record); + + let selection = grid.getSelection()[0]; + let selected_idx = store.indexOf(selection); + + if (selected_idx === removed_idx) { + let newidx = store.getCount() > removed_idx + 1 ? removed_idx + 1: removed_idx - 1; + grid.getSelectionModel().select(newidx, false); + } + + store.remove(record); + me.getView().remove(record.get('itemId')); + me.lookup('addButton').setDisabled(false); + me.updateVMConfig(); + me.checkValidity(); + }, + + onSelectionChange: function(grid, selection) { + let me = this; + if (!selection || selection.length < 1) { + return; + } + + me.getView().setActiveItem(selection[0].data.itemId); + }, + + control: { + 'inputpanel': { + diskidchange: 'onChange', + }, + 'grid[reference=grid]': { + selectionchange: 'onSelectionChange', + }, + }, + + init: function(view) { + let me = this; + me.onAdd(); + me.lookup('grid').getSelectionModel().select(0, false); + }, + }, + + dockedItems: [ + { + xtype: 'container', + layout: { + type: 'vbox', + align: 'stretch', + }, + dock: 'left', + border: false, + width: 130, + items: [ + { + xtype: 'grid', + hideHeaders: true, + reference: 'grid', + flex: 1, + emptyText: gettext('No Disks'), + margin: '0 0 5 0', + store: { + fields: ['name', 'itemId', 'valid'], + data: [], + }, + columns: [ + { + dataIndex: 'name', + renderer: function(val, md, rec) { + let warn = ''; + if (!rec.get('valid')) { + warn = ' '; + } + return val + warn; + }, + flex: 1, + }, + { + xtype: 'actioncolumn', + width: 30, + align: 'center', + menuDisabled: true, + items: [ + { + iconCls: 'x-fa fa-trash critical', + tooltip: 'Delete', + handler: 'onRemove', + isActionDisabled: 'deleteDisabled', + }, + ], + }, + ], + }, + { + xtype: 'button', + reference: 'addButton', + text: gettext('Add'), + iconCls: 'fa fa-plus-circle', + handler: 'onAdd', + }, + { + // dummy field to control wizard validation + xtype: 'textfield', + hidden: true, + reference: 'validationfield', + submitValue: false, + value: true, + validator: (val) => !!val, + }, + ], + }, + ], +}); +/* + * Left Treepanel, containing all the resources we manage in this datacenter: server nodes, server storages, VMs and Containers + */ +Ext.define('PVE.tree.ResourceTree', { + extend: 'Ext.tree.TreePanel', + alias: ['widget.pveResourceTree'], + + userCls: 'proxmox-tags-circle', + + statics: { + typeDefaults: { + node: { + iconCls: 'fa fa-building', + text: gettext('Nodes'), + }, + pool: { + iconCls: 'fa fa-tags', + text: gettext('Resource Pool'), + }, + storage: { + iconCls: 'fa fa-database', + text: gettext('Storage'), + }, + sdn: { + iconCls: 'fa fa-th', + text: gettext('SDN'), + }, + qemu: { + iconCls: 'fa fa-desktop', + text: gettext('Virtual Machine'), + }, + lxc: { + //iconCls: 'x-tree-node-lxc', + iconCls: 'fa fa-cube', + text: gettext('LXC Container'), + }, + template: { + iconCls: 'fa fa-file-o', + }, + }, + }, + + useArrows: true, + + // private + nodeSortFn: function(node1, node2) { + let me = this; + let n1 = node1.data, n2 = node2.data; + + if (!n1.groupbyid === !n2.groupbyid) { + let n1IsGuest = n1.type === 'qemu' || n1.type === 'lxc'; + let n2IsGuest = n2.type === 'qemu' || n2.type === 'lxc'; + if (me['group-guest-types'] || !n1IsGuest || !n2IsGuest) { + // first sort (group) by type + if (n1.type > n2.type) { + return 1; + } else if (n1.type < n2.type) { + return -1; + } + } + + // then sort (group) by ID + if (n1IsGuest) { + if (me['group-templates'] && (!n1.template !== !n2.template)) { + return n1.template ? 1 : -1; // sort templates after regular VMs + } + if (me['sort-field'] === 'vmid') { + if (n1.vmid > n2.vmid) { // prefer VMID as metric for guests + return 1; + } else if (n1.vmid < n2.vmid) { + return -1; + } + } else { + return n1.name.localeCompare(n2.name); + } + } + // same types but not a guest + return n1.id > n2.id ? 1 : n1.id < n2.id ? -1 : 0; + } else if (n1.groupbyid) { + return -1; + } else if (n2.groupbyid) { + return 1; + } + return 0; // should not happen + }, + + // private: fast binary search + findInsertIndex: function(node, child, start, end) { + let me = this; + + let diff = end - start; + if (diff <= 0) { + return start; + } + let mid = start + (diff >> 1); + + let res = me.nodeSortFn(child, node.childNodes[mid]); + if (res <= 0) { + return me.findInsertIndex(node, child, start, mid); + } else { + return me.findInsertIndex(node, child, mid + 1, end); + } + }, + + setIconCls: function(info) { + let cls = PVE.Utils.get_object_icon_class(info.type, info); + if (cls !== '') { + info.iconCls = cls; + } + }, + + // add additional elements to text. Currently only the usage indicator for storages + setText: function(info) { + let me = this; + + let status = ''; + if (info.type === 'storage') { + let usage = info.disk / info.maxdisk; + if (usage >= 0.0 && usage <= 1.0) { + let barHeight = (usage * 100).toFixed(0); + let remainingHeight = (100 - barHeight).toFixed(0); + status = '
'; + status += `
`; + status += `
`; + status += '
'; + } + } + if (Ext.isNumeric(info.vmid) && info.vmid > 0) { + if (PVE.UIOptions.getTreeSortingValue('sort-field') !== 'vmid') { + info.text = `${info.name} (${String(info.vmid)})`; + } + } + + info.text += PVE.Utils.renderTags(info.tags, PVE.UIOptions.tagOverrides); + + info.text = status + info.text; + }, + + setToolTip: function(info) { + if (info.type === 'pool' || info.groupbyid !== undefined) { + return; + } + + let qtips = [gettext('Status') + ': ' + (info.qmpstatus || info.status)]; + if (info.lock) { + qtips.push(Ext.String.format(gettext('Config locked ({0})'), info.lock)); + } + if (info.hastate !== 'unmanaged') { + qtips.push(gettext('HA State') + ": " + info.hastate); + } + + info.qtip = qtips.join(', '); + }, + + // private + addChildSorted: function(node, info) { + let me = this; + + me.setIconCls(info); + me.setText(info); + me.setToolTip(info); + + if (info.groupbyid) { + info.text = info.groupbyid; + if (info.type === 'type') { + let defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid]; + if (defaults && defaults.text) { + info.text = defaults.text; + } + } + } + let child = Ext.create('PVETree', info); + + if (node.childNodes) { + let pos = me.findInsertIndex(node, child, 0, node.childNodes.length); + node.insertBefore(child, node.childNodes[pos]); + } else { + node.insertBefore(child); + } + + return child; + }, + + // private + groupChild: function(node, info, groups, level) { + let me = this; + + let groupBy = groups[level]; + let v = info[groupBy]; + + if (v) { + let group = node.findChild('groupbyid', v); + if (!group) { + let groupinfo; + if (info.type === groupBy) { + groupinfo = info; + } else { + groupinfo = { + type: groupBy, + id: groupBy + "/" + v, + }; + if (groupBy !== 'type') { + groupinfo[groupBy] = v; + } + } + groupinfo.leaf = false; + groupinfo.groupbyid = v; + group = me.addChildSorted(node, groupinfo); + } + if (info.type === groupBy) { + return group; + } + if (group) { + return me.groupChild(group, info, groups, level + 1); + } + } + + return me.addChildSorted(node, info); + }, + + saveSortingOptions: function() { + let me = this; + let changed = false; + for (const key of ['sort-field', 'group-templates', 'group-guest-types']) { + let newValue = PVE.UIOptions.getTreeSortingValue(key); + if (me[key] !== newValue) { + me[key] = newValue; + changed = true; + } + } + return changed; + }, + + initComponent: function() { + let me = this; + me.saveSortingOptions(); + + let rstore = PVE.data.ResourceStore; + let sp = Ext.state.Manager.getProvider(); + + if (!me.viewFilter) { + me.viewFilter = {}; + } + + let pdata = { + dataIndex: {}, + updateCount: 0, + }; + + let store = Ext.create('Ext.data.TreeStore', { + model: 'PVETree', + root: { + expanded: true, + id: 'root', + text: gettext('Datacenter'), + iconCls: 'fa fa-server', + }, + }); + + let stateid = 'rid'; + + const changedFields = [ + 'text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock', 'tags', + ]; + + let updateTree = function() { + store.suspendEvents(); + + let rootnode = me.store.getRootNode(); + // remember selected node (and all parents) + let sm = me.getSelectionModel(); + let lastsel = sm.getSelection()[0]; + let parents = []; + let sorting_changed = me.saveSortingOptions(); + for (let node = lastsel; node; node = node.parentNode) { + parents.push(node); + } + + let groups = me.viewFilter.groups || []; + // explicitly check for node/template, as those are not always grouping attributes + // also check for name for when the tree is sorted by name + let moveCheckAttrs = groups.concat(['node', 'template', 'name']); + let filterfn = me.viewFilter.filterfn; + + let reselect = false; // for disappeared nodes + let index = pdata.dataIndex; + // remove vanished or moved items and update changed items in-place + for (const [key, olditem] of Object.entries(index)) { + // getById() use find(), which is slow (ExtJS4 DP5) + let item = rstore.data.get(olditem.data.id); + + let changed = sorting_changed, moved = sorting_changed; + if (item) { + // test if any grouping attributes changed, catches migrated tree-nodes in server view too + for (const attr of moveCheckAttrs) { + if (item.data[attr] !== olditem.data[attr]) { + moved = true; + break; + } + } + + // tree item has been updated + for (const field of changedFields) { + if (item.data[field] !== olditem.data[field]) { + changed = true; + break; + } + } + // FIXME: also test filterfn()? + } + + if (changed) { + olditem.beginEdit(); + let info = olditem.data; + Ext.apply(info, item.data); + me.setIconCls(info); + me.setText(info); + me.setToolTip(info); + olditem.commit(); + } + if ((!item || moved) && olditem.isLeaf()) { + delete index[key]; + let parentNode = olditem.parentNode; + // a selected item moved (migration) or disappeared (destroyed), so deselect that + // node now and try to reselect the moved (or its parent) node later + if (lastsel && olditem.data.id === lastsel.data.id) { + reselect = true; + sm.deselect(olditem); + } + // store events are suspended, so remove the item manually + store.remove(olditem); + parentNode.removeChild(olditem, true); + } + } + + rstore.each(function(item) { // add new items + let olditem = index[item.data.id]; + if (olditem) { + return; + } + if (filterfn && !filterfn(item)) { + return; + } + let info = Ext.apply({ leaf: true }, item.data); + + let child = me.groupChild(rootnode, info, groups, 0); + if (child) { + index[item.data.id] = child; + } + }); + + store.resumeEvents(); + store.fireEvent('refresh', store); + + // select parent node if original selected node vanished + if (lastsel && !rootnode.findChild('id', lastsel.data.id, true)) { + lastsel = rootnode; + for (const node of parents) { + if (rootnode.findChild('id', node.data.id, true)) { + lastsel = node; + break; + } + } + me.selectById(lastsel.data.id); + } else if (lastsel && reselect) { + me.selectById(lastsel.data.id); + } + + // on first tree load set the selection from the stateful provider + if (!pdata.updateCount) { + rootnode.expand(); + me.applyState(sp.get(stateid)); + } + + pdata.updateCount++; + }; + + sp.on('statechange', (_sp, key, value) => { + if (key === stateid) { + me.applyState(value); + } + }); + + Ext.apply(me, { + allowSelection: true, + store: store, + viewConfig: { + animate: false, // note: animate cause problems with applyState + }, + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + destroy: function() { + rstore.un("load", updateTree); + }, + beforecellmousedown: function(tree, td, cellIndex, record, tr, rowIndex, ev) { + let sm = me.getSelectionModel(); + // disable selection when right clicking except if the record is already selected + me.allowSelection = ev.button !== 2 || sm.isSelected(record); + }, + beforeselect: function(tree, record, index, eopts) { + let allow = me.allowSelection; + me.allowSelection = true; + return allow; + }, + itemdblclick: PVE.Utils.openTreeConsole, + }, + setViewFilter: function(view) { + me.viewFilter = view; + me.clearTree(); + updateTree(); + }, + setDatacenterText: function(clustername) { + let rootnode = me.store.getRootNode(); + + let rnodeText = gettext('Datacenter'); + if (clustername !== undefined) { + rnodeText += ' (' + clustername + ')'; + } + + rootnode.beginEdit(); + rootnode.data.text = rnodeText; + rootnode.commit(); + }, + clearTree: function() { + pdata.updateCount = 0; + let rootnode = me.store.getRootNode(); + rootnode.collapse(); + rootnode.removeAll(); + pdata.dataIndex = {}; + me.getSelectionModel().deselectAll(); + }, + selectExpand: function(node) { + let sm = me.getSelectionModel(); + if (!sm.isSelected(node)) { + sm.select(node); + for (let iter = node; iter; iter = iter.parentNode) { + if (!iter.isExpanded()) { + iter.expand(); + } + } + me.getView().focusRow(node); + } + }, + selectById: function(nodeid) { + let rootnode = me.store.getRootNode(); + let node; + if (nodeid === 'root') { + node = rootnode; + } else { + node = rootnode.findChild('id', nodeid, true); + } + if (node) { + me.selectExpand(node); + } + return node; + }, + applyState: function(state) { + if (state && state.value) { + me.selectById(state.value); + } else { + me.getSelectionModel().deselectAll(); + } + }, + }); + + me.callParent(); + + me.getSelectionModel().on('select', (_sm, n) => sp.set(stateid, { value: n.data.id })); + + rstore.on("load", updateTree); + rstore.startUpdate(); + }, + +}); +Ext.define('PVE.guest.SnapshotTree', { + extend: 'Ext.tree.Panel', + xtype: 'pveGuestSnapshotTree', + + stateful: true, + stateId: 'grid-snapshots', + + viewModel: { + data: { + // should be 'qemu' or 'lxc' + type: undefined, + nodename: undefined, + vmid: undefined, + snapshotAllowed: false, + rollbackAllowed: false, + snapshotFeature: false, + running: false, + selected: '', + load_delay: 3000, + }, + formulas: { + canSnapshot: (get) => get('snapshotAllowed') && get('snapshotFeature'), + canRollback: (get) => get('rollbackAllowed') && get('isSnapshot'), + canRemove: (get) => get('snapshotAllowed') && get('isSnapshot'), + isSnapshot: (get) => get('selected') && get('selected') !== 'current', + buttonText: (get) => get('snapshotAllowed') ? gettext('Edit') : gettext('View'), + showMemory: (get) => get('type') === 'qemu', + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + newSnapshot: function() { + this.run_editor(false); + }, + + editSnapshot: function() { + this.run_editor(true); + }, + + run_editor: function(edit) { + let me = this; + let vm = me.getViewModel(); + let snapname; + if (edit) { + snapname = vm.get('selected'); + if (!snapname || snapname === 'current') { return; } + } + let win = Ext.create('PVE.window.Snapshot', { + nodename: vm.get('nodename'), + vmid: vm.get('vmid'), + viewonly: !vm.get('snapshotAllowed'), + type: vm.get('type'), + isCreate: !edit, + submitText: !edit ? gettext('Take Snapshot') : undefined, + snapname: snapname, + running: vm.get('running'), + }); + win.show(); + me.mon(win, 'destroy', me.reload, me); + }, + + snapshotAction: function(action, method) { + let me = this; + let view = me.getView(); + let vm = me.getViewModel(); + let snapname = vm.get('selected'); + if (!snapname) { return; } + + let nodename = vm.get('nodename'); + let type = vm.get('type'); + let vmid = vm.get('vmid'); + + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/${type}/${vmid}/snapshot/${snapname}/${action}`, + method: method, + waitMsgTarget: view, + callback: function() { + me.reload(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + }, + }); + }, + + rollback: function() { + this.snapshotAction('rollback', 'POST'); + }, + remove: function() { + this.snapshotAction('', 'DELETE'); + }, + cancel: function() { + this.load_task.cancel(); + }, + + reload: function() { + let me = this; + let view = me.getView(); + let vm = me.getViewModel(); + let nodename = vm.get('nodename'); + let vmid = vm.get('vmid'); + let type = vm.get('type'); + let load_delay = vm.get('load_delay'); + + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/${type}/${vmid}/snapshot`, + method: 'GET', + failure: function(response, opts) { + if (me.destroyed) return; + Proxmox.Utils.setErrorMask(view, response.htmlStatus); + me.load_task.delay(load_delay); + }, + success: function(response, opts) { + if (me.destroyed) { + // this is in a delayed task, avoid dragons if view has + // been destroyed already and go home. + return; + } + Proxmox.Utils.setErrorMask(view, false); + var digest = 'invalid'; + var idhash = {}; + var root = { name: '__root', expanded: true, children: [] }; + Ext.Array.each(response.result.data, function(item) { + item.leaf = true; + item.children = []; + if (item.name === 'current') { + vm.set('running', !!item.running); + digest = item.digest + item.running; + item.iconCls = PVE.Utils.get_object_icon_class(vm.get('type'), item); + } else { + item.iconCls = 'fa fa-fw fa-history x-fa-tree'; + } + idhash[item.name] = item; + }); + + if (digest !== me.old_digest) { + me.old_digest = digest; + + Ext.Array.each(response.result.data, function(item) { + if (item.parent && idhash[item.parent]) { + var parent_item = idhash[item.parent]; + parent_item.children.push(item); + parent_item.leaf = false; + parent_item.expanded = true; + parent_item.expandable = false; + } else { + root.children.push(item); + } + }); + + me.getView().setRootNode(root); + } + + me.load_task.delay(load_delay); + }, + }); + + // if we do not have the permissions, we don't have to check + // if we can create a snapshot, since the butten stays disabled + if (!vm.get('snapshotAllowed')) { + return; + } + + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/${type}/${vmid}/feature`, + params: { feature: 'snapshot' }, + method: 'GET', + success: function(response, options) { + if (me.destroyed) { + // this is in a delayed task, the current view could been + // destroyed already; then we mustn't do viemodel set + return; + } + let res = response.result.data; + vm.set('snapshotFeature', !!res.hasFeature); + }, + }); + }, + + select: function(grid, val) { + let vm = this.getViewModel(); + if (val.length < 1) { + vm.set('selected', ''); + return; + } + vm.set('selected', val[0].data.name); + }, + + init: function(view) { + let me = this; + let vm = me.getViewModel(); + me.load_task = new Ext.util.DelayedTask(me.reload, me); + + if (!view.type) { + throw 'guest type not set'; + } + vm.set('type', view.type); + + if (!view.pveSelNode.data.node) { + throw "no node name specified"; + } + vm.set('nodename', view.pveSelNode.data.node); + + if (!view.pveSelNode.data.vmid) { + throw "no VM ID specified"; + } + vm.set('vmid', view.pveSelNode.data.vmid); + + let caps = Ext.state.Manager.get('GuiCap'); + vm.set('snapshotAllowed', !!caps.vms['VM.Snapshot']); + vm.set('rollbackAllowed', !!caps.vms['VM.Snapshot.Rollback']); + + view.getStore().sorters.add({ + property: 'order', + direction: 'ASC', + }); + + me.reload(); + }, + }, + + listeners: { + selectionchange: 'select', + itemdblclick: 'editSnapshot', + beforedestroy: 'cancel', + }, + + layout: 'fit', + rootVisible: false, + animate: false, + sortableColumns: false, + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Take Snapshot'), + disabled: true, + bind: { + disabled: "{!canSnapshot}", + }, + handler: 'newSnapshot', + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Rollback'), + disabled: true, + bind: { + disabled: '{!canRollback}', + }, + confirmMsg: function() { + let view = this.up('treepanel'); + let rec = view.getSelection()[0]; + let vmid = view.getViewModel().get('vmid'); + return Proxmox.Utils.format_task_description('qmrollback', vmid) + + ` '${rec.data.name}'? ${gettext("Current state will be lost.")}`; + }, + handler: 'rollback', + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + bind: { + text: '{buttonText}', + disabled: '{!isSnapshot}', + }, + disabled: true, + edit: true, + handler: 'editSnapshot', + }, + { + xtype: 'proxmoxButton', + text: gettext('Remove'), + disabled: true, + dangerous: true, + bind: { + disabled: '{!canRemove}', + }, + confirmMsg: function() { + let view = this.up('treepanel'); + let { data } = view.getSelection()[0]; + return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${data.name}'`); + }, + handler: 'remove', + }, + { + xtype: 'label', + text: gettext("The current guest configuration does not support taking new snapshots"), + hidden: true, + bind: { + hidden: "{canSnapshot}", + }, + }, + ], + + columnLines: true, + + fields: [ + 'name', + 'description', + 'snapstate', + 'vmstate', + 'running', + { name: 'snaptime', type: 'date', dateFormat: 'timestamp' }, + { + name: 'order', + calculate: function(data) { + return data.snaptime || (data.name === 'current' ? 'ZZZ' : data.snapstate); + }, + }, + ], + + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name'), + dataIndex: 'name', + width: 200, + renderer: (value, _, { data }) => data.name !== 'current' ? value : gettext('NOW'), + }, + { + text: gettext('RAM'), + hidden: true, + bind: { + hidden: '{!showMemory}', + }, + align: 'center', + resizable: false, + dataIndex: 'vmstate', + width: 50, + renderer: (value, _, { data }) => data.name !== 'current' ? Proxmox.Utils.format_boolean(value) : '', + }, + { + text: gettext('Date') + "/" + gettext("Status"), + dataIndex: 'snaptime', + width: 150, + renderer: function(value, metaData, record) { + if (record.data.snapstate) { + return record.data.snapstate; + } else if (value) { + return Ext.Date.format(value, 'Y-m-d H:i:s'); + } + return ''; + }, + }, + { + text: gettext('Description'), + dataIndex: 'description', + flex: 1, + renderer: function(value, metaData, record) { + if (record.data.name === 'current') { + return gettext("You are here!"); + } else { + return Ext.String.htmlEncode(value); + } + }, + }, + ], + +}); +Ext.define('PVE.window.Backup', { + extend: 'Ext.window.Window', + + resizable: false, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.vmtype) { + throw "no VM type specified"; + } + + let compressionSelector = Ext.create('PVE.form.CompressionSelector', { + name: 'compress', + value: 'zstd', + fieldLabel: gettext('Compression'), + }); + + let modeSelector = Ext.create('PVE.form.BackupModeSelector', { + fieldLabel: gettext('Mode'), + value: 'snapshot', + name: 'mode', + }); + + let mailtoField = Ext.create('Ext.form.field.Text', { + fieldLabel: gettext('Send email to'), + name: 'mailto', + emptyText: Proxmox.Utils.noneText, + }); + + const keepNames = [ + ['keep-last', gettext('Keep Last')], + ['keep-hourly', gettext('Keep Hourly')], + ['keep-daily', gettext('Keep Daily')], + ['keep-weekly', gettext('Keep Weekly')], + ['keep-monthly', gettext('Keep Monthly')], + ['keep-yearly', gettext('Keep Yearly')], + ]; + + let pruneSettings = keepNames.map( + name => Ext.create('Ext.form.field.Display', { + name: name[0], + fieldLabel: name[1], + hidden: true, + }), + ); + + let removeCheckbox = Ext.create('Proxmox.form.Checkbox', { + name: 'remove', + checked: false, + hidden: true, + uncheckedValue: 0, + fieldLabel: gettext('Prune'), + autoEl: { + tag: 'div', + 'data-qtip': gettext('Prune older backups afterwards'), + }, + handler: function(checkbox, value) { + pruneSettings.forEach(field => field.setHidden(!value)); + me.down('label[name="pruneLabel"]').setHidden(!value); + }, + }); + + let initialDefaults = false; + + var storagesel = Ext.create('PVE.form.StorageSelector', { + nodename: me.nodename, + name: 'storage', + fieldLabel: gettext('Storage'), + storageContent: 'backup', + allowBlank: false, + listeners: { + change: function(f, v) { + if (!initialDefaults) { + me.setLoading(false); + } + + if (v === null || v === undefined || v === '') { + return; + } + + let store = f.getStore(); + let rec = store.findRecord('storage', v, 0, false, true, true); + + if (rec && rec.data && rec.data.type === 'pbs') { + compressionSelector.setValue('zstd'); + compressionSelector.setDisabled(true); + } else if (!compressionSelector.getEditable()) { + compressionSelector.setDisabled(false); + } + + Proxmox.Utils.API2Request({ + url: `/nodes/${me.nodename}/vzdump/defaults`, + method: 'GET', + params: { + storage: v, + }, + waitMsgTarget: me, + success: function(response, opts) { + const data = response.result.data; + + if (!initialDefaults && data.mailto !== undefined) { + mailtoField.setValue(data.mailto); + } + if (!initialDefaults && data.mode !== undefined) { + modeSelector.setValue(data.mode); + } + if (!initialDefaults && (data['notes-template'] ?? false)) { + me.down('field[name=notes-template]').setValue( + PVE.Utils.unEscapeNotesTemplate(data['notes-template']), + ); + } + + initialDefaults = true; + + // always update storage dependent properties + if (data['prune-backups'] !== undefined) { + const keepParams = PVE.Parser.parsePropertyString( + data["prune-backups"], + ); + if (!keepParams['keep-all']) { + removeCheckbox.setHidden(false); + pruneSettings.forEach(function(field) { + const keep = keepParams[field.name]; + if (keep) { + field.setValue(keep); + } else { + field.reset(); + } + }); + return; + } + } + + // no defaults or keep-all=1 + removeCheckbox.setHidden(true); + removeCheckbox.setValue(false); + pruneSettings.forEach(field => field.reset()); + }, + failure: function(response, opts) { + initialDefaults = true; + + removeCheckbox.setHidden(true); + removeCheckbox.setValue(false); + pruneSettings.forEach(field => field.reset()); + + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, + }, + }); + + let protectedCheckbox = Ext.create('Proxmox.form.Checkbox', { + name: 'protected', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Protected'), + }); + + me.formPanel = Ext.create('Proxmox.panel.InputPanel', { + bodyPadding: 10, + border: false, + column1: [ + storagesel, + modeSelector, + protectedCheckbox, + ], + column2: [ + compressionSelector, + mailtoField, + removeCheckbox, + ], + columnB: [ + { + xtype: 'textareafield', + name: 'notes-template', + fieldLabel: gettext('Notes'), + anchor: '100%', + value: '{{guestname}}', + }, + { + xtype: 'box', + style: { + margin: '8px 0px', + 'line-height': '1.5em', + }, + html: Ext.String.format( + gettext('Possible template variables are: {0}'), + PVE.Utils.notesTemplateVars.map(v => `{{${v}}}`).join(', '), + ), + }, + { + xtype: 'label', + name: 'pruneLabel', + text: gettext('Storage Retention Configuration') + ':', + hidden: true, + }, + { + layout: 'hbox', + border: false, + defaults: { + border: false, + layout: 'anchor', + flex: 1, + }, + items: [ + { + padding: '0 10 0 0', + defaults: { + labelWidth: 110, + }, + items: [ + pruneSettings[0], + pruneSettings[2], + pruneSettings[4], + ], + }, + { + padding: '0 0 0 10', + defaults: { + labelWidth: 110, + }, + items: [ + pruneSettings[1], + pruneSettings[3], + pruneSettings[5], + ], + }, + ], + }, + ], + }); + + var submitBtn = Ext.create('Ext.Button', { + text: gettext('Backup'), + handler: function() { + var storage = storagesel.getValue(); + let values = me.formPanel.getValues(); + var params = { + storage: storage, + vmid: me.vmid, + mode: values.mode, + remove: values.remove, + }; + + if (values.mailto) { + params.mailto = values.mailto; + } + + if (values.compress) { + params.compress = values.compress; + } + + if (values.protected) { + params.protected = values.protected; + } + + if (values['notes-template']) { + params['notes-template'] = PVE.Utils.escapeNotesTemplate( + values['notes-template']); + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/vzdump', + params: params, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + // close later so we reload the grid + // after the task has completed + me.hide(); + + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + listeners: { + close: function() { + me.close(); + }, + }, + }); + win.show(); + }, + }); + }, + }); + + var helpBtn = Ext.create('Proxmox.button.Help', { + onlineHelp: 'chapter_vzdump', + listenToGlobalEvent: false, + hidden: false, + }); + + var title = gettext('Backup') + " " + + (me.vmtype === 'lxc' ? "CT" : "VM") + + " " + me.vmid; + + Ext.apply(me, { + title: title, + modal: true, + layout: 'auto', + border: false, + width: 600, + items: [me.formPanel], + buttons: [helpBtn, '->', submitBtn], + listeners: { + afterrender: function() { + /// cleared within the storage selector's change listener + me.setLoading(gettext('Please wait...')); + storagesel.setValue(me.storage); + }, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.window.BackupConfig', { + extend: 'Ext.window.Window', + title: gettext('Configuration'), + width: 600, + height: 400, + layout: 'fit', + modal: true, + items: { + xtype: 'component', + itemId: 'configtext', + autoScroll: true, + style: { + 'white-space': 'pre', + 'font-family': 'monospace', + padding: '5px', + }, + }, + + initComponent: function() { + var me = this; + + if (!me.volume) { + throw "no volume specified"; + } + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.callParent(); + + Proxmox.Utils.API2Request({ + url: "/nodes/" + nodename + "/vzdump/extractconfig", + method: 'GET', + params: { + volume: me.volume, + }, + failure: function(response, opts) { + me.close(); + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + me.show(); + me.down('#configtext').update(Ext.htmlEncode(response.result.data)); + }, + }); + }, +}); +Ext.define('PVE.window.BulkAction', { + extend: 'Ext.window.Window', + + resizable: true, + width: 800, + height: 600, + modal: true, + layout: { + type: 'fit', + }, + border: false, + + // the action to set, currently there are: `startall`, `migrateall`, `stopall` + action: undefined, + + submit: function(params) { + let me = this; + + Proxmox.Utils.API2Request({ + params: params, + url: `/nodes/${me.nodename}/${me.action}`, + waitMsgTarget: me, + method: 'POST', + failure: response => Ext.Msg.alert('Error', response.htmlStatus), + success: function({ result }, options) { + Ext.create('Proxmox.window.TaskViewer', { + autoShow: true, + upid: result.data, + listeners: { + destroy: () => me.close(), + }, + }); + me.hide(); + }, + }); + }, + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + if (!me.action) { + throw "no action specified"; + } + if (!me.btnText) { + throw "no button text specified"; + } + if (!me.title) { + throw "no title specified"; + } + + let items = []; + if (me.action === 'migrateall') { + items.push( + { + xtype: 'pveNodeSelector', + name: 'target', + disallowedNodes: [me.nodename], + fieldLabel: gettext('Target node'), + allowBlank: false, + onlineValidator: true, + }, + { + xtype: 'proxmoxintegerfield', + name: 'maxworkers', + minValue: 1, + maxValue: 100, + value: 1, + fieldLabel: gettext('Parallel jobs'), + allowBlank: false, + }, + { + xtype: 'fieldcontainer', + fieldLabel: gettext('Allow local disk migration'), + layout: 'hbox', + items: [{ + xtype: 'proxmoxcheckbox', + name: 'with-local-disks', + checked: true, + uncheckedValue: 0, + listeners: { + change: (cb, val) => me.down('#localdiskwarning').setVisible(val), + }, + }, + { + itemId: 'localdiskwarning', + xtype: 'displayfield', + flex: 1, + padding: '0 0 0 10', + userCls: 'pmx-hint', + value: 'Note: Migration with local disks might take long.', + }], + }, + { + itemId: 'lxcwarning', + xtype: 'displayfield', + userCls: 'pmx-hint', + value: 'Warning: Running CTs will be migrated in Restart Mode.', + hidden: true, // only visible if running container chosen + }, + ); + } else if (me.action === 'startall') { + items.push({ + xtype: 'hiddenfield', + name: 'force', + value: 1, + }); + } else if (me.action === 'stopall') { + items.push( + { + xtype: 'proxmoxcheckbox', + name: 'force-stop', + fieldLabel: gettext('Force Stop'), + boxLabel: gettext('Force stop guest if shutdown times out.'), + checked: true, + uncheckedValue: 0, + }, + { + xtype: 'proxmoxintegerfield', + name: 'timeout', + fieldLabel: gettext('Timeout (s)'), + emptyText: '180', + minValue: 0, + maxValue: 7200, + allowBlank: true, + }, + ); + } + + items.push({ + xtype: 'vmselector', + itemId: 'vms', + name: 'vms', + flex: 1, + height: 300, + selectAll: true, + allowBlank: false, + nodename: me.nodename, + action: me.action, + listeners: { + selectionchange: function(vmselector, records) { + if (me.action === 'migrateall') { + let showWarning = records.some( + item => item.data.type === 'lxc' && item.data.status === 'running', + ); + me.down('#lxcwarning').setVisible(showWarning); + } + }, + }, + }); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + layout: { + type: 'vbox', + align: 'stretch', + }, + fieldDefaults: { + labelWidth: me.action === 'migrateall' ? 300 : 120, + anchor: '100%', + }, + items: items, + }); + + let form = me.formPanel.getForm(); + + let submitBtn = Ext.create('Ext.Button', { + text: me.btnText, + handler: function() { + form.isValid(); + me.submit(form.getValues()); + }, + }); + + Ext.apply(me, { + items: [me.formPanel], + buttons: [submitBtn], + }); + + me.callParent(); + + form.on('validitychange', function() { + let valid = form.isValid(); + submitBtn.setDisabled(!valid); + }); + form.isValid(); + }, +}); +Ext.define('PVE.ceph.Install', { + extend: 'Ext.window.Window', + xtype: 'pveCephInstallWindow', + mixins: ['Proxmox.Mixin.CBind'], + + width: 220, + header: false, + resizable: false, + draggable: false, + modal: true, + nodename: undefined, + shadow: false, + border: false, + bodyBorder: false, + closable: false, + cls: 'install-mask', + bodyCls: 'install-mask', + layout: { + align: 'stretch', + pack: 'center', + type: 'vbox', + }, + viewModel: { + data: { + isInstalled: false, + }, + formulas: { + buttonText: function(get) { + if (get('isInstalled')) { + return gettext('Configure Ceph'); + } else { + return gettext('Install Ceph'); + } + }, + windowText: function(get) { + if (get('isInstalled')) { + return `

+ ${Ext.String.format(gettext('{0} is not initialized.'), 'Ceph')} + ${gettext('You need to create an initial config once.')}

`; + } else { + return '

' + + Ext.String.format(gettext('{0} is not installed on this node.'), 'Ceph') + '
' + + gettext('Would you like to install it now?') + '

'; + } + }, + }, + }, + items: [ + { + bind: { + html: '{windowText}', + }, + border: false, + padding: 5, + bodyCls: 'install-mask', + + }, + { + xtype: 'button', + bind: { + text: '{buttonText}', + }, + viewModel: {}, + cbind: { + nodename: '{nodename}', + }, + handler: function() { + let view = this.up('pveCephInstallWindow'); + let wizzard = Ext.create('PVE.ceph.CephInstallWizard', { + nodename: view.nodename, + }); + wizzard.getViewModel().set('isInstalled', this.getViewModel().get('isInstalled')); + wizzard.show(); + view.mon(wizzard, 'beforeClose', function() { + view.fireEvent("cephInstallWindowClosed"); + view.close(); + }); + }, + }, + ], +}); +Ext.define('PVE.window.Clone', { + extend: 'Ext.window.Window', + + resizable: false, + + isTemplate: false, + + onlineHelp: 'qm_copy_and_clone', + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'panel[reference=cloneform]': { + validitychange: 'disableSubmit', + }, + }, + disableSubmit: function(form) { + this.lookupReference('submitBtn').setDisabled(!form.isValid()); + }, + }, + + statics: { + // display a snapshot selector only if needed + wrap: function(nodename, vmid, isTemplate, guestType) { + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/' + guestType + '/' + vmid +'/snapshot', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, opts) { + var snapshotList = response.result.data; + var hasSnapshots = !(snapshotList.length === 1 && + snapshotList[0].name === 'current'); + + Ext.create('PVE.window.Clone', { + nodename: nodename, + guestType: guestType, + vmid: vmid, + isTemplate: isTemplate, + hasSnapshots: hasSnapshots, + }).show(); + }, + }); + }, + }, + + create_clone: function(values) { + var me = this; + + var params = { newid: values.newvmid }; + + if (values.snapname && values.snapname !== 'current') { + params.snapname = values.snapname; + } + + if (values.pool) { + params.pool = values.pool; + } + + if (values.name) { + if (me.guestType === 'lxc') { + params.hostname = values.name; + } else { + params.name = values.name; + } + } + + if (values.target) { + params.target = values.target; + } + + if (values.clonemode === 'copy') { + params.full = 1; + if (values.hdstorage) { + params.storage = values.hdstorage; + if (values.diskformat && me.guestType !== 'lxc') { + params.format = values.diskformat; + } + } + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/clone', + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + me.close(); + }, + }); + }, + + // disable the Storage selector when clone mode is linked clone + updateVisibility: function() { + var me = this; + var clonemode = me.lookupReference('clonemodesel').getValue(); + var disksel = me.lookup('diskselector'); + disksel.setDisabled(clonemode === 'clone'); + }, + + // add to the list of valid nodes each node where + // all the VM disks are available + verifyFeature: function() { + var me = this; + + var snapname = me.lookupReference('snapshotsel').getValue(); + var clonemode = me.lookupReference('clonemodesel').getValue(); + + var params = { feature: clonemode }; + if (snapname !== 'current') { + params.snapname = snapname; + } + + Proxmox.Utils.API2Request({ + waitMsgTarget: me, + url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/feature', + params: params, + method: 'GET', + failure: function(response, opts) { + me.lookupReference('submitBtn').setDisabled(true); + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + var res = response.result.data; + + me.lookupReference('targetsel').allowedNodes = res.nodes; + me.lookupReference('targetsel').validate(); + }, + }); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.snapname) { + me.snapname = 'current'; + } + + if (!me.guestType) { + throw "no Guest Type specified"; + } + + var titletext = me.guestType === 'lxc' ? 'CT' : 'VM'; + if (me.isTemplate) { + titletext += ' Template'; + } + me.title = "Clone " + titletext + " " + me.vmid; + + var col1 = []; + var col2 = []; + + col1.push({ + xtype: 'pveNodeSelector', + name: 'target', + reference: 'targetsel', + fieldLabel: gettext('Target node'), + selectCurNode: true, + allowBlank: false, + onlineValidator: true, + listeners: { + change: function(f, value) { + me.lookupReference('hdstorage').setTargetNode(value); + }, + }, + }); + + var modelist = [['copy', gettext('Full Clone')]]; + if (me.isTemplate) { + modelist.push(['clone', gettext('Linked Clone')]); + } + + col1.push({ + xtype: 'pveGuestIDSelector', + name: 'newvmid', + guestType: me.guestType, + value: '', + loadNextFreeID: true, + validateExists: false, + }, + { + xtype: 'textfield', + name: 'name', + allowBlank: true, + fieldLabel: me.guestType === 'lxc' ? gettext('Hostname') : gettext('Name'), + }, + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true, + }, + ); + + col2.push({ + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Mode'), + name: 'clonemode', + reference: 'clonemodesel', + allowBlank: false, + hidden: !me.isTemplate, + value: me.isTemplate ? 'clone' : 'copy', + comboItems: modelist, + listeners: { + change: function(t, value) { + me.updateVisibility(); + me.verifyFeature(); + }, + }, + }, + { + xtype: 'PVE.form.SnapshotSelector', + name: 'snapname', + reference: 'snapshotsel', + fieldLabel: gettext('Snapshot'), + nodename: me.nodename, + guestType: me.guestType, + vmid: me.vmid, + hidden: !!(me.isTemplate || !me.hasSnapshots), + disabled: false, + allowBlank: false, + value: me.snapname, + listeners: { + change: function(f, value) { + me.verifyFeature(); + }, + }, + }, + { + xtype: 'pveDiskStorageSelector', + reference: 'diskselector', + nodename: me.nodename, + autoSelect: false, + hideSize: true, + hideSelection: true, + storageLabel: gettext('Target Storage'), + allowBlank: true, + storageContent: me.guestType === 'qemu' ? 'images' : 'rootdir', + emptyText: gettext('Same as source'), + disabled: !!me.isTemplate, // because default mode is clone for templates + }); + + var formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + reference: 'cloneform', + border: false, + layout: 'hbox', + defaultType: 'container', + fieldDefaults: { + labelWidth: 100, + anchor: '100%', + }, + items: [ + { + flex: 1, + padding: '0 10 0 0', + layout: 'anchor', + items: col1, + }, + { + flex: 1, + padding: '0 0 0 10', + layout: 'anchor', + items: col2, + }, + ], + }); + + Ext.apply(me, { + modal: true, + width: 600, + height: 250, + border: false, + layout: 'fit', + buttons: [{ + xtype: 'proxmoxHelpButton', + listenToGlobalEvent: false, + hidden: false, + onlineHelp: me.onlineHelp, + }, + '->', + { + reference: 'submitBtn', + text: gettext('Clone'), + disabled: true, + handler: function() { + var cloneForm = me.lookupReference('cloneform'); + if (cloneForm.isValid()) { + me.create_clone(cloneForm.getValues()); + } + }, + }], + items: [formPanel], + }); + + me.callParent(); + + me.verifyFeature(); + }, +}); +Ext.define('PVE.FirewallEnableEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveFirewallEnableEdit'], + mixins: ['Proxmox.Mixin.CBind'], + + subject: gettext('Firewall'), + cbindData: { + defaultValue: 0, + }, + width: 350, + + items: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + uncheckedValue: 0, + cbind: { + defaultValue: '{defaultValue}', + checked: '{defaultValue}', + }, + deleteDefaultValue: false, + fieldLabel: gettext('Firewall'), + }, + { + xtype: 'displayfield', + name: 'warning', + userCls: 'pmx-hint', + value: gettext('Warning: Firewall still disabled at datacenter level!'), + hidden: true, + }, + ], + + beforeShow: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/cluster/firewall/options', + method: 'GET', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + if (!response.result.data.enable) { + me.down('displayfield[name=warning]').setVisible(true); + } + }, + }); + }, +}); +Ext.define('PVE.FirewallLograteInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveFirewallLograteInputPanel', + + viewModel: {}, + + items: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + reference: 'enable', + fieldLabel: gettext('Enable'), + value: true, + }, + { + layout: 'hbox', + border: false, + items: [ + { + xtype: 'numberfield', + name: 'rate', + fieldLabel: gettext('Log rate limit'), + minValue: 1, + maxValue: 99, + allowBlank: false, + flex: 2, + value: 1, + }, + { + xtype: 'box', + html: '
/
', + }, + { + xtype: 'proxmoxKVComboBox', + name: 'unit', + comboItems: [ + ['second', 'second'], + ['minute', 'minute'], + ['hour', 'hour'], + ['day', 'day'], + ], + allowBlank: false, + flex: 1, + value: 'second', + }, + ], + }, + { + xtype: 'numberfield', + name: 'burst', + fieldLabel: gettext('Log burst limit'), + minValue: 1, + maxValue: 99, + value: 5, + }, + ], + + onGetValues: function(values) { + let me = this; + + let cfg = { + enable: values.enable !== undefined ? 1 : 0, + rate: values.rate + '/' + values.unit, + burst: values.burst, + }; + let properties = PVE.Parser.printPropertyString(cfg, undefined); + if (properties === '') { + return { 'delete': 'log_ratelimit' }; + } + return { log_ratelimit: properties }; + }, + + setValues: function(values) { + let me = this; + + let properties = {}; + if (values.log_ratelimit !== undefined) { + properties = PVE.Parser.parsePropertyString(values.log_ratelimit, 'enable'); + if (properties.rate) { + var matches = properties.rate.match(/^(\d+)\/(second|minute|hour|day)$/); + if (matches) { + properties.rate = matches[1]; + properties.unit = matches[2]; + } + } + } + me.callParent([properties]); + }, +}); + +Ext.define('PVE.FirewallLograteEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveFirewallLograteEdit', + + subject: gettext('Log rate limit'), + + items: [{ + xtype: 'pveFirewallLograteInputPanel', + }], + autoLoad: true, +}); +/*global u2f*/ +Ext.define('PVE.window.LoginWindow', { + extend: 'Ext.window.Window', + + viewModel: { + data: { + openid: false, + }, + formulas: { + button_text: function(get) { + if (get("openid") === true) { + return gettext("Login (OpenID redirect)"); + } else { + return gettext("Login"); + } + }, + }, + }, + + controller: { + + xclass: 'Ext.app.ViewController', + + onLogon: async function() { + var me = this; + + var form = this.lookupReference('loginForm'); + var unField = this.lookupReference('usernameField'); + var saveunField = this.lookupReference('saveunField'); + var view = this.getView(); + + if (!form.isValid()) { + return; + } + + let creds = form.getValues(); + + if (this.getViewModel().data.openid === true) { + const redirectURL = location.origin; + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/openid/auth-url', + params: { + realm: creds.realm, + "redirect-url": redirectURL, + }, + method: 'POST', + success: function(resp, opts) { + window.location = resp.result.data; + }, + failure: function(resp, opts) { + Proxmox.Utils.authClear(); + form.unmask(); + Ext.MessageBox.alert( + gettext('Error'), + gettext('OpenID redirect failed.') + `
${resp.htmlStatus}`, + ); + }, + }); + return; + } + + view.el.mask(gettext('Please wait...'), 'x-mask-loading'); + + // set or clear username + var sp = Ext.state.Manager.getProvider(); + if (saveunField.getValue() === true) { + sp.set(unField.getStateId(), unField.getValue()); + } else { + sp.clear(unField.getStateId()); + } + sp.set(saveunField.getStateId(), saveunField.getValue()); + + try { + // Request updated authentication mechanism: + creds['new-format'] = 1; + + let resp = await Proxmox.Async.api2({ + url: '/api2/extjs/access/ticket', + params: creds, + method: 'POST', + }); + + let data = resp.result.data; + if (data.ticket.startsWith("PVE:!tfa!")) { + // Store first factor login information first: + data.LoggedOut = true; + Proxmox.Utils.setAuthData(data); + + data = await me.performTFAChallenge(data); + + // Fill in what we copy over from the 1st factor: + data.CSRFPreventionToken = Proxmox.CSRFPreventionToken; + data.username = Proxmox.UserName; + me.success(data); + } else if (Ext.isDefined(data.NeedTFA)) { + // Store first factor login information first: + data.LoggedOut = true; + Proxmox.Utils.setAuthData(data); + + if (Ext.isDefined(data.U2FChallenge)) { + me.perform_u2f(data); + } else { + me.perform_otp(); + } + } else { + me.success(data); + } + } catch (error) { + me.failure(error); + } + }, + + /* START NEW TFA CODE (pbs copy) */ + performTFAChallenge: async function(data) { + let me = this; + + let userid = data.username; + let ticket = data.ticket; + let challenge = JSON.parse(decodeURIComponent( + ticket.split(':')[1].slice("!tfa!".length), + )); + + let resp = await new Promise((resolve, reject) => { + Ext.create('Proxmox.window.TfaLoginWindow', { + userid, + ticket, + challenge, + onResolve: value => resolve(value), + onReject: reject, + }).show(); + }); + + return resp.result.data; + }, + /* END NEW TFA CODE (pbs copy) */ + + failure: function(resp) { + var me = this; + var view = me.getView(); + view.el.unmask(); + var handler = function() { + var uf = me.lookupReference('usernameField'); + uf.focus(true, true); + }; + + let emsg = gettext("Login failed. Please try again"); + + if (resp.failureType === "connect") { + emsg = gettext("Connection failure. Network error or Proxmox VE services not running?"); + } + + Ext.MessageBox.alert(gettext('Error'), emsg, handler); + }, + success: function(data) { + var me = this; + var view = me.getView(); + var handler = view.handler || Ext.emptyFn; + handler.call(me, data); + view.close(); + }, + + perform_otp: function() { + var me = this; + var win = Ext.create('PVE.window.TFALoginWindow', { + onLogin: function(value) { + me.finish_tfa(value); + }, + onCancel: function() { + Proxmox.LoggedOut = false; + Proxmox.Utils.authClear(); + me.getView().show(); + }, + }); + win.show(); + }, + + perform_u2f: function(data) { + var me = this; + // Show the message: + var msg = Ext.Msg.show({ + title: 'U2F: '+gettext('Verification'), + message: gettext('Please press the button on your U2F Device'), + buttons: [], + }); + var chlg = data.U2FChallenge; + var key = { + version: chlg.version, + keyHandle: chlg.keyHandle, + }; + u2f.sign(chlg.appId, chlg.challenge, [key], function(res) { + msg.close(); + if (res.errorCode) { + Proxmox.Utils.authClear(); + Ext.Msg.alert(gettext('Error'), PVE.Utils.render_u2f_error(res.errorCode)); + return; + } + delete res.errorCode; + me.finish_tfa(JSON.stringify(res)); + }); + }, + finish_tfa: function(res) { + var me = this; + var view = me.getView(); + view.el.mask(gettext('Please wait...'), 'x-mask-loading'); + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: { + response: res, + }, + method: 'POST', + timeout: 5000, // it'll delay both success & failure + success: function(resp, opts) { + view.el.unmask(); + // Fill in what we copy over from the 1st factor: + var data = resp.result.data; + data.CSRFPreventionToken = Proxmox.CSRFPreventionToken; + data.username = Proxmox.UserName; + // Finish logging in: + me.success(data); + }, + failure: function(resp, opts) { + Proxmox.Utils.authClear(); + me.failure(resp); + }, + }); + }, + + control: { + 'field[name=username]': { + specialkey: function(f, e) { + if (e.getKey() === e.ENTER) { + var pf = this.lookupReference('passwordField'); + if (!pf.getValue()) { + pf.focus(false); + } + } + }, + }, + 'field[name=lang]': { + change: function(f, value) { + var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10); + Ext.util.Cookies.set('PVELangCookie', value, dt); + this.getView().mask(gettext('Please wait...'), 'x-mask-loading'); + window.location.reload(); + }, + }, + 'field[name=realm]': { + change: function(f, value) { + let record = f.store.getById(value); + if (record === undefined) return; + let data = record.data; + this.getViewModel().set("openid", data.type === "openid"); + }, + }, + 'button[reference=loginButton]': { + click: 'onLogon', + }, + '#': { + show: function() { + var me = this; + + var sp = Ext.state.Manager.getProvider(); + var checkboxField = this.lookupReference('saveunField'); + var unField = this.lookupReference('usernameField'); + + var checked = sp.get(checkboxField.getStateId()); + checkboxField.setValue(checked); + + if (checked === true) { + var username = sp.get(unField.getStateId()); + unField.setValue(username); + var pwField = this.lookupReference('passwordField'); + pwField.focus(); + } + + let auth = Proxmox.Utils.getOpenIDRedirectionAuthorization(); + if (auth !== undefined) { + Proxmox.Utils.authClear(); + + let loginForm = this.lookupReference('loginForm'); + loginForm.mask(gettext('OpenID login - please wait...'), 'x-mask-loading'); + + const redirectURL = location.origin; + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/openid/login', + params: { + state: auth.state, + code: auth.code, + "redirect-url": redirectURL, + }, + method: 'POST', + failure: function(response) { + loginForm.unmask(); + let error = response.htmlStatus; + Ext.MessageBox.alert( + gettext('Error'), + gettext('OpenID login failed, please try again') + `
${error}`, + () => { window.location = redirectURL; }, + ); + }, + success: function(response, options) { + loginForm.unmask(); + let data = response.result.data; + history.replaceState(null, '', redirectURL); + me.success(data); + }, + }); + } + }, + }, + }, + }, + + width: 400, + modal: true, + border: false, + draggable: true, + closable: false, + resizable: false, + layout: 'auto', + + title: gettext('Proxmox VE Login'), + + defaultFocus: 'usernameField', + defaultButton: 'loginButton', + + items: [{ + xtype: 'form', + layout: 'form', + url: '/api2/extjs/access/ticket', + reference: 'loginForm', + + fieldDefaults: { + labelAlign: 'right', + allowBlank: false, + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('User name'), + name: 'username', + itemId: 'usernameField', + reference: 'usernameField', + stateId: 'login-username', + bind: { + visible: "{!openid}", + disabled: "{openid}", + }, + }, + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Password'), + name: 'password', + reference: 'passwordField', + bind: { + visible: "{!openid}", + disabled: "{openid}", + }, + }, + { + xtype: 'pmxRealmComboBox', + name: 'realm', + }, + { + xtype: 'proxmoxLanguageSelector', + fieldLabel: gettext('Language'), + value: Ext.util.Cookies.get('PVELangCookie') || Proxmox.defaultLang || 'en', + name: 'lang', + reference: 'langField', + submitValue: false, + }, + ], + buttons: [ + { + xtype: 'checkbox', + fieldLabel: gettext('Save User name'), + name: 'saveusername', + reference: 'saveunField', + stateId: 'login-saveusername', + labelWidth: 250, + labelAlign: 'right', + submitValue: false, + bind: { + visible: "{!openid}", + }, + }, + { + bind: { + text: "{button_text}", + }, + reference: 'loginButton', + }, + ], + }], + }); +Ext.define('PVE.window.Migrate', { + extend: 'Ext.window.Window', + + vmtype: undefined, + nodename: undefined, + vmid: undefined, + maxHeight: 450, + + viewModel: { + data: { + vmid: undefined, + nodename: undefined, + vmtype: undefined, + running: false, + qemu: { + onlineHelp: 'qm_migration', + commonName: 'VM', + }, + lxc: { + onlineHelp: 'pct_migration', + commonName: 'CT', + }, + migration: { + possible: true, + preconditions: [], + 'with-local-disks': 0, + mode: undefined, + allowedNodes: undefined, + overwriteLocalResourceCheck: false, + hasLocalResources: false, + }, + + }, + + formulas: { + setMigrationMode: function(get) { + if (get('running')) { + if (get('vmtype') === 'qemu') { + return gettext('Online'); + } else { + return gettext('Restart Mode'); + } + } else { + return gettext('Offline'); + } + }, + setStorageselectorHidden: function(get) { + if (get('migration.with-local-disks') && get('running')) { + return false; + } else { + return true; + } + }, + setLocalResourceCheckboxHidden: function(get) { + if (get('running') || !get('migration.hasLocalResources') || + Proxmox.UserName !== 'root@pam') { + return true; + } else { + return false; + } + }, + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'panel[reference=formPanel]': { + validityChange: function(panel, isValid) { + this.getViewModel().set('migration.possible', isValid); + this.checkMigratePreconditions(); + }, + }, + }, + + init: function(view) { + var me = this, + vm = view.getViewModel(); + + if (!view.nodename) { + throw "missing custom view config: nodename"; + } + vm.set('nodename', view.nodename); + + if (!view.vmid) { + throw "missing custom view config: vmid"; + } + vm.set('vmid', view.vmid); + + if (!view.vmtype) { + throw "missing custom view config: vmtype"; + } + vm.set('vmtype', view.vmtype); + + view.setTitle( + Ext.String.format('{0} {1} {2}', gettext('Migrate'), vm.get(view.vmtype).commonName, view.vmid), + ); + me.lookup('proxmoxHelpButton').setHelpConfig({ + onlineHelp: vm.get(view.vmtype).onlineHelp, + }); + me.lookup('formPanel').isValid(); + }, + + onTargetChange: function(nodeSelector) { + // Always display the storages of the currently seleceted migration target + this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value); + this.checkMigratePreconditions(); + }, + + startMigration: function() { + var me = this, + view = me.getView(), + vm = me.getViewModel(); + + var values = me.lookup('formPanel').getValues(); + var params = { + target: values.target, + }; + + if (vm.get('migration.mode')) { + params[vm.get('migration.mode')] = 1; + } + if (vm.get('migration.with-local-disks')) { + params['with-local-disks'] = 1; + } + //offline migration to a different storage currently might fail at a late stage + //(i.e. after some disks have been moved), so don't expose it yet in the GUI + if (vm.get('migration.with-local-disks') && vm.get('running') && values.targetstorage) { + params.targetstorage = values.targetstorage; + } + + if (vm.get('migration.overwriteLocalResourceCheck')) { + params.force = 1; + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate', + waitMsgTarget: view, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var extraTitle = Ext.String.format(' ({0} ---> {1})', vm.get('nodename'), params.target); + + Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + extraTitle: extraTitle, + }).show(); + + view.close(); + }, + }); + }, + + checkMigratePreconditions: function(resetMigrationPossible) { + var me = this, + vm = me.getViewModel(); + + var vmrec = PVE.data.ResourceStore.findRecord('vmid', vm.get('vmid'), + 0, false, false, true); + if (vmrec && vmrec.data && vmrec.data.running) { + vm.set('running', true); + } + + if (vm.get('vmtype') === 'qemu') { + me.checkQemuPreconditions(resetMigrationPossible); + } else { + me.checkLxcPreconditions(resetMigrationPossible); + } + me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')]; + + // Only allow nodes where the local storage is available in case of offline migration + // where storage migration is not possible + me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes'); + + me.lookup('formPanel').isValid(); + }, + + checkQemuPreconditions: async function(resetMigrationPossible) { + let me = this, + vm = me.getViewModel(), + migrateStats; + + if (vm.get('running')) { + vm.set('migration.mode', 'online'); + } + + try { + if (me.fetchingNodeMigrateInfo && me.fetchingNodeMigrateInfo === vm.get('nodename')) { + return; + } + me.fetchingNodeMigrateInfo = vm.get('nodename'); + let { result } = await Proxmox.Async.api2({ + url: `/nodes/${vm.get('nodename')}/${vm.get('vmtype')}/${vm.get('vmid')}/migrate`, + method: 'GET', + }); + migrateStats = result.data; + me.fetchingNodeMigrateInfo = false; + } catch (error) { + Ext.Msg.alert(gettext('Error'), error.htmlStatus); + return; + } + + if (migrateStats.running) { + vm.set('running', true); + } + // Get migration object from viewmodel to prevent to many bind callbacks + let migration = vm.get('migration'); + if (resetMigrationPossible) { + migration.possible = true; + } + migration.preconditions = []; + + if (migrateStats.allowed_nodes) { + migration.allowedNodes = migrateStats.allowed_nodes; + let target = me.lookup('pveNodeSelector').value; + if (target.length && !migrateStats.allowed_nodes.includes(target)) { + let disallowed = migrateStats.not_allowed_nodes[target]; + let missingStorages = disallowed.unavailable_storages.join(', '); + + migration.possible = false; + migration.preconditions.push({ + text: 'Storage (' + missingStorages + ') not available on selected target. ' + + 'Start VM to use live storage migration or select other target node', + severity: 'error', + }); + } + } + + if (migrateStats.local_resources.length) { + migration.hasLocalResources = true; + if (!migration.overwriteLocalResourceCheck || vm.get('running')) { + migration.possible = false; + migration.preconditions.push({ + text: Ext.String.format('Can\'t migrate VM with local resources: {0}', + migrateStats.local_resources.join(', ')), + severity: 'error', + }); + } else { + migration.preconditions.push({ + text: Ext.String.format('Migrate VM with local resources: {0}. ' + + 'This might fail if resources aren\'t available on the target node.', + migrateStats.local_resources.join(', ')), + severity: 'warning', + }); + } + } + + if (migrateStats.local_disks.length) { + migrateStats.local_disks.forEach(function(disk) { + if (disk.cdrom && disk.cdrom === 1) { + if (!disk.volid.includes('vm-' + vm.get('vmid') + '-cloudinit')) { + migration.possible = false; + migration.preconditions.push({ + text: "Can't migrate VM with local CD/DVD", + severity: 'error', + }); + } + } else { + let size = disk.size ? '(' + Proxmox.Utils.render_size(disk.size) + ')' : ''; + migration['with-local-disks'] = 1; + migration.preconditions.push({ + text: Ext.String.format('Migration with local disk might take long: {0} {1}', disk.volid, size), + severity: 'warning', + }); + } + }); + } + + vm.set('migration', migration); + }, + checkLxcPreconditions: function(resetMigrationPossible) { + let vm = this.getViewModel(); + if (vm.get('running')) { + vm.set('migration.mode', 'restart'); + } + }, + }, + + width: 600, + modal: true, + layout: { + type: 'vbox', + align: 'stretch', + }, + border: false, + items: [ + { + xtype: 'form', + reference: 'formPanel', + bodyPadding: 10, + border: false, + layout: 'hbox', + items: [ + { + xtype: 'container', + flex: 1, + items: [{ + xtype: 'displayfield', + name: 'source', + fieldLabel: gettext('Source node'), + bind: { + value: '{nodename}', + }, + }, + { + xtype: 'displayfield', + reference: 'migrationMode', + fieldLabel: gettext('Mode'), + bind: { + value: '{setMigrationMode}', + }, + }], + }, + { + xtype: 'container', + flex: 1, + items: [{ + xtype: 'pveNodeSelector', + reference: 'pveNodeSelector', + name: 'target', + fieldLabel: gettext('Target node'), + allowBlank: false, + disallowedNodes: undefined, + onlineValidator: true, + listeners: { + change: 'onTargetChange', + }, + }, + { + xtype: 'pveStorageSelector', + reference: 'pveDiskStorageSelector', + name: 'targetstorage', + fieldLabel: gettext('Target storage'), + storageContent: 'images', + allowBlank: true, + autoSelect: false, + emptyText: gettext('Current layout'), + bind: { + hidden: '{setStorageselectorHidden}', + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'overwriteLocalResourceCheck', + fieldLabel: gettext('Force'), + autoEl: { + tag: 'div', + 'data-qtip': 'Overwrite local resources unavailable check', + }, + bind: { + hidden: '{setLocalResourceCheckboxHidden}', + value: '{migration.overwriteLocalResourceCheck}', + }, + listeners: { + change: { + fn: 'checkMigratePreconditions', + extraArg: true, + }, + }, + }], + }, + ], + }, + { + xtype: 'gridpanel', + reference: 'preconditionGrid', + selectable: false, + flex: 1, + columns: [{ + text: '', + dataIndex: 'severity', + renderer: function(v) { + switch (v) { + case 'warning': + return ' '; + case 'error': + return ''; + default: + return v; + } + }, + width: 35, + }, + { + text: 'Info', + dataIndex: 'text', + cellWrap: true, + flex: 1, + }], + bind: { + hidden: '{!migration.preconditions.length}', + store: { + fields: ['severity', 'text'], + data: '{migration.preconditions}', + sorters: 'text', + }, + }, + }, + + ], + buttons: [ + { + xtype: 'proxmoxHelpButton', + reference: 'proxmoxHelpButton', + onlineHelp: 'pct_migration', + listenToGlobalEvent: false, + hidden: false, + }, + '->', + { + xtype: 'button', + reference: 'submitButton', + text: gettext('Migrate'), + handler: 'startMigration', + bind: { + disabled: '{!migration.possible}', + }, + }, + ], +}); +Ext.define('pve-prune-list', { + extend: 'Ext.data.Model', + fields: [ + 'type', + 'vmid', + { + name: 'ctime', + type: 'date', + dateFormat: 'timestamp', + }, + ], +}); + +Ext.define('PVE.PruneInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pvePruneInputPanel', + mixins: ['Proxmox.Mixin.CBind'], + + onGetValues: function(values) { + let me = this; + + // the API expects a single prune-backups property string + let pruneBackups = PVE.Parser.printPropertyString(values); + values = { + 'prune-backups': pruneBackups, + 'type': me.backup_type, + 'vmid': me.backup_id, + }; + + return values; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + if (!view.url) { + throw "no url specified"; + } + if (!view.backup_type) { + throw "no backup_type specified"; + } + if (!view.backup_id) { + throw "no backup_id specified"; + } + + this.reload(); // initial load + }, + + reload: function() { + let view = this.getView(); + + // helper to allow showing why a backup is kept + let addKeepReasons = function(backups, params) { + const rules = [ + 'keep-last', + 'keep-hourly', + 'keep-daily', + 'keep-weekly', + 'keep-monthly', + 'keep-yearly', + 'keep-all', // when all keep options are not set + ]; + let counter = {}; + + backups.sort((a, b) => b.ctime - a.ctime); + + let ruleIndex = -1; + let nextRule = function() { + let rule; + do { + ruleIndex++; + rule = rules[ruleIndex]; + } while (!params[rule] && rule !== 'keep-all'); + counter[rule] = 0; + return rule; + }; + + let rule = nextRule(); + for (let backup of backups) { + if (backup.mark === 'keep') { + counter[rule]++; + if (rule !== 'keep-all') { + backup.keepReason = rule + ': ' + counter[rule]; + if (counter[rule] >= params[rule]) { + rule = nextRule(); + } + } else { + backup.keepReason = rule; + } + } + } + }; + + let params = view.getValues(); + let keepParams = PVE.Parser.parsePropertyString(params["prune-backups"]); + + Proxmox.Utils.API2Request({ + url: view.url, + method: "GET", + params: params, + callback: function() { + // for easy breakpoint setting + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var data = response.result.data; + addKeepReasons(data, keepParams); + view.pruneStore.setData(data); + }, + }); + }, + + control: { + field: { change: 'reload' }, + }, + }, + + column1: [ + { + xtype: 'pmxPruneKeepField', + name: 'keep-last', + fieldLabel: gettext('keep-last'), + }, + { + xtype: 'pmxPruneKeepField', + name: 'keep-hourly', + fieldLabel: gettext('keep-hourly'), + }, + { + xtype: 'pmxPruneKeepField', + name: 'keep-daily', + fieldLabel: gettext('keep-daily'), + }, + { + xtype: 'pmxPruneKeepField', + name: 'keep-weekly', + fieldLabel: gettext('keep-weekly'), + }, + { + xtype: 'pmxPruneKeepField', + name: 'keep-monthly', + fieldLabel: gettext('keep-monthly'), + }, + { + xtype: 'pmxPruneKeepField', + name: 'keep-yearly', + fieldLabel: gettext('keep-yearly'), + }, + ], + + initComponent: function() { + var me = this; + + me.pruneStore = Ext.create('Ext.data.Store', { + model: 'pve-prune-list', + sorters: { property: 'ctime', direction: 'DESC' }, + }); + + me.column2 = [ + { + xtype: 'grid', + height: 200, + store: me.pruneStore, + columns: [ + { + header: gettext('Backup Time'), + sortable: true, + dataIndex: 'ctime', + renderer: function(value, metaData, record) { + let text = Ext.Date.format(value, 'Y-m-d H:i:s'); + if (record.data.mark === 'remove') { + return '
'+ text +'
'; + } else { + return text; + } + }, + flex: 1, + }, + { + text: 'Keep (reason)', + dataIndex: 'mark', + renderer: function(value, metaData, record) { + if (record.data.mark === 'keep') { + return 'true (' + record.data.keepReason + ')'; + } else if (record.data.mark === 'protected') { + return 'true (protected)'; + } else if (record.data.mark === 'renamed') { + return 'true (renamed)'; + } else { + return 'false'; + } + }, + flex: 1, + }, + ], + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.window.Prune', { + extend: 'Proxmox.window.Edit', + + method: 'DELETE', + submitText: gettext("Prune"), + + fieldDefaults: { labelWidth: 130 }, + + isCreate: true, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename specified"; + } + if (!me.storage) { + throw "no storage specified"; + } + if (!me.backup_type) { + throw "no backup_type specified"; + } + if (me.backup_type !== 'qemu' && me.backup_type !== 'lxc') { + throw "unknown backup type: " + me.backup_type; + } + if (!me.backup_id) { + throw "no backup_id specified"; + } + + let title = Ext.String.format( + gettext("Prune Backups for '{0}' on Storage '{1}'"), + me.backup_type + '/' + me.backup_id, + me.storage, + ); + + Ext.apply(me, { + url: '/api2/extjs/nodes/' + me.nodename + '/storage/' + me.storage + "/prunebackups", + title: title, + items: [ + { + xtype: 'pvePruneInputPanel', + url: '/api2/extjs/nodes/' + me.nodename + '/storage/' + me.storage + "/prunebackups", + backup_type: me.backup_type, + backup_id: me.backup_id, + storage: me.storage, + }, + ], + }); + + me.callParent(); + }, +}); +Ext.define('PVE.window.Restore', { + extend: 'Ext.window.Window', // fixme: Proxmox.window.Edit? + + resizable: false, + width: 500, + modal: true, + layout: 'auto', + border: false, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + '#liveRestore': { + change: function(el, newVal) { + let liveWarning = this.lookupReference('liveWarning'); + liveWarning.setHidden(!newVal); + let start = this.lookupReference('start'); + start.setDisabled(newVal); + }, + }, + 'form': { + validitychange: function(f, valid) { + this.lookupReference('doRestoreBtn').setDisabled(!valid); + }, + }, + }, + + doRestore: function() { + let me = this; + let view = me.getView(); + + let values = view.down('form').getForm().getValues(); + + let params = { + vmid: view.vmid || values.vmid, + force: view.vmid ? 1 : 0, + }; + if (values.unique) { + params.unique = 1; + } + if (values.start && !values['live-restore']) { + params.start = 1; + } + if (values['live-restore']) { + params['live-restore'] = 1; + } + if (values.storage) { + params.storage = values.storage; + } + + ['bwlimit', 'cores', 'name', 'memory', 'sockets'].forEach(opt => { + if ((values[opt] ?? '') !== '') { + params[opt] = values[opt]; + } + }); + + if (params.name && view.vmtype === 'lxc') { + params.hostname = params.name; + delete params.name; + } + + let confirmMsg; + if (view.vmtype === 'lxc') { + params.ostemplate = view.volid; + params.restore = 1; + if (values.unprivileged !== 'keep') { + params.unprivileged = values.unprivileged; + } + confirmMsg = Proxmox.Utils.format_task_description('vzrestore', params.vmid); + } else if (view.vmtype === 'qemu') { + params.archive = view.volid; + confirmMsg = Proxmox.Utils.format_task_description('qmrestore', params.vmid); + } else { + throw 'unknown VM type'; + } + + let executeRestore = () => { + Proxmox.Utils.API2Request({ + url: `/nodes/${view.nodename}/${view.vmtype}`, + params: params, + method: 'POST', + waitMsgTarget: view, + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: function(response, options) { + Ext.create('Proxmox.window.TaskViewer', { + autoShow: true, + upid: response.result.data, + }); + view.close(); + }, + }); + }; + + if (view.vmid) { + confirmMsg += `. ${Ext.String.format( + gettext('This will permanently erase current {0} data.'), + view.vmtype === 'lxc' ? 'CT' : 'VM', + )}`; + if (view.vmtype === 'lxc') { + confirmMsg += `
${gettext('Mount point volumes are also erased.')}`; + } + Ext.Msg.confirm(gettext('Confirm'), confirmMsg, function(btn) { + if (btn === 'yes') { + executeRestore(); + } + }); + } else { + executeRestore(); + } + }, + + afterRender: function() { + let view = this.getView(); + + Proxmox.Utils.API2Request({ + url: `/nodes/${view.nodename}/vzdump/extractconfig`, + method: 'GET', + waitMsgTarget: view, + params: { + volume: view.volid, + }, + failure: response => Ext.Msg.alert('Error', response.htmlStatus), + success: function(response, options) { + let allStoragesAvailable = true; + + response.result.data.split('\n').forEach(line => { + let [_, key, value] = line.match(/^([^:]+):\s*(\S+)\s*$/) ?? []; + + if (!key) { + return; + } + + if (key === '#qmdump#map') { + let match = value.match(/^(\S+):(\S+):(\S*):(\S*):$/) ?? []; + // if a /dev/XYZ disk was backed up, ther is no storage hint + allStoragesAvailable &&= !!match[3] && !!PVE.data.ResourceStore.getById( + `storage/${view.nodename}/${match[3]}`); + } else if (key === 'name' || key === 'hostname') { + view.lookupReference('nameField').setEmptyText(value); + } else if (key === 'memory' || key === 'cores' || key === 'sockets') { + view.lookupReference(`${key}Field`).setEmptyText(value); + } + }); + + if (!allStoragesAvailable) { + let storagesel = view.down('pveStorageSelector[name=storage]'); + storagesel.allowBlank = false; + storagesel.setEmptyText(''); + } + }, + }); + }, + }, + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + if (!me.volid) { + throw "no volume ID specified"; + } + if (!me.vmtype) { + throw "no vmtype specified"; + } + + let storagesel = Ext.create('PVE.form.StorageSelector', { + nodename: me.nodename, + name: 'storage', + value: '', + fieldLabel: gettext('Storage'), + storageContent: me.vmtype === 'lxc' ? 'rootdir' : 'images', + // when restoring a container without specifying a storage, the backend defaults + // to 'local', which is unintuitive and 'rootdir' might not even be allowed on it + allowBlank: me.vmtype !== 'lxc', + emptyText: me.vmtype === 'lxc' ? '' : gettext('From backup configuration'), + autoSelect: me.vmtype === 'lxc', + }); + + let items = [ + { + xtype: 'displayfield', + value: me.volidText || me.volid, + fieldLabel: gettext('Source'), + }, + storagesel, + { + xtype: 'pmxDisplayEditField', + name: 'vmid', + fieldLabel: me.vmtype === 'lxc' ? 'CT' : 'VM', + value: me.vmid, + editable: !me.vmid, + editConfig: { + xtype: 'pveGuestIDSelector', + guestType: me.vmtype, + loadNextFreeID: true, + validateExists: false, + }, + }, + { + xtype: 'pveBandwidthField', + name: 'bwlimit', + backendUnit: 'KiB', + allowZero: true, + fieldLabel: gettext('Bandwidth Limit'), + emptyText: gettext('Defaults to target storage restore limit'), + autoEl: { + tag: 'div', + 'data-qtip': gettext("Use '0' to disable all bandwidth limits."), + }, + }, + { + xtype: 'fieldcontainer', + layout: 'hbox', + items: [{ + xtype: 'proxmoxcheckbox', + name: 'unique', + fieldLabel: gettext('Unique'), + flex: 1, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Autogenerate unique properties, e.g., MAC addresses'), + }, + checked: false, + }, + { + xtype: 'proxmoxcheckbox', + name: 'start', + reference: 'start', + flex: 1, + fieldLabel: gettext('Start after restore'), + labelWidth: 105, + checked: false, + }], + }, + ]; + + if (me.vmtype === 'lxc') { + items.push( + { + xtype: 'radiogroup', + fieldLabel: gettext('Privilege Level'), + reference: 'noVNCScalingGroup', + height: '15px', // renders faster with value assigned + layout: { + type: 'hbox', + algin: 'stretch', + }, + autoEl: { + tag: 'div', + 'data-qtip': + gettext('Choose if you want to keep or override the privilege level of the restored Container.'), + }, + items: [ + { + xtype: 'radiofield', + name: 'unprivileged', + inputValue: 'keep', + boxLabel: gettext('From Backup'), + flex: 1, + checked: true, + }, + { + xtype: 'radiofield', + name: 'unprivileged', + inputValue: '1', + boxLabel: gettext('Unprivileged'), + flex: 1, + }, + { + xtype: 'radiofield', + name: 'unprivileged', + inputValue: '0', + boxLabel: gettext('Privileged'), + flex: 1, + //margin: '0 0 0 10', + }, + ], + }, + ); + } else if (me.vmtype === 'qemu') { + items.push({ + xtype: 'proxmoxcheckbox', + name: 'live-restore', + itemId: 'liveRestore', + flex: 1, + fieldLabel: gettext('Live restore'), + checked: false, + hidden: !me.isPBS, + }, + { + xtype: 'displayfield', + reference: 'liveWarning', + // TODO: Remove once more tested/stable? + value: gettext('Note: If anything goes wrong during the live-restore, new data written by the VM may be lost.'), + userCls: 'pmx-hint', + hidden: true, + }); + } + + items.push({ + xtype: 'fieldset', + title: `${gettext('Override Settings')}:`, + layout: 'hbox', + defaults: { + border: false, + layout: 'anchor', + flex: 1, + }, + items: [ + { + padding: '0 10 0 0', + items: [{ + xtype: 'textfield', + fieldLabel: me.vmtype === 'lxc' ? gettext('Hostname') : gettext('Name'), + name: 'name', + reference: 'nameField', + allowBlank: true, + }, { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Cores'), + name: 'cores', + reference: 'coresField', + minValue: 1, + maxValue: 128, + allowBlank: true, + }], + }, + { + padding: '0 0 0 10', + items: [ + { + xtype: 'pveMemoryField', + fieldLabel: gettext('Memory'), + name: 'memory', + reference: 'memoryField', + value: '', + allowBlank: true, + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Sockets'), + name: 'sockets', + reference: 'socketsField', + minValue: 1, + maxValue: 4, + allowBlank: true, + hidden: me.vmtype !== 'qemu', + disabled: me.vmtype !== 'qemu', + }], + }, + ], + }); + + let title = gettext('Restore') + ": " + (me.vmtype === 'lxc' ? 'CT' : 'VM'); + if (me.vmid) { + title = `${gettext('Overwrite')} ${title} ${me.vmid}`; + } + + Ext.apply(me, { + title: title, + items: [ + { + xtype: 'form', + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%', + }, + items: items, + }, + ], + buttons: [ + { + text: gettext('Restore'), + reference: 'doRestoreBtn', + handler: 'doRestore', + }, + ], + }); + + me.callParent(); + }, +}); +/* + * SafeDestroy window with additional checkboxes for removing guests + */ +Ext.define('PVE.window.SafeDestroyGuest', { + extend: 'Proxmox.window.SafeDestroy', + alias: 'widget.pveSafeDestroyGuest', + + additionalItems: [ + { + xtype: 'proxmoxcheckbox', + name: 'purge', + reference: 'purgeCheckbox', + boxLabel: gettext('Purge from job configurations'), + checked: false, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Remove from replication, HA and backup jobs'), + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'destroyUnreferenced', + reference: 'destroyUnreferencedCheckbox', + boxLabel: gettext('Destroy unreferenced disks owned by guest'), + checked: false, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Scan all enabled storages for unreferenced disks and delete them.'), + }, + }, + ], + + note: gettext('Referenced disks will always be destroyed.'), + + getParams: function() { + let me = this; + + const purgeCheckbox = me.lookupReference('purgeCheckbox'); + me.params.purge = purgeCheckbox.checked ? 1 : 0; + + const destroyUnreferencedCheckbox = me.lookupReference('destroyUnreferencedCheckbox'); + me.params["destroy-unreferenced-disks"] = destroyUnreferencedCheckbox.checked ? 1 : 0; + + return me.callParent(); + }, +}); +/* + * SafeDestroy window with additional checkboxes for removing a storage on the disk level. + */ +Ext.define('PVE.window.SafeDestroyStorage', { + extend: 'Proxmox.window.SafeDestroy', + alias: 'widget.pveSafeDestroyStorage', + + showProgress: true, + + additionalItems: [ + { + xtype: 'proxmoxcheckbox', + name: 'wipeDisks', + reference: 'wipeDisksCheckbox', + boxLabel: gettext('Cleanup Disks'), + checked: true, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Wipe labels and other left-overs'), + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'cleanupConfig', + reference: 'cleanupConfigCheckbox', + boxLabel: gettext('Cleanup Storage Configuration'), + checked: true, + }, + ], + + getParams: function() { + let me = this; + + me.params['cleanup-disks'] = me.lookupReference('wipeDisksCheckbox').checked ? 1 : 0; + me.params['cleanup-config'] = me.lookupReference('cleanupConfigCheckbox').checked ? 1 : 0; + + return me.callParent(); + }, +}); +Ext.define('PVE.window.Settings', { + extend: 'Ext.window.Window', + + width: '800px', + title: gettext('My Settings'), + iconCls: 'fa fa-gear', + modal: true, + bodyPadding: 10, + resizable: false, + + buttons: [ + { + xtype: 'proxmoxHelpButton', + onlineHelp: 'gui_my_settings', + hidden: false, + }, + '->', + { + text: gettext('Close'), + handler: function() { + this.up('window').close(); + }, + }, + ], + + layout: 'hbox', + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + var me = this; + var sp = Ext.state.Manager.getProvider(); + + var username = sp.get('login-username') || Proxmox.Utils.noneText; + me.lookupReference('savedUserName').setValue(Ext.String.htmlEncode(username)); + var vncMode = sp.get('novnc-scaling') || 'auto'; + me.lookupReference('noVNCScalingGroup').setValue({ noVNCScalingField: vncMode }); + + let summarycolumns = sp.get('summarycolumns', 'auto'); + me.lookup('summarycolumns').setValue(summarycolumns); + + me.lookup('guestNotesCollapse').setValue(sp.get('guest-notes-collapse', 'never')); + + var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight']; + settings.forEach(function(setting) { + var val = localStorage.getItem('pve-xterm-' + setting); + if (val !== undefined && val !== null) { + var field = me.lookup(setting); + field.setValue(val); + field.resetOriginalValue(); + } + }); + }, + + set_button_status: function() { + let me = this; + let form = me.lookup('xtermform'); + + let valid = form.isValid(), dirty = form.isDirty(); + let hasValues = Object.values(form.getValues()).some(v => !!v); + + me.lookup('xtermsave').setDisabled(!dirty || !valid); + me.lookup('xtermreset').setDisabled(!hasValues); + }, + + control: { + '#xtermjs form': { + dirtychange: 'set_button_status', + validitychange: 'set_button_status', + }, + '#xtermjs button': { + click: function(button) { + var me = this; + var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight']; + settings.forEach(function(setting) { + var field = me.lookup(setting); + if (button.reference === 'xtermsave') { + var value = field.getValue(); + if (value) { + localStorage.setItem('pve-xterm-' + setting, value); + } else { + localStorage.removeItem('pve-xterm-' + setting); + } + } else if (button.reference === 'xtermreset') { + field.setValue(undefined); + localStorage.removeItem('pve-xterm-' + setting); + } + field.resetOriginalValue(); + }); + me.set_button_status(); + }, + }, + 'button[name=reset]': { + click: function() { + let blacklist = ['GuiCap', 'login-username', 'dash-storages']; + let sp = Ext.state.Manager.getProvider(); + for (const state of Object.keys(sp.state)) { + if (!blacklist.includes(state)) { + sp.clear(state); + } + } + window.location.reload(); + }, + }, + 'button[name=clear-username]': { + click: function() { + let me = this; + me.lookupReference('savedUserName').setValue(Proxmox.Utils.noneText); + Ext.state.Manager.getProvider().clear('login-username'); + }, + }, + 'grid[reference=dashboard-storages]': { + selectionchange: function(grid, selected) { + var me = this; + var sp = Ext.state.Manager.getProvider(); + + // saves the selected storageids as "id1,id2,id3,..." or clears the variable + if (selected.length > 0) { + sp.set('dash-storages', Ext.Array.pluck(selected, 'id').join(',')); + } else { + sp.clear('dash-storages'); + } + }, + afterrender: function(grid) { + let store = grid.getStore(); + let storages = Ext.state.Manager.getProvider().get('dash-storages') || ''; + + let items = []; + storages.split(',').forEach(storage => { + if (storage !== '') { // we have to get the records to be able to select them + let item = store.getById(storage); + if (item) { + items.push(item); + } + } + }); + grid.suspendEvent('selectionchange'); + grid.getSelectionModel().select(items); + grid.resumeEvent('selectionchange'); + }, + }, + 'field[reference=summarycolumns]': { + change: (el, newValue) => Ext.state.Manager.getProvider().set('summarycolumns', newValue), + }, + 'field[reference=guestNotesCollapse]': { + change: (e, v) => Ext.state.Manager.getProvider().set('guest-notes-collapse', v), + }, + }, + }, + + items: [{ + xtype: 'fieldset', + flex: 1, + title: gettext('Webinterface Settings'), + margin: '5', + layout: { + type: 'vbox', + align: 'left', + }, + defaults: { + width: '100%', + margin: '0 0 10 0', + }, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Dashboard Storages'), + labelAlign: 'left', + labelWidth: '50%', + }, + { + xtype: 'grid', + maxHeight: 150, + reference: 'dashboard-storages', + selModel: { + selType: 'checkboxmodel', + }, + columns: [{ + header: gettext('Name'), + dataIndex: 'storage', + flex: 1, + }, { + header: gettext('Node'), + dataIndex: 'node', + flex: 1, + }], + store: { + type: 'diff', + field: ['type', 'storage', 'id', 'node'], + rstore: PVE.data.ResourceStore, + filters: [{ + property: 'type', + value: 'storage', + }], + sorters: ['node', 'storage'], + }, + }, + { + xtype: 'box', + autoEl: { tag: 'hr' }, + }, + { + xtype: 'container', + layout: 'hbox', + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Saved User Name') + ':', + labelWidth: 150, + stateId: 'login-username', + reference: 'savedUserName', + flex: 1, + value: '', + }, + { + xtype: 'button', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + text: gettext('Reset'), + name: 'clear-username', + }, + ], + }, + { + xtype: 'box', + autoEl: { tag: 'hr' }, + }, + { + xtype: 'container', + layout: 'hbox', + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Layout') + ':', + flex: 1, + }, + { + xtype: 'button', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + text: gettext('Reset'), + tooltip: gettext('Reset all layout changes (for example, column widths)'), + name: 'reset', + }, + ], + }, + { + xtype: 'box', + autoEl: { tag: 'hr' }, + }, + { + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Summary columns') + ':', + labelWidth: 150, + stateId: 'summarycolumns', + reference: 'summarycolumns', + comboItems: [ + ['auto', 'auto'], + ['1', '1'], + ['2', '2'], + ['3', '3'], + ], + }, + { + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Guest Notes') + ':', + labelWidth: 150, + stateId: 'guest-notes-collapse', + reference: 'guestNotesCollapse', + comboItems: [ + ['never', 'Show by default'], + ['always', 'Collapse by default'], + ['auto', 'auto (Collapse if empty)'], + ], + }, + ], + }, + { + xtype: 'container', + layout: 'vbox', + flex: 1, + margin: '5', + defaults: { + width: '100%', + // right margin ensures that the right border of the fieldsets + // is shown + margin: '0 2 10 0', + }, + items: [ + { + xtype: 'fieldset', + itemId: 'xtermjs', + title: gettext('xterm.js Settings'), + items: [{ + xtype: 'form', + reference: 'xtermform', + border: false, + layout: { + type: 'vbox', + algin: 'left', + }, + defaults: { + width: '100%', + margin: '0 0 10 0', + }, + items: [ + { + xtype: 'textfield', + name: 'fontFamily', + reference: 'fontFamily', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Font-Family'), + }, + { + xtype: 'proxmoxintegerfield', + emptyText: Proxmox.Utils.defaultText, + name: 'fontSize', + reference: 'fontSize', + minValue: 1, + fieldLabel: gettext('Font-Size'), + }, + { + xtype: 'numberfield', + name: 'letterSpacing', + reference: 'letterSpacing', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Letter Spacing'), + }, + { + xtype: 'numberfield', + name: 'lineHeight', + minValue: 0.1, + reference: 'lineHeight', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Line Height'), + }, + { + xtype: 'container', + layout: { + type: 'hbox', + pack: 'end', + }, + defaults: { + margin: '0 0 0 5', + }, + items: [ + { + xtype: 'button', + reference: 'xtermreset', + disabled: true, + text: gettext('Reset'), + }, + { + xtype: 'button', + reference: 'xtermsave', + disabled: true, + text: gettext('Save'), + }, + ], + }, + ], + }], + }, { + xtype: 'fieldset', + title: gettext('noVNC Settings'), + items: [ + { + xtype: 'radiogroup', + fieldLabel: gettext('Scaling mode'), + reference: 'noVNCScalingGroup', + height: '15px', // renders faster with value assigned + layout: { + type: 'hbox', + }, + items: [ + { + xtype: 'radiofield', + name: 'noVNCScalingField', + inputValue: 'auto', + boxLabel: 'Auto', + }, + { + xtype: 'radiofield', + name: 'noVNCScalingField', + inputValue: 'scale', + boxLabel: 'Local Scaling', + margin: '0 0 0 10', + }, { + xtype: 'radiofield', + name: 'noVNCScalingField', + inputValue: 'off', + boxLabel: 'Off', + margin: '0 0 0 10', + }, + ], + listeners: { + change: function(el, { noVNCScalingField }) { + let provider = Ext.state.Manager.getProvider(); + if (noVNCScalingField === 'auto') { + provider.clear('novnc-scaling'); + } else { + provider.set('novnc-scaling', noVNCScalingField); + } + }, + }, + }, + ], + }, + ], + }], +}); +Ext.define('PVE.window.Snapshot', { + extend: 'Proxmox.window.Edit', + + viewModel: { + data: { + type: undefined, + isCreate: undefined, + running: false, + guestAgentEnabled: false, + }, + formulas: { + runningWithoutGuestAgent: (get) => get('type') === 'qemu' && get('running') && !get('guestAgentEnabled'), + shouldWarnAboutFS: (get) => get('isCreate') && get('runningWithoutGuestAgent') && get('!vmstate.checked'), + }, + }, + + onGetValues: function(values) { + let me = this; + + if (me.type === 'lxc') { + delete values.vmstate; + } + + return values; + }, + + initComponent: function() { + var me = this; + var vm = me.getViewModel(); + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.type) { + throw "no type specified"; + } + + vm.set('type', me.type); + vm.set('running', me.running); + vm.set('isCreate', me.isCreate); + + if (me.type === 'qemu' && me.isCreate) { + Proxmox.Utils.API2Request({ + url: `/nodes/${me.nodename}/${me.type}/${me.vmid}/config`, + params: { 'current': '1' }, + method: 'GET', + success: function(response, options) { + let res = response.result.data; + let enabled = PVE.Parser.parsePropertyString(res.agent, 'enabled'); + vm.set('guestAgentEnabled', !!PVE.Parser.parseBoolean(enabled.enabled)); + }, + }); + } + + me.items = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'snapname', + value: me.snapname, + fieldLabel: gettext('Name'), + vtype: 'ConfigId', + allowBlank: false, + }, + { + xtype: 'displayfield', + hidden: me.isCreate, + disabled: me.isCreate, + name: 'snaptime', + renderer: PVE.Utils.render_timestamp_human_readable, + fieldLabel: gettext('Timestamp'), + }, + { + xtype: 'proxmoxcheckbox', + hidden: me.type !== 'qemu' || !me.isCreate || !me.running, + disabled: me.type !== 'qemu' || !me.isCreate || !me.running, + name: 'vmstate', + reference: 'vmstate', + uncheckedValue: 0, + defaultValue: 0, + checked: 1, + fieldLabel: gettext('Include RAM'), + }, + { + xtype: 'textareafield', + grow: true, + editable: !me.viewonly, + name: 'description', + fieldLabel: gettext('Description'), + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + name: 'fswarning', + hidden: true, + value: gettext('It is recommended to either include the RAM or use the QEMU Guest Agent when taking a snapshot of a running VM to avoid inconsistencies.'), + bind: { + hidden: '{!shouldWarnAboutFS}', + }, + }, + { + title: gettext('Settings'), + hidden: me.isCreate, + xtype: 'grid', + itemId: 'summary', + border: true, + height: 200, + store: { + model: 'KeyValue', + sorters: [ + { + property: 'key', + direction: 'ASC', + }, + ], + }, + columns: [ + { + header: gettext('Key'), + width: 150, + dataIndex: 'key', + }, + { + header: gettext('Value'), + flex: 1, + dataIndex: 'value', + }, + ], + }, + ]; + + me.url = `/nodes/${me.nodename}/${me.type}/${me.vmid}/snapshot`; + + let subject; + if (me.isCreate) { + subject = (me.type === 'qemu' ? 'VM' : 'CT') + me.vmid + ' ' + gettext('Snapshot'); + me.method = 'POST'; + me.showTaskViewer = true; + } else { + subject = `${gettext('Snapshot')} ${me.snapname}`; + me.url += `/${me.snapname}/config`; + } + + Ext.apply(me, { + subject: subject, + width: me.isCreate ? 450 : 620, + height: me.isCreate ? undefined : 420, + }); + + me.callParent(); + + if (!me.snapname) { + return; + } + + me.load({ + success: function(response) { + let kvarray = []; + Ext.Object.each(response.result.data, function(key, value) { + if (key === 'description' || key === 'snaptime') { + return; + } + kvarray.push({ key: key, value: value }); + }); + + let summarystore = me.down('#summary').getStore(); + summarystore.suspendEvents(); + summarystore.add(kvarray); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh', summarystore); + + me.setValues(response.result.data); + }, + }); + }, +}); +Ext.define('PVE.panel.StartupInputPanel', { + extend: 'Proxmox.panel.InputPanel', + onlineHelp: 'qm_startup_and_shutdown', + + onGetValues: function(values) { + var me = this; + + var res = PVE.Parser.printStartup(values); + + if (res === undefined || res === '') { + return { 'delete': 'startup' }; + } + + return { startup: res }; + }, + + setStartup: function(value) { + var me = this; + + var startup = PVE.Parser.parseStartup(value); + if (startup) { + me.setValues(startup); + } + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: 'textfield', + name: 'order', + defaultValue: '', + emptyText: 'any', + fieldLabel: gettext('Start/Shutdown order'), + }, + { + xtype: 'textfield', + name: 'up', + defaultValue: '', + emptyText: 'default', + fieldLabel: gettext('Startup delay'), + }, + { + xtype: 'textfield', + name: 'down', + defaultValue: '', + emptyText: 'default', + fieldLabel: gettext('Shutdown timeout'), + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.window.StartupEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveWindowStartupEdit', + onlineHelp: undefined, + + initComponent: function() { + let me = this; + + let ipanelConfig = me.onlineHelp ? { onlineHelp: me.onlineHelp } : {}; + let ipanel = Ext.create('PVE.panel.StartupInputPanel', ipanelConfig); + + Ext.applyIf(me, { + subject: gettext('Start/Shutdown order'), + fieldDefaults: { + labelWidth: 120, + }, + items: [ipanel], + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + me.vmconfig = response.result.data; + ipanel.setStartup(me.vmconfig.startup); + }, + }); + }, +}); +Ext.define('PVE.window.DownloadUrlToStorage', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveStorageDownloadUrl', + mixins: ['Proxmox.Mixin.CBind'], + + isCreate: true, + + method: 'POST', + + showTaskViewer: true, + + title: gettext('Download from URL'), + submitText: gettext('Download'), + + cbindData: function(initialConfig) { + var me = this; + return { + nodename: me.nodename, + storage: me.storage, + content: me.content, + }; + }, + + cbind: { + url: '/nodes/{nodename}/storage/{storage}/download-url', + }, + + + viewModel: { + data: { + size: '-', + mimetype: '-', + enableQuery: true, + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + urlChange: function(field) { + this.resetMetaInfo(); + this.setQueryEnabled(); + }, + setQueryEnabled: function() { + this.getViewModel().set('enableQuery', true); + }, + resetMetaInfo: function() { + let vm = this.getViewModel(); + vm.set('size', '-'); + vm.set('mimetype', '-'); + }, + + urlCheck: function(field) { + let me = this; + let view = me.getView(); + + const queryParam = view.getValues(); + + me.getViewModel().set('enableQuery', false); + me.resetMetaInfo(); + let urlField = view.down('[name=url]'); + + Proxmox.Utils.API2Request({ + url: `/nodes/${view.nodename}/query-url-metadata`, + method: 'GET', + params: { + url: queryParam.url, + 'verify-certificates': queryParam['verify-certificates'], + }, + waitMsgTarget: view, + failure: res => { + urlField.setValidation(res.result.message); + urlField.validate(); + Ext.MessageBox.alert(gettext('Error'), res.htmlStatus); + // re-enable so one can directly requery, e.g., if it was just a network hiccup + me.setQueryEnabled(); + }, + success: function(res, opt) { + urlField.setValidation(); + urlField.validate(); + + let data = res.result.data; + view.setValues({ + filename: data.filename || "", + size: (data.size && Proxmox.Utils.format_size(data.size)) || gettext("Unknown"), + mimetype: data.mimetype || gettext("Unknown"), + }); + }, + }); + }, + + hashChange: function(field) { + let checksum = Ext.getCmp('downloadUrlChecksum'); + if (field.getValue() === '__default__') { + checksum.setDisabled(true); + checksum.setValue(""); + checksum.allowBlank = true; + } else { + checksum.setDisabled(false); + checksum.allowBlank = false; + } + }, + }, + + items: [ + { + xtype: 'inputpanel', + border: false, + onGetValues: function(values) { + if (typeof values.checksum === 'string') { + values.checksum = values.checksum.trim(); + } + return values; + }, + columnT: [ + { + xtype: 'fieldcontainer', + layout: 'hbox', + fieldLabel: gettext('URL'), + items: [ + { + xtype: 'textfield', + name: 'url', + emptyText: gettext("Enter URL to download"), + allowBlank: false, + flex: 1, + listeners: { + change: 'urlChange', + }, + }, + { + xtype: 'button', + name: 'check', + text: gettext('Query URL'), + margin: '0 0 0 5', + bind: { + disabled: '{!enableQuery}', + }, + listeners: { + click: 'urlCheck', + }, + }, + ], + }, + { + xtype: 'textfield', + name: 'filename', + allowBlank: false, + fieldLabel: gettext('File name'), + emptyText: gettext("Please (re-)query URL to get meta information"), + }, + ], + column1: [ + { + xtype: 'displayfield', + name: 'size', + fieldLabel: gettext('File size'), + bind: { + value: '{size}', + }, + }, + ], + column2: [ + { + xtype: 'displayfield', + name: 'mimetype', + fieldLabel: gettext('MIME type'), + bind: { + value: '{mimetype}', + }, + }, + ], + advancedColumn1: [ + { + xtype: 'pveHashAlgorithmSelector', + name: 'checksum-algorithm', + fieldLabel: gettext('Hash algorithm'), + allowBlank: true, + hasNoneOption: true, + value: '__default__', + listeners: { + change: 'hashChange', + }, + }, + { + xtype: 'textfield', + name: 'checksum', + fieldLabel: gettext('Checksum'), + allowBlank: true, + disabled: true, + emptyText: gettext('none'), + id: 'downloadUrlChecksum', + }, + ], + advancedColumn2: [ + { + xtype: 'proxmoxcheckbox', + name: 'verify-certificates', + fieldLabel: gettext('Verify certificates'), + uncheckedValue: 0, + checked: true, + listeners: { + change: 'setQueryEnabled', + }, + }, + ], + }, + { + xtype: 'hiddenfield', + name: 'content', + cbind: { + value: '{content}', + }, + }, + ], + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + if (!me.storage) { + throw "no storage ID specified"; + } + + me.callParent(); + }, +}); + +Ext.define('PVE.window.UploadToStorage', { + extend: 'Ext.window.Window', + alias: 'widget.pveStorageUpload', + mixins: ['Proxmox.Mixin.CBind'], + + resizable: false, + modal: true, + + title: gettext('Upload'), + + acceptedExtensions: { + iso: ['.img', '.iso'], + vztmpl: ['.tar.gz', '.tar.xz', '.tar.zst'], + }, + + cbindData: function(initialConfig) { + const me = this; + const ext = me.acceptedExtensions[me.content] || []; + + me.url = `/nodes/${me.nodename}/storage/${me.storage}/upload`; + + return { + extensions: ext.join(', '), + filenameRegex: RegExp('^.*(?:' + ext.join('|').replaceAll('.', '\\.') + ')$', 'i'), + }; + }, + + viewModel: { + data: { + size: '-', + mimetype: '-', + filename: '', + }, + }, + + controller: { + submit: function(button) { + const view = this.getView(); + const form = this.lookup('formPanel').getForm(); + const abortBtn = this.lookup('abortBtn'); + const pbar = this.lookup('progressBar'); + + const updateProgress = function(per, bytes) { + let text = (per * 100).toFixed(2) + '%'; + if (bytes) { + text += " (" + Proxmox.Utils.format_size(bytes) + ')'; + } + pbar.updateProgress(per, text); + }; + + const fd = new FormData(); + + button.setDisabled(true); + abortBtn.setDisabled(false); + + fd.append("content", view.content); + + const fileField = form.findField('file'); + const file = fileField.fileInputEl.dom.files[0]; + fileField.setDisabled(true); + + const filenameField = form.findField('filename'); + const filename = filenameField.getValue(); + filenameField.setDisabled(true); + + const algorithmField = form.findField('checksum-algorithm'); + algorithmField.setDisabled(true); + if (algorithmField.getValue() !== '__default__') { + fd.append("checksum-algorithm", algorithmField.getValue()); + + const checksumField = form.findField('checksum'); + fd.append("checksum", checksumField.getValue()?.trim()); + checksumField.setDisabled(true); + } + + fd.append("filename", file, filename); + + pbar.setVisible(true); + updateProgress(0); + + const xhr = new XMLHttpRequest(); + view.xhr = xhr; + + xhr.addEventListener("load", function(e) { + if (xhr.status === 200) { + view.hide(); + + const result = JSON.parse(xhr.response); + const upid = result.data; + Ext.create('Proxmox.window.TaskViewer', { + autoShow: true, + upid: upid, + taskDone: view.taskDone, + listeners: { + destroy: function() { + view.close(); + }, + }, + }); + + return; + } + const err = Ext.htmlEncode(xhr.statusText); + let msg = `${gettext('Error')} ${xhr.status.toString()}: ${err}`; + if (xhr.responseText !== "") { + const result = Ext.decode(xhr.responseText); + result.message = msg; + msg = Proxmox.Utils.extractRequestError(result, true); + } + Ext.Msg.alert(gettext('Error'), msg, btn => view.close()); + }, false); + + xhr.addEventListener("error", function(e) { + const err = e.target.status.toString(); + const msg = `Error '${err}' occurred while receiving the document.`; + Ext.Msg.alert(gettext('Error'), msg, btn => view.close()); + }); + + xhr.upload.addEventListener("progress", function(evt) { + if (evt.lengthComputable) { + const percentComplete = evt.loaded / evt.total; + updateProgress(percentComplete, evt.loaded); + } + }, false); + + xhr.open("POST", `/api2/json${view.url}`, true); + xhr.send(fd); + }, + + validitychange: function(f, valid) { + const submitBtn = this.lookup('submitBtn'); + submitBtn.setDisabled(!valid); + }, + + fileChange: function(input) { + const vm = this.getViewModel(); + const name = input.value.replace(/^.*(\/|\\)/, ''); + const fileInput = input.fileInputEl.dom; + vm.set('filename', name); + vm.set('size', (fileInput.files[0] && Proxmox.Utils.format_size(fileInput.files[0].size)) || '-'); + vm.set('mimetype', (fileInput.files[0] && fileInput.files[0].type) || '-'); + }, + + hashChange: function(field, value) { + const checksum = this.lookup('downloadUrlChecksum'); + if (value === '__default__') { + checksum.setDisabled(true); + checksum.setValue(""); + } else { + checksum.setDisabled(false); + } + }, + }, + + items: [ + { + xtype: 'form', + reference: 'formPanel', + method: 'POST', + waitMsgTarget: true, + bodyPadding: 10, + border: false, + width: 400, + fieldDefaults: { + labelWidth: 100, + anchor: '100%', + }, + items: [ + { + xtype: 'filefield', + name: 'file', + buttonText: gettext('Select File'), + allowBlank: false, + fieldLabel: gettext('File'), + cbind: { + accept: '{extensions}', + }, + listeners: { + change: 'fileChange', + }, + }, + { + xtype: 'textfield', + name: 'filename', + allowBlank: false, + fieldLabel: gettext('File name'), + bind: { + value: '{filename}', + }, + cbind: { + regex: '{filenameRegex}', + }, + regexText: gettext('Wrong file extension'), + }, + { + xtype: 'displayfield', + name: 'size', + fieldLabel: gettext('File size'), + bind: { + value: '{size}', + }, + }, + { + xtype: 'displayfield', + name: 'mimetype', + fieldLabel: gettext('MIME type'), + bind: { + value: '{mimetype}', + }, + }, + { + xtype: 'pveHashAlgorithmSelector', + name: 'checksum-algorithm', + fieldLabel: gettext('Hash algorithm'), + allowBlank: true, + hasNoneOption: true, + value: '__default__', + listeners: { + change: 'hashChange', + }, + }, + { + xtype: 'textfield', + name: 'checksum', + fieldLabel: gettext('Checksum'), + allowBlank: false, + disabled: true, + emptyText: gettext('none'), + reference: 'downloadUrlChecksum', + }, + { + xtype: 'progressbar', + text: 'Ready', + hidden: true, + reference: 'progressBar', + }, + { + xtype: 'hiddenfield', + name: 'content', + cbind: { + value: '{content}', + }, + }, + ], + listeners: { + validitychange: 'validitychange', + }, + }, + ], + + buttons: [ + { + xtype: 'button', + text: gettext('Abort'), + reference: 'abortBtn', + disabled: true, + handler: function() { + const me = this; + me.up('pveStorageUpload').close(); + }, + }, + { + text: gettext('Upload'), + reference: 'submitBtn', + disabled: true, + handler: 'submit', + }, + ], + + listeners: { + close: function() { + const me = this; + if (me.xhr) { + me.xhr.abort(); + delete me.xhr; + } + }, + }, + + initComponent: function() { + const me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + if (!me.storage) { + throw "no storage ID specified"; + } + if (!me.acceptedExtensions[me.content]) { + throw "content type not supported"; + } + + me.callParent(); + }, +}); +Ext.define('PVE.window.ScheduleSimulator', { + extend: 'Ext.window.Window', + + title: gettext('Job Schedule Simulator'), + + viewModel: { + data: { + simulatedOnce: false, + }, + formulas: { + gridEmptyText: get => get('simulatedOnce') ? Proxmox.Utils.NoneText : gettext('No simulation done'), + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + close: function() { + this.getView().close(); + }, + simulate: function() { + let me = this; + let schedule = me.lookup('schedule').getValue(); + if (!schedule) { + return; + } + let iterations = me.lookup('iterations').getValue() || 10; + Proxmox.Utils.API2Request({ + url: '/cluster/jobs/schedule-analyze', + method: 'GET', + params: { + schedule, + iterations, + }, + failure: response => { + me.getViewModel().set('simulatedOnce', true); + me.lookup('grid').getStore().setData([]); + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response) { + let schedules = response.result.data; + me.lookup('grid').getStore().setData(schedules); + me.getViewModel().set('simulatedOnce', true); + }, + }); + }, + + scheduleChanged: function(field, value) { + this.lookup('simulateBtn').setDisabled(!value); + }, + + renderDate: function(value) { + let date = new Date(value*1000); + return date.toLocaleDateString(); + }, + + renderTime: function(value) { + let date = new Date(value*1000); + return date.toLocaleTimeString(); + }, + + init: function(view) { + let me = this; + if (view.schedule) { + me.lookup('schedule').setValue(view.schedule); + } + }, + }, + + bodyPadding: 10, + modal: true, + resizable: false, + width: 600, + + layout: 'fit', + + items: [ + { + xtype: 'inputpanel', + column1: [ + { + xtype: 'pveCalendarEvent', + reference: 'schedule', + fieldLabel: gettext('Schedule'), + listeners: { + change: 'scheduleChanged', + }, + }, + { + xtype: 'proxmoxintegerfield', + reference: 'iterations', + fieldLabel: gettext('Iterations'), + minValue: 1, + maxValue: 100, + value: 10, + }, + { + xtype: 'container', + layout: 'hbox', + items: [ + { + xtype: 'box', + flex: 1, + }, + { + xtype: 'button', + reference: 'simulateBtn', + text: gettext('Simulate'), + handler: 'simulate', + disabled: true, + }, + ], + }, + ], + + column2: [ + { + xtype: 'grid', + reference: 'grid', + bind: { + emptyText: '{gridEmptyText}', + }, + scrollable: true, + height: 300, + columns: [ + { + text: gettext('Date'), + renderer: 'renderDate', + dataIndex: 'timestamp', + flex: 1, + }, + { + text: gettext('Time'), + renderer: 'renderTime', + dataIndex: 'timestamp', + align: 'right', + flex: 1, + }, + ], + store: { + fields: ['timestamp'], + data: [], + sorter: 'timestamp', + }, + }, + ], + }, + ], + + buttons: [ + { + text: gettext('Done'), + handler: 'close', + }, + ], +}); +Ext.define('PVE.window.Wizard', { + extend: 'Ext.window.Window', + + activeTitle: '', // used for automated testing + + width: 720, + height: 510, + + modal: true, + border: false, + + draggable: true, + closable: true, + resizable: false, + + layout: 'border', + + getValues: function(dirtyOnly) { + let me = this; + + let values = {}; + + me.down('form').getForm().getFields().each(field => { + if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) { + Proxmox.Utils.assemble_field_data(values, field.getSubmitData()); + } + }); + + me.query('inputpanel').forEach(panel => { + Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly)); + }); + + return values; + }, + + initComponent: function() { + var me = this; + + var tabs = me.items || []; + delete me.items; + + /* + * Items may have the following functions: + * validator(): per tab custom validation + * onSubmit(): submit handler + * onGetValues(): overwrite getValues results + */ + + Ext.Array.each(tabs, function(tab) { + tab.disabled = true; + }); + tabs[0].disabled = false; + + let maxidx = 0, curidx = 0; + + let check_card = function(card) { + let fields = card.query('field, fieldcontainer'); + if (card.isXType('fieldcontainer')) { + fields.unshift(card); + } + let valid = true; + for (const field of fields) { + // Note: not all fielcontainer have isValid() + if (Ext.isFunction(field.isValid) && !field.isValid()) { + valid = false; + } + } + if (Ext.isFunction(card.validator)) { + return card.validator(); + } + return valid; + }; + + let disableTab = function(card) { + let tp = me.down('#wizcontent'); + for (let idx = tp.items.indexOf(card); idx < tp.items.getCount(); idx++) { + let tab = tp.items.getAt(idx); + if (tab) { + tab.disable(); + } + } + }; + + let tabchange = function(tp, newcard, oldcard) { + if (newcard.onSubmit) { + me.down('#next').setVisible(false); + me.down('#submit').setVisible(true); + } else { + me.down('#next').setVisible(true); + me.down('#submit').setVisible(false); + } + let valid = check_card(newcard); + me.down('#next').setDisabled(!valid); + me.down('#submit').setDisabled(!valid); + me.down('#back').setDisabled(tp.items.indexOf(newcard) === 0); + + let idx = tp.items.indexOf(newcard); + if (idx > maxidx) { + maxidx = idx; + } + curidx = idx; + + let ntab = tp.items.getAt(idx + 1); + if (valid && ntab && !newcard.onSubmit) { + ntab.enable(); + } + }; + + if (me.subject && !me.title) { + me.title = Proxmox.Utils.dialog_title(me.subject, true, false); + } + + let sp = Ext.state.Manager.getProvider(); + let advancedOn = sp.get('proxmox-advanced-cb'); + + Ext.apply(me, { + items: [ + { + xtype: 'form', + region: 'center', + layout: 'fit', + border: false, + margins: '5 5 0 5', + fieldDefaults: { + labelWidth: 100, + anchor: '100%', + }, + items: [{ + itemId: 'wizcontent', + xtype: 'tabpanel', + activeItem: 0, + bodyPadding: 0, + listeners: { + afterrender: function(tp) { + tabchange(tp, this.getActiveTab()); + }, + tabchange: function(tp, newcard, oldcard) { + tabchange(tp, newcard, oldcard); + }, + }, + defaults: { + padding: 10, + }, + items: tabs, + }], + }, + ], + fbar: [ + { + xtype: 'proxmoxHelpButton', + itemId: 'help', + }, + '->', + { + xtype: 'proxmoxcheckbox', + boxLabelAlign: 'before', + boxLabel: gettext('Advanced'), + value: advancedOn, + listeners: { + change: function(_, value) { + let tp = me.down('#wizcontent'); + tp.query('inputpanel').forEach(function(ip) { + ip.setAdvancedVisible(value); + }); + sp.set('proxmox-advanced-cb', value); + }, + }, + }, + { + text: gettext('Back'), + disabled: true, + itemId: 'back', + minWidth: 60, + handler: function() { + let tp = me.down('#wizcontent'); + let prev = tp.items.indexOf(tp.getActiveTab()) - 1; + if (prev < 0) { + return; + } + let ntab = tp.items.getAt(prev); + if (ntab) { + tp.setActiveTab(ntab); + } + }, + }, + { + text: gettext('Next'), + disabled: true, + itemId: 'next', + minWidth: 60, + handler: function() { + let tp = me.down('#wizcontent'); + let activeTab = tp.getActiveTab(); + if (!check_card(activeTab)) { + return; + } + let next = tp.items.indexOf(activeTab) + 1; + let ntab = tp.items.getAt(next); + if (ntab) { + ntab.enable(); + tp.setActiveTab(ntab); + } + }, + }, + { + text: gettext('Finish'), + minWidth: 60, + hidden: true, + itemId: 'submit', + handler: function() { + let tp = me.down('#wizcontent'); + tp.getActiveTab().onSubmit(); + }, + }, + ], + }); + me.callParent(); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + panel.setAdvancedVisible(advancedOn); + }); + + Ext.Array.each(me.query('field'), function(field) { + let validcheck = function() { + let tp = me.down('#wizcontent'); + + // check validity for current to last enabled tab, as local change may affect validity of a later one + for (let i = curidx; i <= maxidx && i < tp.items.getCount(); i++) { + let tab = tp.items.getAt(i); + let valid = check_card(tab); + + // only set the buttons on the current panel + if (i === curidx) { + me.down('#next').setDisabled(!valid); + me.down('#submit').setDisabled(!valid); + } + // if a panel is invalid, then disable all following, else enable the next tab + let nextTab = tp.items.getAt(i + 1); + if (!valid) { + disableTab(nextTab); + return; + } else if (nextTab && !tab.onSubmit) { + nextTab.enable(); + } + } + }; + field.on('change', validcheck); + field.on('validitychange', validcheck); + }); + }, +}); +Ext.define('PVE.window.GuestDiskReassign', { + extend: 'Proxmox.window.Edit', + mixins: ['Proxmox.Mixin.CBind'], + + resizable: false, + modal: true, + width: 350, + border: false, + layout: 'fit', + showReset: false, + showProgress: true, + method: 'POST', + + viewModel: { + data: { + mpType: '', + }, + formulas: { + mpMaxCount: get => get('mpType') === 'mp' + ? PVE.Utils.mp_counts.mps - 1 + : PVE.Utils.mp_counts.unused - 1, + }, + }, + + cbindData: function() { + let me = this; + return { + vmid: me.vmid, + disk: me.disk, + isQemu: me.type === 'qemu', + nodename: me.nodename, + url: () => { + let endpoint = me.type === 'qemu' ? 'move_disk' : 'move_volume'; + return `/nodes/${me.nodename}/${me.type}/${me.vmid}/${endpoint}`; + }, + }; + }, + + cbind: { + title: get => get('isQemu') ? gettext('Reassign Disk') : gettext('Reassign Volume'), + submitText: get => get('title'), + qemu: '{isQemu}', + url: '{url}', + }, + + getValues: function() { + let me = this; + let values = me.formPanel.getForm().getValues(); + + let params = { + vmid: me.vmid, + 'target-vmid': values.targetVmid, + }; + + params[me.qemu ? 'disk' : 'volume'] = me.disk; + + if (me.qemu) { + params['target-disk'] = `${values.controller}${values.deviceid}`; + } else { + params['target-volume'] = `${values.mpType}${values.mpId}`; + } + return params; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + initViewModel: function(model) { + let view = this.getView(); + let mpTypeValue = view.disk.match(/^unused\d+/) ? 'unused' : 'mp'; + model.set('mpType', mpTypeValue); + }, + + onMpTypeChange: function(value) { + let view = this.getView(); + view.getViewModel().set('mpType', value.getValue()); + view.lookup('mpIdSelector').validate(); + }, + + onTargetVMChange: function(f, vmid) { + let me = this; + let view = me.getView(); + let diskSelector = view.lookup('diskSelector'); + if (!vmid) { + diskSelector.setVMConfig(null); + me.VMConfig = null; + return; + } + + let url = `/nodes/${view.nodename}/${view.type}/${vmid}/config`; + Proxmox.Utils.API2Request({ + url: url, + method: 'GET', + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: function({ result }, options) { + if (view.qemu) { + diskSelector.setVMConfig(result.data); + diskSelector.setDisabled(false); + } else { + let mpIdSelector = view.lookup('mpIdSelector'); + let mpType = view.lookup('mpType'); + + view.VMConfig = result.data; + + mpIdSelector.setValue( + PVE.Utils.nextFreeMP( + view.getViewModel().get('mpType'), + view.VMConfig, + ).id, + ); + + mpType.setDisabled(false); + mpIdSelector.setDisabled(false); + mpIdSelector.validate(); + } + }, + }); + }, + }, + + defaultFocus: 'sourceDisk', + items: [ + { + xtype: 'displayfield', + name: 'sourceDisk', + fieldLabel: gettext('Source'), + cbind: { + name: get => get('isQemu') ? 'disk' : 'volume', + value: '{disk}', + }, + allowBlank: false, + }, + { + xtype: 'vmComboSelector', + name: 'targetVmid', + allowBlank: false, + fieldLabel: gettext('Target Guest'), + store: { + model: 'PVEResources', + autoLoad: true, + sorters: 'vmid', + cbind: {}, // for nested cbinds + filters: [ + { + property: 'type', + cbind: { value: '{type}' }, + }, + { + property: 'node', + cbind: { value: '{nodename}' }, + }, + // FIXME: remove, artificial restriction that doesn't gains us anything.. + { + property: 'vmid', + operator: '!=', + cbind: { value: '{vmid}' }, + }, + { + property: 'template', + value: 0, + }, + ], + }, + listeners: { change: 'onTargetVMChange' }, + }, + { + xtype: 'pveControllerSelector', + reference: 'diskSelector', + withUnused: true, + disabled: true, + cbind: { + hidden: '{!isQemu}', + }, + }, + { + xtype: 'container', + layout: 'hbox', + cbind: { + hidden: '{isQemu}', + disabled: '{isQemu}', + }, + items: [ + { + xtype: 'pmxDisplayEditField', + cbind: { + editable: get => !get('disk').match(/^unused\d+/), + value: get => get('disk').match(/^unused\d+/) ? 'unused' : 'mp', + }, + disabled: true, + name: 'mpType', + reference: 'mpType', + fieldLabel: gettext('Add as'), + submitValue: true, + flex: 4, + editConfig: { + xtype: 'proxmoxKVComboBox', + name: 'mpTypeCombo', + deleteEmpty: false, + cbind: { + hidden: '{isQemu}', + }, + comboItems: [ + ['mp', gettext('Mount Point')], + ['unused', gettext('Unused')], + ], + listeners: { change: 'onMpTypeChange' }, + }, + }, + { + xtype: 'proxmoxintegerfield', + name: 'mpId', + reference: 'mpIdSelector', + minValue: 0, + flex: 1, + allowBlank: false, + validateOnChange: true, + disabled: true, + bind: { + maxValue: '{mpMaxCount}', + }, + validator: function(value) { + let view = this.up('window'); + let type = view.getViewModel().get('mpType'); + if (Ext.isDefined(view.VMConfig[`${type}${value}`])) { + return "Mount point is already in use."; + } + return true; + }, + }, + ], + }, + ], + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.type) { + throw "no type specified"; + } + + me.callParent(); + }, +}); +Ext.define('PVE.window.TreeSettingsEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveTreeSettingsEdit', + + title: gettext('Tree Settings'), + isCreate: false, + + url: '#', // ignored as submit() gets overriden here, but the parent class requires it + + width: 450, + fieldDefaults: { + labelWidth: 150, + }, + + items: [ + { + xtype: 'inputpanel', + items: [ + { + xtype: 'proxmoxKVComboBox', + name: 'sort-field', + fieldLabel: gettext('Sort Key'), + comboItems: [ + ['__default__', `${Proxmox.Utils.defaultText} (VMID)`], + ['vmid', 'VMID'], + ['name', gettext('Name')], + ], + defaultValue: '__default__', + value: '__default__', + deleteEmpty: false, + }, + { + xtype: 'proxmoxKVComboBox', + name: 'group-templates', + fieldLabel: gettext('Group Templates'), + comboItems: [ + ['__default__', `${Proxmox.Utils.defaultText} (${gettext("Yes")})`], + [1, gettext('Yes')], + [0, gettext('No')], + ], + defaultValue: '__default__', + value: '__default__', + deleteEmpty: false, + }, + { + xtype: 'proxmoxKVComboBox', + name: 'group-guest-types', + fieldLabel: gettext('Group Guest Types'), + comboItems: [ + ['__default__', `${Proxmox.Utils.defaultText} (${gettext("Yes")})`], + [1, gettext('Yes')], + [0, gettext('No')], + ], + defaultValue: '__default__', + value: '__default__', + deleteEmpty: false, + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext('Settings are saved in the local storage of the browser'), + }, + ], + }, + ], + + submit: function() { + let me = this; + + let localStorage = Ext.state.Manager.getProvider(); + localStorage.set('pve-tree-sorting', me.down('inputpanel').getValues() || null); + + me.apiCallDone(); + me.close(); + }, + + initComponent: function() { + let me = this; + + me.callParent(); + + let localStorage = Ext.state.Manager.getProvider(); + me.down('inputpanel').setValues(localStorage.get('pve-tree-sorting')); + }, + +}); +Ext.define('PVE.ha.FencingView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveFencingView'], + + onlineHelp: 'ha_manager_fencing', + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-ha-fencing', + data: [], + }); + + Ext.apply(me, { + store: store, + stateful: false, + viewConfig: { + trackOver: false, + deferEmptyText: false, + emptyText: 'Use watchdog based fencing.', + }, + columns: [ + { + header: 'Node', + width: 100, + sortable: true, + dataIndex: 'node', + }, + { + header: gettext('Command'), + flex: 1, + dataIndex: 'command', + }, + ], + }); + + me.callParent(); + }, +}, function() { + Ext.define('pve-ha-fencing', { + extend: 'Ext.data.Model', + fields: [ + 'node', 'command', 'digest', + ], + }); +}); +Ext.define('PVE.ha.GroupInputPanel', { + extend: 'Proxmox.panel.InputPanel', + onlineHelp: 'ha_manager_groups', + + groupId: undefined, + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = 'group'; + } + + return values; + }, + + initComponent: function() { + var me = this; + + let update_nodefield, update_node_selection; + + let sm = Ext.create('Ext.selection.CheckboxModel', { + mode: 'SIMPLE', + listeners: { + selectionchange: function(model, selected) { + update_nodefield(selected); + }, + }, + }); + + let store = Ext.create('Ext.data.Store', { + fields: ['node', 'mem', 'cpu', 'priority'], + data: PVE.data.ResourceStore.getNodes(), // use already cached data to avoid an API call + proxy: { + type: 'memory', + reader: { type: 'json' }, + }, + sorters: [ + { + property: 'node', + direction: 'ASC', + }, + ], + }); + + var nodegrid = Ext.createWidget('grid', { + store: store, + border: true, + height: 300, + selModel: sm, + columns: [ + { + header: gettext('Node'), + flex: 1, + dataIndex: 'node', + }, + { + header: gettext('Memory usage') + " %", + renderer: PVE.Utils.render_mem_usage_percent, + sortable: true, + width: 150, + dataIndex: 'mem', + }, + { + header: gettext('CPU usage'), + renderer: Proxmox.Utils.render_cpu, + sortable: true, + width: 150, + dataIndex: 'cpu', + }, + { + header: 'Priority', + xtype: 'widgetcolumn', + dataIndex: 'priority', + sortable: true, + stopSelection: true, + widget: { + xtype: 'proxmoxintegerfield', + minValue: 0, + maxValue: 1000, + isFormField: false, + listeners: { + change: function(numberfield, value, old_value) { + let record = numberfield.getWidgetRecord(); + record.set('priority', value); + update_nodefield(sm.getSelection()); + record.commit(); + }, + }, + }, + }, + ], + }); + + let nodefield = Ext.create('Ext.form.field.Hidden', { + name: 'nodes', + value: '', + listeners: { + change: function(field, value) { + update_node_selection(value); + }, + }, + isValid: function() { + let value = this.getValue(); + return value && value.length !== 0; + }, + }); + + update_node_selection = function(string) { + sm.deselectAll(true); + + string.split(',').forEach(function(e, idx, array) { + let [node, priority] = e.split(':'); + store.each(function(record) { + if (record.get('node') === node) { + sm.select(record, true); + record.set('priority', priority); + record.commit(); + } + }); + }); + nodegrid.reconfigure(store); + }; + + update_nodefield = function(selected) { + let nodes = selected + .map(({ data }) => data.node + (data.priority ? `:${data.priority}` : '')) + .join(','); + + // nodefield change listener calls us again, which results in a + // endless recursion, suspend the event temporary to avoid this + nodefield.suspendEvent('change'); + nodefield.setValue(nodes); + nodefield.resumeEvent('change'); + }; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'group', + value: me.groupId || '', + fieldLabel: 'ID', + vtype: 'StorageId', + allowBlank: false, + }, + nodefield, + ]; + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'restricted', + uncheckedValue: 0, + fieldLabel: 'restricted', + }, + { + xtype: 'proxmoxcheckbox', + name: 'nofailback', + uncheckedValue: 0, + fieldLabel: 'nofailback', + }, + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment'), + }, + nodegrid, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.ha.GroupEdit', { + extend: 'Proxmox.window.Edit', + + groupId: undefined, + + initComponent: function() { + var me = this; + + me.isCreate = !me.groupId; + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/ha/groups'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/ha/groups/' + me.groupId; + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.ha.GroupInputPanel', { + isCreate: me.isCreate, + groupId: me.groupId, + }); + + Ext.apply(me, { + subject: gettext('HA Group'), + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + + ipanel.setValues(values); + }, + }); + } + }, +}); +Ext.define('PVE.ha.GroupSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveHAGroupSelector'], + + value: [], + autoSelect: false, + valueField: 'group', + displayField: 'group', + listConfig: { + columns: [ + { + header: gettext('Group'), + width: 100, + sortable: true, + dataIndex: 'group', + }, + { + header: gettext('Nodes'), + width: 100, + sortable: false, + dataIndex: 'nodes', + }, + { + header: gettext('Comment'), + flex: 1, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + }, + ], + }, + store: { + model: 'pve-ha-groups', + sorters: { + property: 'group', + direction: 'ASC', + }, + }, + + initComponent: function() { + var me = this; + me.callParent(); + me.getStore().load(); + }, + +}, function() { + Ext.define('pve-ha-groups', { + extend: 'Ext.data.Model', + fields: [ + 'group', 'type', 'digest', 'nodes', 'comment', + { + name: 'restricted', + type: 'boolean', + }, + { + name: 'nofailback', + type: 'boolean', + }, + ], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/ha/groups", + }, + idProperty: 'group', + }); +}); +Ext.define('PVE.ha.GroupsView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveHAGroupsView'], + + onlineHelp: 'ha_manager_groups', + + stateful: true, + stateId: 'grid-ha-groups', + + initComponent: function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + var store = new Ext.data.Store({ + model: 'pve-ha-groups', + sorters: { + property: 'group', + direction: 'ASC', + }, + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + Ext.create('PVE.ha.GroupEdit', { + groupId: rec.data.group, + listeners: { + destroy: () => store.load(), + }, + autoShow: true, + }); + }; + + let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/ha/groups/', + callback: () => store.load(), + }); + let edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + Ext.apply(me, { + store: store, + selModel: sm, + viewConfig: { + trackOver: false, + }, + tbar: [ + { + text: gettext('Create'), + disabled: !caps.nodes['Sys.Console'], + handler: function() { + Ext.create('PVE.ha.GroupEdit', { + listeners: { + destroy: () => store.load(), + }, + autoShow: true, + }); + }, + }, + edit_btn, + remove_btn, + ], + columns: [ + { + header: gettext('Group'), + width: 150, + sortable: true, + dataIndex: 'group', + }, + { + header: 'restricted', + width: 100, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'restricted', + }, + { + header: 'nofailback', + width: 100, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'nofailback', + }, + { + header: gettext('Nodes'), + flex: 1, + sortable: false, + dataIndex: 'nodes', + }, + { + header: gettext('Comment'), + flex: 1, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + }, + ], + listeners: { + activate: reload, + beforeselect: (grid, record, index, eOpts) => caps.nodes['Sys.Console'], + itemdblclick: run_editor, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.ha.VMResourceInputPanel', { + extend: 'Proxmox.panel.InputPanel', + onlineHelp: 'ha_manager_resource_config', + vmid: undefined, + + onGetValues: function(values) { + var me = this; + + if (values.vmid) { + values.sid = values.vmid; + } + delete values.vmid; + + PVE.Utils.delete_if_default(values, 'group', '', me.isCreate); + PVE.Utils.delete_if_default(values, 'max_restart', '1', me.isCreate); + PVE.Utils.delete_if_default(values, 'max_relocate', '1', me.isCreate); + + return values; + }, + + initComponent: function() { + var me = this; + var MIN_QUORUM_VOTES = 3; + + var disabledHint = Ext.createWidget({ + xtype: 'displayfield', // won't get submitted by default + userCls: 'pmx-hint', + value: 'Disabling the resource will stop the guest system. ' + + 'See the online help for details.', + hidden: true, + }); + + var fewVotesHint = Ext.createWidget({ + itemId: 'fewVotesHint', + xtype: 'displayfield', + userCls: 'pmx-hint', + value: 'At least three quorum votes are recommended for reliable HA.', + hidden: true, + }); + + Proxmox.Utils.API2Request({ + url: '/cluster/config/nodes', + method: 'GET', + failure: function(response) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response) { + var nodes = response.result.data; + var votes = 0; + Ext.Array.forEach(nodes, function(node) { + var vote = parseInt(node.quorum_votes, 10); // parse as base 10 + votes += vote || 0; // parseInt might return NaN, which is false + }); + + if (votes < MIN_QUORUM_VOTES) { + fewVotesHint.setVisible(true); + } + }, + }); + + var vmidStore = me.vmid ? {} : { + model: 'PVEResources', + autoLoad: true, + sorters: 'vmid', + filters: [ + { + property: 'type', + value: /lxc|qemu/, + }, + { + property: 'hastate', + value: /unmanaged/, + }, + ], + }; + + // value is a string above, but a number below + me.column1 = [ + { + xtype: me.vmid ? 'displayfield' : 'vmComboSelector', + submitValue: me.isCreate, + name: 'vmid', + fieldLabel: me.vmid && me.guestType === 'ct' ? 'CT' : 'VM', + value: me.vmid, + store: vmidStore, + validateExists: true, + }, + { + xtype: 'proxmoxintegerfield', + name: 'max_restart', + fieldLabel: gettext('Max. Restart'), + value: 1, + minValue: 0, + maxValue: 10, + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'max_relocate', + fieldLabel: gettext('Max. Relocate'), + value: 1, + minValue: 0, + maxValue: 10, + allowBlank: false, + }, + ]; + + me.column2 = [ + { + xtype: 'pveHAGroupSelector', + name: 'group', + fieldLabel: gettext('Group'), + }, + { + xtype: 'proxmoxKVComboBox', + name: 'state', + value: 'started', + fieldLabel: gettext('Request State'), + comboItems: [ + ['started', 'started'], + ['stopped', 'stopped'], + ['ignored', 'ignored'], + ['disabled', 'disabled'], + ], + listeners: { + 'change': function(field, newValue) { + if (newValue === 'disabled') { + disabledHint.setVisible(true); + } else if (disabledHint.isVisible()) { + disabledHint.setVisible(false); + } + }, + }, + }, + disabledHint, + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment'), + }, + fewVotesHint, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.ha.VMResourceEdit', { + extend: 'Proxmox.window.Edit', + + vmid: undefined, + guestType: undefined, + isCreate: undefined, + + initComponent: function() { + var me = this; + + if (me.isCreate === undefined) { + me.isCreate = !me.vmid; + } + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/ha/resources'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/ha/resources/' + me.vmid; + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.ha.VMResourceInputPanel', { + isCreate: me.isCreate, + vmid: me.vmid, + guestType: me.guestType, + }); + + Ext.apply(me, { + subject: gettext('Resource') + ': ' + gettext('Container') + + '/' + gettext('Virtual Machine'), + isAdd: true, + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + + var regex = /^(\S+):(\S+)$/; + var res = regex.exec(values.sid); + + if (res[1] !== 'vm' && res[1] !== 'ct') { + throw "got unexpected resource type"; + } + + values.vmid = res[2]; + + ipanel.setValues(values); + }, + }); + } + }, +}); +Ext.define('PVE.ha.ResourcesView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveHAResourcesView'], + + onlineHelp: 'ha_manager_resources', + + stateful: true, + stateId: 'grid-ha-resources', + + initComponent: function() { + let me = this; + + if (!me.rstore) { + throw "no store given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + let store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + filters: { + property: 'type', + value: 'service', + }, + }); + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + let sid = rec.data.sid; + + let res = sid.match(/^(\S+):(\S+)$/); + if (!res || (res[1] !== 'vm' && res[1] !== 'ct')) { + console.warn(`unknown HA service ID type ${sid}`); + return; + } + let [, guestType, vmid] = res; + Ext.create('PVE.ha.VMResourceEdit', { + guestType: guestType, + vmid: vmid, + listeners: { + destroy: () => me.rstore.load(), + }, + autoShow: true, + }); + }; + + let caps = Ext.state.Manager.get('GuiCap'); + + Ext.apply(me, { + store: store, + selModel: sm, + viewConfig: { + trackOver: false, + }, + tbar: [ + { + text: gettext('Add'), + disabled: !caps.nodes['Sys.Console'], + handler: function() { + Ext.create('PVE.ha.VMResourceEdit', { + listeners: { + destroy: () => me.rstore.load(), + }, + autoShow: true, + }); + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }, + { + xtype: 'proxmoxStdRemoveButton', + selModel: sm, + getUrl: function(rec) { + return `/cluster/ha/resources/${rec.get('sid')}`; + }, + callback: () => me.rstore.load(), + }, + ], + columns: [ + { + header: 'ID', + width: 100, + sortable: true, + dataIndex: 'sid', + }, + { + header: gettext('State'), + width: 100, + sortable: true, + dataIndex: 'state', + }, + { + header: gettext('Node'), + width: 100, + sortable: true, + dataIndex: 'node', + }, + { + header: gettext('Request State'), + width: 100, + hidden: true, + sortable: true, + renderer: v => v || 'started', + dataIndex: 'request_state', + }, + { + header: gettext('CRM State'), + width: 100, + hidden: true, + sortable: true, + dataIndex: 'crm_state', + }, + { + header: gettext('Name'), + width: 100, + sortable: true, + dataIndex: 'vname', + }, + { + header: gettext('Max. Restart'), + width: 100, + sortable: true, + renderer: (v) => v === undefined ? '1' : v, + dataIndex: 'max_restart', + }, + { + header: gettext('Max. Relocate'), + width: 100, + sortable: true, + renderer: (v) => v === undefined ? '1' : v, + dataIndex: 'max_relocate', + }, + { + header: gettext('Group'), + width: 200, + sortable: true, + renderer: function(value, metaData, { data }) { + if (data.errors && data.errors.group) { + metaData.tdCls = 'proxmox-invalid-row'; + let html = `

${Ext.htmlEncode(data.errors.group)}

`; + metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + html + '"'; + } + return value; + }, + dataIndex: 'group', + }, + { + header: gettext('Description'), + flex: 1, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + }, + ], + listeners: { + beforeselect: (grid, record, index, eOpts) => caps.nodes['Sys.Console'], + itemdblclick: run_editor, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.ha.Status', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveHAStatus', + + onlineHelp: 'chapter_ha_manager', + layout: { + type: 'vbox', + align: 'stretch', + }, + + initComponent: function() { + var me = this; + + me.rstore = Ext.create('Proxmox.data.ObjectStore', { + interval: me.interval, + model: 'pve-ha-status', + storeid: 'pve-store-' + ++Ext.idSeed, + groupField: 'type', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/ha/status/current', + }, + }); + + me.items = [{ + xtype: 'pveHAStatusView', + title: gettext('Status'), + rstore: me.rstore, + border: 0, + collapsible: true, + padding: '0 0 20 0', + }, { + xtype: 'pveHAResourcesView', + flex: 1, + collapsible: true, + title: gettext('Resources'), + border: 0, + rstore: me.rstore, + }]; + + me.callParent(); + me.on('activate', me.rstore.startUpdate); + }, +}); +Ext.define('PVE.ha.StatusView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveHAStatusView'], + + onlineHelp: 'chapter_ha_manager', + + sortPriority: { + quorum: 1, + master: 2, + lrm: 3, + service: 4, + }, + + initComponent: function() { + var me = this; + + if (!me.rstore) { + throw "no rstore given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + sortAfterUpdate: true, + sorters: [{ + sorterFn: function(rec1, rec2) { + var p1 = me.sortPriority[rec1.data.type]; + var p2 = me.sortPriority[rec2.data.type]; + return p1 !== p2 ? p1 > p2 ? 1 : -1 : 0; + }, + }], + filters: { + property: 'type', + value: 'service', + operator: '!=', + }, + }); + + Ext.apply(me, { + store: store, + stateful: false, + viewConfig: { + trackOver: false, + }, + columns: [ + { + header: gettext('Type'), + width: 80, + dataIndex: 'type', + }, + { + header: gettext('Status'), + width: 80, + flex: 1, + dataIndex: 'status', + }, + ], + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + }, +}, function() { + Ext.define('pve-ha-status', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'type', 'node', 'status', 'sid', + 'state', 'group', 'comment', + 'max_restart', 'max_relocate', 'type', + 'crm_state', 'request_state', + { + name: 'vname', + convert: function(value, record) { + let sid = record.data.sid; + if (!sid) return ''; + + let res = sid.match(/^(\S+):(\S+)$/); + if (res[1] !== 'vm' && res[1] !== 'ct') { + return '-'; + } + let vmid = res[2]; + return PVE.data.ResourceStore.guestName(vmid); + }, + }, + ], + idProperty: 'id', + }); +}); +Ext.define('PVE.dc.ACLAdd', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveACLAdd'], + + url: '/access/acl', + method: 'PUT', + isAdd: true, + isCreate: true, + + width: 400, + + initComponent: function() { + let me = this; + + let items = [ + { + xtype: me.path ? 'hiddenfield' : 'pvePermPathSelector', + name: 'path', + value: me.path, + allowBlank: false, + fieldLabel: gettext('Path'), + }, + ]; + + if (me.aclType === 'group') { + me.subject = gettext("Group Permission"); + items.push({ + xtype: 'pveGroupSelector', + name: 'groups', + fieldLabel: gettext('Group'), + }); + } else if (me.aclType === 'user') { + me.subject = gettext("User Permission"); + items.push({ + xtype: 'pmxUserSelector', + name: 'users', + fieldLabel: gettext('User'), + }); + } else if (me.aclType === 'token') { + me.subject = gettext("API Token Permission"); + items.push({ + xtype: 'pveTokenSelector', + name: 'tokens', + fieldLabel: gettext('API Token'), + }); + } else { + throw "unknown ACL type"; + } + + items.push({ + xtype: 'pmxRoleSelector', + name: 'roles', + value: 'NoAccess', + fieldLabel: gettext('Role'), + }); + + if (!me.path) { + items.push({ + xtype: 'proxmoxcheckbox', + name: 'propagate', + checked: true, + uncheckedValue: 0, + fieldLabel: gettext('Propagate'), + }); + } + + let ipanel = Ext.create('Proxmox.panel.InputPanel', { + items: items, + onlineHelp: 'pveum_permission_management', + }); + + Ext.apply(me, { + items: [ipanel], + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.dc.ACLView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveACLView'], + + onlineHelp: 'chapter_user_management', + + stateful: true, + stateId: 'grid-acls', + + // use fixed path + path: undefined, + + initComponent: function() { + let me = this; + + let store = Ext.create('Ext.data.Store', { + model: 'pve-acl', + proxy: { + type: 'proxmox', + url: "/api2/json/access/acl", + }, + sorters: { + property: 'path', + direction: 'ASC', + }, + }); + + if (me.path) { + store.addFilter(Ext.create('Ext.util.Filter', { + filterFn: item => item.data.path === me.path, + })); + } + + let render_ugid = function(ugid, metaData, record) { + if (record.data.type === 'group') { + return '@' + ugid; + } + + return Ext.String.htmlEncode(ugid); + }; + + let columns = [ + { + header: gettext('User') + '/' + gettext('Group') + '/' + gettext('API Token'), + flex: 1, + sortable: true, + renderer: render_ugid, + dataIndex: 'ugid', + }, + { + header: gettext('Role'), + flex: 1, + sortable: true, + dataIndex: 'roleid', + }, + ]; + + if (!me.path) { + columns.unshift({ + header: gettext('Path'), + flex: 1, + sortable: true, + dataIndex: 'path', + }); + columns.push({ + header: gettext('Propagate'), + width: 80, + sortable: true, + dataIndex: 'propagate', + }); + } + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: gettext('Are you sure you want to remove this entry'), + handler: function(btn, event, rec) { + var params = { + 'delete': 1, + path: rec.data.path, + roles: rec.data.roleid, + }; + if (rec.data.type === 'group') { + params.groups = rec.data.ugid; + } else if (rec.data.type === 'user') { + params.users = rec.data.ugid; + } else if (rec.data.type === 'token') { + params.tokens = rec.data.ugid; + } else { + throw 'unknown data type'; + } + + Proxmox.Utils.API2Request({ + url: '/access/acl', + params: params, + method: 'PUT', + waitMsgTarget: me, + callback: () => store.load(), + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + }, + }); + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + menu: { + xtype: 'menu', + items: [ + { + text: gettext('Group Permission'), + iconCls: 'fa fa-fw fa-group', + handler: function() { + var win = Ext.create('PVE.dc.ACLAdd', { + aclType: 'group', + path: me.path, + }); + win.on('destroy', () => store.load()); + win.show(); + }, + }, + { + text: gettext('User Permission'), + iconCls: 'fa fa-fw fa-user', + handler: function() { + var win = Ext.create('PVE.dc.ACLAdd', { + aclType: 'user', + path: me.path, + }); + win.on('destroy', () => store.load()); + win.show(); + }, + }, + { + text: gettext('API Token Permission'), + iconCls: 'fa fa-fw fa-user-o', + handler: function() { + let win = Ext.create('PVE.dc.ACLAdd', { + aclType: 'token', + path: me.path, + }); + win.on('destroy', () => store.load()); + win.show(); + }, + }, + ], + }, + }, + remove_btn, + ], + viewConfig: { + trackOver: false, + }, + columns: columns, + listeners: { + activate: () => store.load(), + }, + }); + + me.callParent(); + }, +}, function() { + Ext.define('pve-acl', { + extend: 'Ext.data.Model', + fields: [ + 'path', 'type', 'ugid', 'roleid', + { + name: 'propagate', + type: 'boolean', + }, + ], + }); +}); +Ext.define('pve-acme-accounts', { + extend: 'Ext.data.Model', + fields: ['name'], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/acme/account", + }, + idProperty: 'name', +}); + +Ext.define('pve-acme-plugins', { + extend: 'Ext.data.Model', + fields: ['type', 'plugin', 'api'], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/acme/plugins", + }, + idProperty: 'plugin', +}); + +Ext.define('PVE.dc.ACMEAccountView', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveACMEAccountView', + + title: gettext('Accounts'), + + controller: { + xclass: 'Ext.app.ViewController', + + addAccount: function() { + let me = this; + let view = me.getView(); + let defaultExists = view.getStore().findExact('name', 'default') !== -1; + Ext.create('PVE.node.ACMEAccountCreate', { + defaultExists, + taskDone: function() { + me.reload(); + }, + }).show(); + }, + + viewAccount: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (selection.length < 1) return; + Ext.create('PVE.node.ACMEAccountView', { + accountname: selection[0].data.name, + }).show(); + }, + + reload: function() { + let me = this; + let view = me.getView(); + view.getStore().rstore.load(); + }, + + showTaskAndReload: function(options, success, response) { + let me = this; + if (!success) return; + + let upid = response.result.data; + Ext.create('Proxmox.window.TaskProgress', { + upid, + taskDone: function() { + me.reload(); + }, + }).show(); + }, + }, + + minHeight: 150, + emptyText: gettext('No Accounts configured'), + + columns: [ + { + dataIndex: 'name', + text: gettext('Name'), + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Add'), + selModel: false, + handler: 'addAccount', + }, + { + xtype: 'proxmoxButton', + text: gettext('View'), + handler: 'viewAccount', + disabled: true, + }, + { + xtype: 'proxmoxStdRemoveButton', + baseurl: '/cluster/acme/account', + callback: 'showTaskAndReload', + }, + ], + + listeners: { + itemdblclick: 'viewAccount', + }, + + store: { + type: 'diff', + autoDestroy: true, + autoDestroyRstore: true, + rstore: { + type: 'update', + storeid: 'pve-acme-accounts', + model: 'pve-acme-accounts', + autoStart: true, + }, + sorters: 'name', + }, +}); + +Ext.define('PVE.dc.ACMEPluginView', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveACMEPluginView', + + title: gettext('Challenge Plugins'), + + controller: { + xclass: 'Ext.app.ViewController', + + addPlugin: function() { + let me = this; + Ext.create('PVE.dc.ACMEPluginEditor', { + isCreate: true, + apiCallDone: function() { + me.reload(); + }, + }).show(); + }, + + editPlugin: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (selection.length < 1) return; + let plugin = selection[0].data.plugin; + Ext.create('PVE.dc.ACMEPluginEditor', { + url: `/cluster/acme/plugins/${plugin}`, + apiCallDone: function() { + me.reload(); + }, + }).show(); + }, + + reload: function() { + let me = this; + let view = me.getView(); + view.getStore().rstore.load(); + }, + }, + + minHeight: 150, + emptyText: gettext('No Plugins configured'), + + columns: [ + { + dataIndex: 'plugin', + text: gettext('Plugin'), + renderer: Ext.String.htmlEncode, + flex: 1, + }, + { + dataIndex: 'api', + text: 'API', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Add'), + handler: 'addPlugin', + selModel: false, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + handler: 'editPlugin', + disabled: true, + }, + { + xtype: 'proxmoxStdRemoveButton', + baseurl: '/cluster/acme/plugins', + callback: 'reload', + }, + ], + + listeners: { + itemdblclick: 'editPlugin', + }, + + store: { + type: 'diff', + autoDestroy: true, + autoDestroyRstore: true, + rstore: { + type: 'update', + storeid: 'pve-acme-plugins', + model: 'pve-acme-plugins', + autoStart: true, + filters: item => !!item.data.api, + }, + sorters: 'plugin', + }, +}); + +Ext.define('PVE.dc.ACMEClusterView', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveACMEClusterView', + + onlineHelp: 'sysadmin_certificate_management', + + items: [ + { + region: 'north', + border: false, + xtype: 'pveACMEAccountView', + }, + { + region: 'center', + border: false, + xtype: 'pveACMEPluginView', + }, + ], +}); +Ext.define('PVE.dc.ACMEPluginEditor', { + extend: 'Proxmox.window.Edit', + xtype: 'pveACMEPluginEditor', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'sysadmin_certs_acme_plugins', + + isAdd: true, + isCreate: false, + + width: 550, + url: '/cluster/acme/plugins/', + + subject: 'ACME DNS Plugin', + + items: [ + { + xtype: 'inputpanel', + // we dynamically create fields from the given schema + // things we have to do here: + // * save which fields we created to remove them again + // * split the data from the generic 'data' field into the boxes + // * on deletion collect those values again + // * save the original values of the data field + createdFields: {}, + createdInitially: false, + originalValues: {}, + createSchemaFields: function(schema) { + let me = this; + // we know where to add because we define it right below + let container = me.down('container'); + let datafield = me.down('field[name=data]'); + let hintfield = me.down('field[name=hint]'); + if (!me.createdInitially) { + [me.originalValues] = PVE.Parser.parseACMEPluginData(datafield.getValue()); + } + + // collect values from custom fields and add it to 'data'', + // then remove the custom fields + let data = []; + for (const [name, field] of Object.entries(me.createdFields)) { + let value = field.getValue(); + if (value !== undefined && value !== null && value !== '') { + data.push(`${name}=${value}`); + } + container.remove(field); + } + let datavalue = datafield.getValue(); + if (datavalue !== undefined && datavalue !== null && datavalue !== '') { + data.push(datavalue); + } + datafield.setValue(data.join('\n')); + + me.createdFields = {}; + + if (typeof schema.fields !== 'object') { + schema.fields = {}; + } + // create custom fields according to schema + let gotSchemaField = false; + let cmp = (a, b) => a[0].localeCompare(b[0]); + for (const [name, definition] of Object.entries(schema.fields).sort(cmp)) { + let xtype; + switch (definition.type) { + case 'string': + xtype = 'proxmoxtextfield'; + break; + case 'integer': + xtype = 'proxmoxintegerfield'; + break; + case 'number': + xtype = 'numberfield'; + break; + default: + console.warn(`unknown type '${definition.type}'`); + xtype = 'proxmoxtextfield'; + break; + } + + let label = name; + if (typeof definition.name === "string") { + label = definition.name; + } + + let field = Ext.create({ + xtype, + name: `custom_${name}`, + fieldLabel: label, + width: '100%', + labelWidth: 150, + labelSeparator: '=', + emptyText: definition.default || '', + autoEl: definition.description ? { + tag: 'div', + 'data-qtip': definition.description, + } : undefined, + }); + + me.createdFields[name] = field; + container.add(field); + gotSchemaField = true; + } + datafield.setHidden(gotSchemaField); // prefer schema-fields + + if (schema.description) { + hintfield.setValue(schema.description); + hintfield.setHidden(false); + } else { + hintfield.setValue(''); + hintfield.setHidden(true); + } + + // parse data from field and set it to the custom ones + let extradata = []; + [data, extradata] = PVE.Parser.parseACMEPluginData(datafield.getValue()); + for (const [key, value] of Object.entries(data)) { + if (me.createdFields[key]) { + me.createdFields[key].setValue(value); + me.createdFields[key].originalValue = me.originalValues[key]; + } else { + extradata.push(`${key}=${value}`); + } + } + datafield.setValue(extradata.join('\n')); + if (!me.createdInitially) { + datafield.resetOriginalValue(); + me.createdInitially = true; // save that we initally set that + } + }, + onGetValues: function(values) { + let me = this; + let win = me.up('pveACMEPluginEditor'); + if (win.isCreate) { + values.id = values.plugin; + values.type = 'dns'; // the only one for now + } + delete values.plugin; + + PVE.Utils.delete_if_default(values, 'validation-delay', '30', win.isCreate); + + let data = ''; + for (const [name, field] of Object.entries(me.createdFields)) { + let value = field.getValue(); + if (value !== null && value !== undefined && value !== '') { + data += `${name}=${value}\n`; + } + delete values[`custom_${name}`]; + } + values.data = Ext.util.Base64.encode(data + values.data); + return values; + }, + items: [ + { + xtype: 'pmxDisplayEditField', + cbind: { + editable: (get) => get('isCreate'), + submitValue: (get) => get('isCreate'), + }, + editConfig: { + flex: 1, + xtype: 'proxmoxtextfield', + allowBlank: false, + }, + name: 'plugin', + labelWidth: 150, + fieldLabel: gettext('Plugin ID'), + }, + { + xtype: 'proxmoxintegerfield', + name: 'validation-delay', + labelWidth: 150, + fieldLabel: gettext('Validation Delay'), + emptyText: 30, + cbind: { + deleteEmpty: '{!isCreate}', + }, + minValue: 0, + maxValue: 48*60*60, + }, + { + xtype: 'pveACMEApiSelector', + name: 'api', + labelWidth: 150, + listeners: { + change: function(selector) { + let schema = selector.getSchema(); + selector.up('inputpanel').createSchemaFields(schema); + }, + }, + }, + { + xtype: 'textarea', + fieldLabel: gettext('API Data'), + labelWidth: 150, + name: 'data', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Hint'), + labelWidth: 150, + name: 'hint', + hidden: true, + }, + ], + }, + ], + + initComponent: function() { + var me = this; + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, opts) { + me.setValues(response.result.data); + }, + }); + } else { + me.method = 'POST'; + } + }, +}); +Ext.define('PVE.panel.AuthBase', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveAuthBasePanel', + + type: '', + + onGetValues: function(values) { + let me = this; + + if (!values.port) { + if (!me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'port' }); + } + delete values.port; + } + + if (me.isCreate) { + values.type = me.type; + } + + return values; + }, + + initComponent: function() { + let me = this; + + let options = PVE.Utils.authSchema[me.type]; + + if (!me.column1) { me.column1 = []; } + if (!me.column2) { me.column2 = []; } + if (!me.columnB) { me.columnB = []; } + + // first field is name + me.column1.unshift({ + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'realm', + fieldLabel: gettext('Realm'), + value: me.realm, + allowBlank: false, + }); + + // last field is default' + me.column1.push({ + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Default'), + name: 'default', + uncheckedValue: 0, + }); + + if (options.tfa) { + // last field of column2is tfa + me.column2.push({ + xtype: 'pveTFASelector', + deleteEmpty: !me.isCreate, + }); + } + + me.columnB.push({ + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment'), + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.dc.AuthEditBase', { + extend: 'Proxmox.window.Edit', + + onlineHelp: 'pveum_authentication_realms', + + isAdd: true, + + fieldDefaults: { + labelWidth: 120, + }, + + initComponent: function() { + var me = this; + + me.isCreate = !me.realm; + + if (me.isCreate) { + me.url = '/api2/extjs/access/domains'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/access/domains/' + me.realm; + me.method = 'PUT'; + } + + let authConfig = PVE.Utils.authSchema[me.authType]; + if (!authConfig) { + throw 'unknown auth type'; + } else if (!authConfig.add && me.isCreate) { + throw 'trying to add non addable realm'; + } + + me.subject = authConfig.name; + + let items; + let bodyPadding; + if (authConfig.syncipanel) { + bodyPadding = 0; + items = { + xtype: 'tabpanel', + region: 'center', + layout: 'fit', + bodyPadding: 10, + items: [ + { + title: gettext('General'), + realm: me.realm, + xtype: authConfig.ipanel, + isCreate: me.isCreate, + type: me.authType, + }, + { + title: gettext('Sync Options'), + realm: me.realm, + xtype: authConfig.syncipanel, + isCreate: me.isCreate, + type: me.authType, + }, + ], + }; + } else { + items = [{ + realm: me.realm, + xtype: authConfig.ipanel, + isCreate: me.isCreate, + type: me.authType, + }]; + } + + Ext.apply(me, { + items, + bodyPadding, + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var data = response.result.data || {}; + // just to be sure (should not happen) + if (data.type !== me.authType) { + me.close(); + throw "got wrong auth type"; + } + me.setValues(data); + }, + }); + } + }, +}); +Ext.define('PVE.panel.ADInputPanel', { + extend: 'PVE.panel.AuthBase', + xtype: 'pveAuthADPanel', + + initComponent: function() { + let me = this; + + if (me.type !== 'ad') { + throw 'invalid type'; + } + + me.column1 = [ + { + xtype: 'textfield', + name: 'domain', + fieldLabel: gettext('Domain'), + emptyText: 'company.net', + allowBlank: false, + }, + ]; + + me.column2 = [ + { + xtype: 'textfield', + fieldLabel: gettext('Server'), + name: 'server1', + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Fallback Server'), + deleteEmpty: !me.isCreate, + name: 'server2', + }, + { + xtype: 'proxmoxintegerfield', + name: 'port', + fieldLabel: gettext('Port'), + minValue: 1, + maxValue: 65535, + emptyText: gettext('Default'), + submitEmptyText: false, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'SSL', + name: 'secure', + uncheckedValue: 0, + listeners: { + change: function(field, newValue) { + let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]'); + if (newValue === true) { + verifyCheckbox.enable(); + } else { + verifyCheckbox.disable(); + verifyCheckbox.setValue(0); + } + }, + }, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Verify Certificate'), + name: 'verify', + unceckedValue: 0, + disabled: true, + checked: false, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Verify SSL certificate of the server'), + }, + }, + ]; + + me.callParent(); + }, + onGetValues: function(values) { + let me = this; + + if (!values.verify) { + if (!me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'verify' }); + } + delete values.verify; + } + + return me.callParent([values]); + }, +}); +Ext.define('PVE.panel.LDAPInputPanel', { + extend: 'PVE.panel.AuthBase', + xtype: 'pveAuthLDAPPanel', + + initComponent: function() { + let me = this; + + if (me.type !== 'ldap') { + throw 'invalid type'; + } + + me.column1 = [ + { + xtype: 'textfield', + name: 'base_dn', + fieldLabel: gettext('Base Domain Name'), + emptyText: 'CN=Users,DC=Company,DC=net', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'user_attr', + emptyText: 'uid / sAMAccountName', + fieldLabel: gettext('User Attribute Name'), + allowBlank: false, + }, + ]; + + me.column2 = [ + { + xtype: 'textfield', + fieldLabel: gettext('Server'), + name: 'server1', + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Fallback Server'), + deleteEmpty: !me.isCreate, + name: 'server2', + }, + { + xtype: 'proxmoxintegerfield', + name: 'port', + fieldLabel: gettext('Port'), + minValue: 1, + maxValue: 65535, + emptyText: gettext('Default'), + submitEmptyText: false, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'SSL', + name: 'secure', + uncheckedValue: 0, + listeners: { + change: function(field, newValue) { + let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]'); + if (newValue === true) { + verifyCheckbox.enable(); + } else { + verifyCheckbox.disable(); + verifyCheckbox.setValue(0); + } + }, + }, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Verify Certificate'), + name: 'verify', + unceckedValue: 0, + disabled: true, + checked: false, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Verify SSL certificate of the server'), + }, + }, + ]; + + me.callParent(); + }, + onGetValues: function(values) { + let me = this; + + if (!values.verify) { + if (!me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'verify' }); + } + delete values.verify; + } + + return me.callParent([values]); + }, +}); + +Ext.define('PVE.panel.LDAPSyncInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveAuthLDAPSyncPanel', + + editableAttributes: ['email'], + editableDefaults: ['scope', 'enable-new'], + default_opts: {}, + sync_attributes: {}, + + // (de)construct the sync-attributes from the list above, + // not touching all others + onGetValues: function(values) { + let me = this; + me.editableDefaults.forEach((attr) => { + if (values[attr]) { + me.default_opts[attr] = values[attr]; + delete values[attr]; + } else { + delete me.default_opts[attr]; + } + }); + let vanished_opts = []; + ['acl', 'entry', 'properties'].forEach((prop) => { + if (values[`remove-vanished-${prop}`]) { + vanished_opts.push(prop); + } + delete values[`remove-vanished-${prop}`]; + }); + me.default_opts['remove-vanished'] = vanished_opts.join(';'); + + values['sync-defaults-options'] = PVE.Parser.printPropertyString(me.default_opts); + me.editableAttributes.forEach((attr) => { + if (values[attr]) { + me.sync_attributes[attr] = values[attr]; + delete values[attr]; + } else { + delete me.sync_attributes[attr]; + } + }); + values.sync_attributes = PVE.Parser.printPropertyString(me.sync_attributes); + + PVE.Utils.delete_if_default(values, 'sync-defaults-options'); + PVE.Utils.delete_if_default(values, 'sync_attributes'); + + // Force values.delete to be an array + if (typeof values.delete === 'string') { + values.delete = values.delete.split(','); + } + + if (me.isCreate) { + delete values.delete; // on create we cannot delete values + } + + return values; + }, + + setValues: function(values) { + let me = this; + if (values.sync_attributes) { + me.sync_attributes = PVE.Parser.parsePropertyString(values.sync_attributes); + delete values.sync_attributes; + me.editableAttributes.forEach((attr) => { + if (me.sync_attributes[attr]) { + values[attr] = me.sync_attributes[attr]; + } + }); + } + if (values['sync-defaults-options']) { + me.default_opts = PVE.Parser.parsePropertyString(values['sync-defaults-options']); + delete values.default_opts; + me.editableDefaults.forEach((attr) => { + if (me.default_opts[attr]) { + values[attr] = me.default_opts[attr]; + } + }); + + if (me.default_opts['remove-vanished']) { + let opts = me.default_opts['remove-vanished'].split(';'); + for (const opt of opts) { + values[`remove-vanished-${opt}`] = 1; + } + } + } + return me.callParent([values]); + }, + + column1: [ + { + xtype: 'proxmoxtextfield', + name: 'bind_dn', + deleteEmpty: true, + emptyText: Proxmox.Utils.noneText, + fieldLabel: gettext('Bind User'), + }, + { + xtype: 'proxmoxtextfield', + inputType: 'password', + name: 'password', + emptyText: gettext('Unchanged'), + fieldLabel: gettext('Bind Password'), + }, + { + xtype: 'proxmoxtextfield', + name: 'email', + fieldLabel: gettext('E-Mail attribute'), + }, + { + xtype: 'proxmoxtextfield', + name: 'group_name_attr', + deleteEmpty: true, + fieldLabel: gettext('Groupname attr.'), + }, + { + xtype: 'displayfield', + value: gettext('Default Sync Options'), + }, + { + xtype: 'proxmoxKVComboBox', + name: 'scope', + emptyText: Proxmox.Utils.NoneText, + fieldLabel: gettext('Scope'), + value: '__default__', + deleteEmpty: false, + comboItems: [ + ['__default__', Proxmox.Utils.NoneText], + ['users', gettext('Users')], + ['groups', gettext('Groups')], + ['both', gettext('Users and Groups')], + ], + }, + ], + + column2: [ + { + xtype: 'proxmoxtextfield', + name: 'user_classes', + fieldLabel: gettext('User classes'), + deleteEmpty: true, + emptyText: 'inetorgperson, posixaccount, person, user', + }, + { + xtype: 'proxmoxtextfield', + name: 'group_classes', + fieldLabel: gettext('Group classes'), + deleteEmpty: true, + emptyText: 'groupOfNames, group, univentionGroup, ipausergroup', + }, + { + xtype: 'proxmoxtextfield', + name: 'filter', + fieldLabel: gettext('User Filter'), + deleteEmpty: true, + }, + { + xtype: 'proxmoxtextfield', + name: 'group_filter', + fieldLabel: gettext('Group Filter'), + deleteEmpty: true, + }, + { + // fake for spacing + xtype: 'displayfield', + value: ' ', + }, + { + xtype: 'proxmoxKVComboBox', + value: '__default__', + deleteEmpty: false, + comboItems: [ + [ + '__default__', + Ext.String.format( + gettext("{0} ({1})"), + Proxmox.Utils.yesText, + Proxmox.Utils.defaultText, + ), + ], + ['1', Proxmox.Utils.yesText], + ['0', Proxmox.Utils.noText], + ], + name: 'enable-new', + fieldLabel: gettext('Enable new users'), + }, + ], + + columnB: [ + { + xtype: 'fieldset', + title: gettext('Remove Vanished Options'), + items: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('ACL'), + name: 'remove-vanished-acl', + boxLabel: gettext('Remove ACLs of vanished users and groups.'), + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Entry'), + name: 'remove-vanished-entry', + boxLabel: gettext('Remove vanished user and group entries.'), + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Properties'), + name: 'remove-vanished-properties', + boxLabel: gettext('Remove vanished properties from synced users.'), + }, + ], + }, + ], +}); +Ext.define('PVE.panel.OpenIDInputPanel', { + extend: 'PVE.panel.AuthBase', + xtype: 'pveAuthOpenIDPanel', + mixins: ['Proxmox.Mixin.CBind'], + + onGetValues: function(values) { + let me = this; + + if (!values.verify) { + if (!me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'verify' }); + } + delete values.verify; + } + + return me.callParent([values]); + }, + + columnT: [ + { + xtype: 'textfield', + name: 'issuer-url', + fieldLabel: gettext('Issuer URL'), + allowBlank: false, + }, + ], + + column1: [ + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Client ID'), + name: 'client-id', + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Client Key'), + cbind: { + deleteEmpty: '{!isCreate}', + }, + name: 'client-key', + }, + ], + + column2: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Autocreate Users'), + name: 'autocreate', + value: 0, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'pmxDisplayEditField', + name: 'username-claim', + fieldLabel: gettext('Username Claim'), + editConfig: { + xtype: 'proxmoxKVComboBox', + editable: true, + comboItems: [ + ['__default__', Proxmox.Utils.defaultText], + ['subject', 'subject'], + ['username', 'username'], + ['email', 'email'], + ], + }, + cbind: { + value: get => get('isCreate') ? '__default__' : Proxmox.Utils.defaultText, + deleteEmpty: '{!isCreate}', + editable: '{isCreate}', + }, + }, + { + xtype: 'proxmoxtextfield', + name: 'scopes', + fieldLabel: gettext('Scopes'), + emptyText: `${Proxmox.Utils.defaultText} (email profile)`, + submitEmpty: false, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'proxmoxKVComboBox', + name: 'prompt', + fieldLabel: gettext('Prompt'), + editable: true, + emptyText: gettext('Auth-Provider Default'), + comboItems: [ + ['__default__', gettext('Auth-Provider Default')], + ['none', 'none'], + ['login', 'login'], + ['consent', 'consent'], + ['select_account', 'select_account'], + ], + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + ], + + advancedColumnB: [ + { + xtype: 'proxmoxtextfield', + name: 'acr-values', + fieldLabel: gettext('ACR Values'), + submitEmpty: false, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + ], + + initComponent: function() { + let me = this; + + if (me.type !== 'openid') { + throw 'invalid type'; + } + + me.callParent(); + }, +}); + +Ext.define('PVE.dc.AuthView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveAuthView'], + + onlineHelp: 'pveum_authentication_realms', + + stateful: true, + stateId: 'grid-authrealms', + + viewConfig: { + trackOver: false, + }, + + columns: [ + { + header: gettext('Realm'), + width: 100, + sortable: true, + dataIndex: 'realm', + }, + { + header: gettext('Type'), + width: 100, + sortable: true, + dataIndex: 'type', + }, + { + header: gettext('TFA'), + width: 100, + sortable: true, + dataIndex: 'tfa', + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + + store: { + model: 'pmx-domains', + sorters: { + property: 'realm', + direction: 'ASC', + }, + }, + + openEditWindow: function(authType, realm) { + let me = this; + Ext.create('PVE.dc.AuthEditBase', { + authType, + realm, + listeners: { + destroy: () => me.reload(), + }, + }).show(); + }, + + reload: function() { + let me = this; + me.getStore().load(); + }, + + run_editor: function() { + let me = this; + let rec = me.getSelection()[0]; + if (!rec) { + return; + } + me.openEditWindow(rec.data.type, rec.data.realm); + }, + + open_sync_window: function() { + let me = this; + let rec = me.getSelection()[0]; + if (!rec) { + return; + } + Ext.create('PVE.dc.SyncWindow', { + realm: rec.data.realm, + listeners: { + destroy: () => me.reload(), + }, + }).show(); + }, + + initComponent: function() { + var me = this; + + let items = []; + for (const [authType, config] of Object.entries(PVE.Utils.authSchema)) { + if (!config.add) { continue; } + items.push({ + text: config.name, + iconCls: 'fa fa-fw ' + (config.iconCls || 'fa-address-book-o'), + handler: () => me.openEditWindow(authType), + }); + } + + Ext.apply(me, { + tbar: [ + { + text: gettext('Add'), + menu: { + items: items, + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + disabled: true, + handler: () => me.run_editor(), + }, + { + xtype: 'proxmoxStdRemoveButton', + baseurl: '/access/domains/', + enableFn: (rec) => PVE.Utils.authSchema[rec.data.type].add, + callback: () => me.reload(), + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Sync'), + disabled: true, + enableFn: (rec) => Boolean(PVE.Utils.authSchema[rec.data.type].syncipanel), + handler: () => me.open_sync_window(), + }, + ], + listeners: { + activate: () => me.reload(), + itemdblclick: () => me.run_editor(), + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.dc.BackupDiskTree', { + extend: 'Ext.tree.Panel', + alias: 'widget.pveBackupDiskTree', + + folderSort: true, + rootVisible: false, + + store: { + sorters: 'id', + data: {}, + }, + + tools: [ + { + type: 'expand', + tooltip: gettext('Expand All'), + callback: panel => panel.expandAll(), + }, + { + type: 'collapse', + tooltip: gettext('Collapse All'), + callback: panel => panel.collapseAll(), + }, + ], + + columns: [ + { + xtype: 'treecolumn', + text: gettext('Guest Image'), + renderer: function(value, meta, record) { + if (record.data.type) { + // guest level + let ret = value; + if (record.data.name) { + ret += " (" + record.data.name + ")"; + } + return ret; + } else { + // extJS needs unique IDs but we only want to show the volumes key from "vmid:key" + return value.split(':')[1] + " - " + record.data.name; + } + }, + dataIndex: 'id', + flex: 6, + }, + { + text: gettext('Type'), + dataIndex: 'type', + flex: 1, + }, + { + text: gettext('Backup Job'), + renderer: PVE.Utils.render_backup_status, + dataIndex: 'included', + flex: 3, + }, + ], + + reload: function() { + let me = this; + let sm = me.getSelectionModel(); + + Proxmox.Utils.API2Request({ + url: `/cluster/backup/${me.jobid}/included_volumes`, + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + }, + success: function(response, opts) { + sm.deselectAll(); + me.setRootNode(response.result.data); + me.expandAll(); + }, + }); + }, + + initComponent: function() { + var me = this; + + if (!me.jobid) { + throw "no job id specified"; + } + + var sm = Ext.create('Ext.selection.TreeModel', {}); + + Ext.apply(me, { + selModel: sm, + fields: ['id', 'type', + { + type: 'string', + name: 'iconCls', + calculate: function(data) { + var txt = 'fa x-fa-tree fa-'; + if (data.leaf && !data.type) { + return txt + 'hdd-o'; + } else if (data.type === 'qemu') { + return txt + 'desktop'; + } else if (data.type === 'lxc') { + return txt + 'cube'; + } else { + return txt + 'question-circle'; + } + }, + }, + ], + header: { + items: [{ + xtype: 'textfield', + fieldLabel: gettext('Search'), + labelWidth: 50, + emptyText: 'Name, VMID, Type', + width: 200, + padding: '0 5 0 0', + enableKeyEvents: true, + listeners: { + buffer: 500, + keyup: function(field) { + let searchValue = field.getValue().toLowerCase(); + me.store.clearFilter(true); + me.store.filterBy(function(record) { + let data = {}; + if (record.data.depth === 0) { + return true; + } else if (record.data.depth === 1) { + data = record.data; + } else if (record.data.depth === 2) { + data = record.parentNode.data; + } + + for (const property of ['name', 'id', 'type']) { + if (!data[property]) { + continue; + } + let v = data[property].toString(); + if (v !== undefined) { + v = v.toLowerCase(); + if (v.includes(searchValue)) { + return true; + } + } + } + return false; + }); + }, + }, + }], + }, + }); + + me.callParent(); + + me.reload(); + }, +}); + +Ext.define('PVE.dc.BackupInfo', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveBackupInfo', + + viewModel: { + data: { + retentionType: 'none', + }, + formulas: { + hasRetention: (get) => get('retentionType') !== 'none', + retentionKeepAll: (get) => get('retentionType') === 'all', + }, + }, + + padding: '5 0 5 10', + + column1: [ + { + xtype: 'displayfield', + name: 'node', + fieldLabel: gettext('Node'), + renderer: value => value || `-- ${gettext('All')} --`, + }, + { + xtype: 'displayfield', + name: 'storage', + fieldLabel: gettext('Storage'), + }, + { + xtype: 'displayfield', + name: 'schedule', + fieldLabel: gettext('Schedule'), + }, + { + xtype: 'displayfield', + name: 'next-run', + fieldLabel: gettext('Next Run'), + renderer: PVE.Utils.render_next_event, + }, + { + xtype: 'displayfield', + name: 'selMode', + fieldLabel: gettext('Selection mode'), + }, + ], + column2: [ + { + xtype: 'displayfield', + name: 'mailnotification', + fieldLabel: gettext('Notification'), + renderer: function(value) { + let mailto = this.up('pveBackupInfo')?.record?.mailto || 'root@localhost'; + let when = gettext('Always'); + if (value === 'failure') { + when = gettext('On failure only'); + } + return `${when} (${mailto})`; + }, + }, + { + xtype: 'displayfield', + name: 'compress', + fieldLabel: gettext('Compression'), + }, + { + xtype: 'displayfield', + name: 'mode', + fieldLabel: gettext('Mode'), + renderer: function(value) { + const modeToDisplay = { + snapshot: gettext('Snapshot'), + stop: gettext('Stop'), + suspend: gettext('Snapshot'), + }; + return modeToDisplay[value] ?? gettext('Unknown'); + }, + }, + { + xtype: 'displayfield', + name: 'enabled', + fieldLabel: gettext('Enabled'), + renderer: v => PVE.Parser.parseBoolean(v.toString()) ? gettext('Yes') : gettext('No'), + }, + { + xtype: 'displayfield', + name: 'pool', + fieldLabel: gettext('Pool to backup'), + }, + ], + + columnB: [ + { + xtype: 'displayfield', + name: 'comment', + fieldLabel: gettext('Comment'), + }, + { + xtype: 'fieldset', + title: gettext('Retention Configuration'), + layout: 'hbox', + collapsible: true, + defaults: { + border: false, + layout: 'anchor', + flex: 1, + }, + bind: { + hidden: '{!hasRetention}', + }, + items: [ + { + padding: '0 10 0 0', + defaults: { + labelWidth: 110, + }, + items: [{ + xtype: 'displayfield', + name: 'keep-all', + fieldLabel: gettext('Keep All'), + renderer: Proxmox.Utils.format_boolean, + bind: { + hidden: '{!retentionKeepAll}', + }, + }].concat( + [ + ['keep-last', gettext('Keep Last')], + ['keep-hourly', gettext('Keep Hourly')], + ].map( + name => ({ + xtype: 'displayfield', + name: name[0], + fieldLabel: name[1], + bind: { + hidden: '{!hasRetention || retentionKeepAll}', + }, + }), + ), + ), + }, + { + padding: '0 0 0 10', + defaults: { + labelWidth: 110, + }, + items: [ + ['keep-daily', gettext('Keep Daily')], + ['keep-weekly', gettext('Keep Weekly')], + ].map( + name => ({ + xtype: 'displayfield', + name: name[0], + fieldLabel: name[1], + bind: { + hidden: '{!hasRetention || retentionKeepAll}', + }, + }), + ), + }, + { + padding: '0 0 0 10', + defaults: { + labelWidth: 110, + }, + items: [ + ['keep-monthly', gettext('Keep Monthly')], + ['keep-yearly', gettext('Keep Yearly')], + ].map( + name => ({ + xtype: 'displayfield', + name: name[0], + fieldLabel: name[1], + bind: { + hidden: '{!hasRetention || retentionKeepAll}', + }, + }), + ), + }, + ], + }, + ], + + setValues: function(values) { + var me = this; + let vm = me.getViewModel(); + + Ext.iterate(values, function(fieldId, val) { + let field = me.query('[isFormField][name=' + fieldId + ']')[0]; + if (field) { + field.setValue(val); + } + }); + + if (values['prune-backups'] || values.maxfiles !== undefined) { + let keepValues; + if (values['prune-backups']) { + keepValues = values['prune-backups']; + } else if (values.maxfiles > 0) { + keepValues = { 'keep-last': values.maxfiles }; + } else { + keepValues = { 'keep-all': 1 }; + } + + vm.set('retentionType', keepValues['keep-all'] ? 'all' : 'other'); + + // set values of all keep-X fields + ['all', 'last', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'].forEach(time => { + let name = `keep-${time}`; + me.query(`[isFormField][name=${name}]`)[0]?.setValue(keepValues[name]); + }); + } else { + vm.set('retentionType', 'none'); + } + + // selection Mode depends on the presence/absence of several keys + let selModeField = me.query('[isFormField][name=selMode]')[0]; + let selMode = 'none'; + if (values.vmid) { + selMode = gettext('Include selected VMs'); + } + if (values.all) { + selMode = gettext('All'); + } + if (values.exclude) { + selMode = gettext('Exclude selected VMs'); + } + if (values.pool) { + selMode = gettext('Pool based'); + } + selModeField.setValue(selMode); + + if (!values.pool) { + let poolField = me.query('[isFormField][name=pool]')[0]; + poolField.setVisible(0); + } + }, + + initComponent: function() { + var me = this; + + if (!me.record) { + throw "no data provided"; + } + me.callParent(); + + me.setValues(me.record); + }, +}); + + +Ext.define('PVE.dc.BackedGuests', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveBackedGuests', + + stateful: true, + stateId: 'grid-dc-backed-guests', + + textfilter: '', + + columns: [ + { + header: gettext('Type'), + dataIndex: "type", + renderer: PVE.Utils.render_resource_type, + flex: 1, + sortable: true, + }, + { + header: gettext('VMID'), + dataIndex: 'vmid', + flex: 1, + sortable: true, + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 2, + sortable: true, + }, + ], + viewConfig: { + stripeRows: true, + trackOver: false, + }, + + initComponent: function() { + let me = this; + + me.store.clearFilter(true); + + Ext.apply(me, { + tbar: [ + '->', + gettext('Search') + ':', + ' ', + { + xtype: 'textfield', + width: 200, + emptyText: 'Name, VMID, Type', + enableKeyEvents: true, + listeners: { + buffer: 500, + keyup: function(field) { + let searchValue = field.getValue().toLowerCase(); + me.store.clearFilter(true); + me.store.filterBy(function(record) { + let data = record.data; + for (const property of ['name', 'vmid', 'type']) { + if (data[property] === null) { + continue; + } + let v = data[property].toString(); + if (v !== undefined) { + if (v.toLowerCase().includes(searchValue)) { + return true; + } + } + } + return false; + }); + }, + }, + }, + ], + }); + me.callParent(); + }, +}); +Ext.define('PVE.dc.BackupEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcBackupEdit'], + + mixins: ['Proxmox.Mixin.CBind'], + + defaultFocus: undefined, + + subject: gettext("Backup Job"), + bodyPadding: 0, + + url: '/api2/extjs/cluster/backup', + method: 'POST', + isCreate: true, + + cbindData: function() { + let me = this; + if (me.jobid) { + me.isCreate = false; + me.method = 'PUT'; + me.url += `/${me.jobid}`; + } + return {}; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + onGetValues: function(values) { + let me = this; + let isCreate = me.getView().isCreate; + if (!values.node) { + if (!isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' }); + } + delete values.node; + } + + if (!values.id && isCreate) { + values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13); + } + + let selMode = values.selMode; + delete values.selMode; + + if (selMode === 'all') { + values.all = 1; + values.exclude = ''; + delete values.vmid; + } else if (selMode === 'exclude') { + values.all = 1; + values.exclude = values.vmid; + delete values.vmid; + } else if (selMode === 'pool') { + delete values.vmid; + } + + if (selMode !== 'pool') { + delete values.pool; + } + return values; + }, + + nodeChange: function(f, value) { + let me = this; + me.lookup('storageSelector').setNodename(value); + let vmgrid = me.lookup('vmgrid'); + let store = vmgrid.getStore(); + + store.clearFilter(); + store.filterBy(function(rec) { + return !value || rec.get('node') === value; + }); + + let mode = me.lookup('modeSelector').getValue(); + if (mode === 'all') { + vmgrid.selModel.selectAll(true); + } + if (mode === 'pool') { + me.selectPoolMembers(); + } + }, + + storageChange: function(f, v) { + let me = this; + let rec = f.getStore().findRecord('storage', v, 0, false, true, true); + let compressionSelector = me.lookup('compressionSelector'); + + if (rec?.data?.type === 'pbs') { + compressionSelector.setValue('zstd'); + compressionSelector.setDisabled(true); + } else if (!compressionSelector.getEditable()) { + compressionSelector.setDisabled(false); + } + }, + + selectPoolMembers: function() { + let me = this; + let vmgrid = me.lookup('vmgrid'); + let poolid = me.lookup('poolSelector').getValue(); + + vmgrid.getSelectionModel().deselectAll(true); + if (!poolid) { + return; + } + vmgrid.getStore().filter([ + { + id: 'poolFilter', + property: 'pool', + value: poolid, + }, + ]); + vmgrid.selModel.selectAll(true); + }, + + modeChange: function(f, value, oldValue) { + let me = this; + let vmgrid = me.lookup('vmgrid'); + vmgrid.getStore().removeFilter('poolFilter'); + + if (oldValue === 'all' && value !== 'all') { + vmgrid.getSelectionModel().deselectAll(true); + } + + if (value === 'all') { + vmgrid.getSelectionModel().selectAll(true); + } + + if (value === 'pool') { + me.selectPoolMembers(); + } + }, + + init: function(view) { + let me = this; + if (view.isCreate) { + me.lookup('modeSelector').setValue('include'); + } else { + view.load({ + success: function(response, _options) { + let data = response.result.data; + + if (data.exclude) { + data.vmid = data.exclude; + data.selMode = 'exclude'; + } else if (data.all) { + data.vmid = ''; + data.selMode = 'all'; + } else if (data.pool) { + data.selMode = 'pool'; + data.selPool = data.pool; + } else { + data.selMode = 'include'; + } + + me.getViewModel().set('selMode', data.selMode); + + if (data['prune-backups']) { + Object.assign(data, data['prune-backups']); + delete data['prune-backups']; + } else if (data.maxfiles !== undefined) { + if (data.maxfiles > 0) { + data['keep-last'] = data.maxfiles; + } else { + data['keep-all'] = 1; + } + delete data.maxfiles; + } + + if (data['notes-template']) { + data['notes-template'] = + PVE.Utils.unEscapeNotesTemplate(data['notes-template']); + } + + view.setValues(data); + }, + }); + } + }, + }, + + viewModel: { + data: { + selMode: 'include', + }, + + formulas: { + poolMode: (get) => get('selMode') === 'pool', + disableVMSelection: (get) => get('selMode') !== 'include' && get('selMode') !== 'exclude', + }, + }, + + items: [ + { + xtype: 'tabpanel', + region: 'center', + layout: 'fit', + bodyPadding: 10, + items: [ + { + xtype: 'container', + title: gettext('General'), + region: 'center', + layout: { + type: 'vbox', + align: 'stretch', + }, + items: [ + { + xtype: 'inputpanel', + onlineHelp: 'chapter_vzdump', + column1: [ + { + xtype: 'pveNodeSelector', + name: 'node', + fieldLabel: gettext('Node'), + allowBlank: true, + editable: true, + autoSelect: false, + emptyText: '-- ' + gettext('All') + ' --', + listeners: { + change: 'nodeChange', + }, + }, + { + xtype: 'pveStorageSelector', + reference: 'storageSelector', + fieldLabel: gettext('Storage'), + clusterView: true, + storageContent: 'backup', + allowBlank: false, + name: 'storage', + listeners: { + change: 'storageChange', + }, + }, + { + xtype: 'pveCalendarEvent', + fieldLabel: gettext('Schedule'), + allowBlank: false, + name: 'schedule', + }, + { + xtype: 'proxmoxKVComboBox', + reference: 'modeSelector', + comboItems: [ + ['include', gettext('Include selected VMs')], + ['all', gettext('All')], + ['exclude', gettext('Exclude selected VMs')], + ['pool', gettext('Pool based')], + ], + fieldLabel: gettext('Selection mode'), + name: 'selMode', + value: '', + bind: { + value: '{selMode}', + }, + listeners: { + change: 'modeChange', + }, + }, + { + xtype: 'pvePoolSelector', + reference: 'poolSelector', + fieldLabel: gettext('Pool to backup'), + hidden: true, + allowBlank: false, + name: 'pool', + listeners: { + change: 'selectPoolMembers', + }, + bind: { + hidden: '{!poolMode}', + disabled: '{!poolMode}', + }, + }, + ], + column2: [ + { + xtype: 'textfield', + fieldLabel: gettext('Send email to'), + name: 'mailto', + }, + { + xtype: 'pveEmailNotificationSelector', + fieldLabel: gettext('Email'), + name: 'mailnotification', + cbind: { + value: (get) => get('isCreate') ? 'always' : '', + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'pveCompressionSelector', + reference: 'compressionSelector', + fieldLabel: gettext('Compression'), + name: 'compress', + cbind: { + deleteEmpty: '{!isCreate}', + }, + value: 'zstd', + }, + { + xtype: 'pveBackupModeSelector', + fieldLabel: gettext('Mode'), + value: 'snapshot', + name: 'mode', + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enable'), + name: 'enabled', + uncheckedValue: 0, + defaultValue: 1, + checked: true, + }, + ], + columnB: [ + { + xtype: 'proxmoxtextfield', + name: 'comment', + fieldLabel: gettext('Job Comment'), + cbind: { + deleteEmpty: '{!isCreate}', + }, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Description of the job'), + }, + }, + { + xtype: 'vmselector', + reference: 'vmgrid', + height: 300, + name: 'vmid', + disabled: true, + allowBlank: false, + columnSelection: ['vmid', 'node', 'status', 'name', 'type'], + bind: { + disabled: '{disableVMSelection}', + }, + }, + ], + advancedColumn1: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Repeat missed'), + name: 'repeat-missed', + uncheckedValue: 0, + defaultValue: 0, + cbind: { + deleteDefaultValue: '{!isCreate}', + }, + }, + ], + onGetValues: function(values) { + return this.up('window').getController().onGetValues(values); + }, + }, + ], + }, + { + xtype: 'pveBackupJobPrunePanel', + title: gettext('Retention'), + cbind: { + isCreate: '{isCreate}', + }, + keepAllDefaultForCreate: false, + showPBSHint: false, + fallbackHintHtml: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'), + }, + { + xtype: 'inputpanel', + title: gettext('Note Template'), + region: 'center', + layout: { + type: 'vbox', + align: 'stretch', + }, + onGetValues: function(values) { + if (values['notes-template']) { + values['notes-template'] = + PVE.Utils.escapeNotesTemplate(values['notes-template']); + } + return values; + }, + items: [ + { + xtype: 'textarea', + name: 'notes-template', + fieldLabel: gettext('Backup Notes'), + height: 100, + maxLength: 512, + cbind: { + deleteEmpty: '{!isCreate}', + value: (get) => get('isCreate') ? '{{guestname}}' : undefined, + }, + }, + { + xtype: 'box', + style: { + margin: '8px 0px', + 'line-height': '1.5em', + }, + html: gettext('The notes are added to each backup created by this job.') + + '
' + + Ext.String.format( + gettext('Possible template variables are: {0}'), + PVE.Utils.notesTemplateVars.map(v => `{{${v}}}`).join(', '), + ), + }, + ], + }, + ], + }, + ], +}); + +Ext.define('PVE.dc.BackupView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveDcBackupView'], + + onlineHelp: 'chapter_vzdump', + + allText: '-- ' + gettext('All') + ' --', + + initComponent: function() { + let me = this; + + let store = new Ext.data.Store({ + model: 'pve-cluster-backup', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/backup", + }, + }); + + let not_backed_store = new Ext.data.Store({ + sorters: 'vmid', + proxy: { + type: 'proxmox', + url: 'api2/json/cluster/backup-info/not-backed-up', + }, + }); + + let noBackupJobInfoButton; + let reload = function() { + store.load(); + not_backed_store.load({ + callback: records => noBackupJobInfoButton.setVisible(records.length > 0), + }); + }; + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + let win = Ext.create('PVE.dc.BackupEdit', { + jobid: rec.data.id, + }); + win.on('destroy', reload); + win.show(); + }; + + let run_detail = function() { + let record = sm.getSelection()[0]; + if (!record) { + return; + } + Ext.create('Ext.window.Window', { + modal: true, + width: 800, + height: Ext.getBody().getViewSize().height > 1000 ? 800 : 600, // factor out as common infra? + resizable: true, + layout: 'fit', + title: gettext('Backup Details'), + items: [ + { + xtype: 'panel', + region: 'center', + layout: { + type: 'vbox', + align: 'stretch', + }, + items: [ + { + xtype: 'pveBackupInfo', + flex: 0, + layout: 'fit', + record: record.data, + }, + { + xtype: 'pveBackupDiskTree', + title: gettext('Included disks'), + flex: 1, + jobid: record.data.id, + }, + ], + }, + ], + }).show(); + }; + + let run_backup_now = function(job) { + job = Ext.clone(job); + + let jobNode = job.node; + // Remove properties related to scheduling + delete job.enabled; + delete job.starttime; + delete job.dow; + delete job.id; + delete job.schedule; + delete job.type; + delete job.node; + delete job.comment; + delete job['next-run']; + delete job['repeat-missed']; + job.all = job.all === true ? 1 : 0; + + ['performance', 'prune-backups'].forEach(key => { + if (job[key]) { + job[key] = PVE.Parser.printPropertyString(job[key]); + } + }); + + let allNodes = PVE.data.ResourceStore.getNodes(); + let nodes = allNodes.filter(node => node.status === 'online').map(node => node.node); + let errors = []; + + if (jobNode !== undefined) { + if (!nodes.includes(jobNode)) { + Ext.Msg.alert('Error', "Node '"+ jobNode +"' from backup job isn't online!"); + return; + } + nodes = [jobNode]; + } else { + let unkownNodes = allNodes.filter(node => node.status !== 'online'); + if (unkownNodes.length > 0) {errors.push(unkownNodes.map(node => node.node + ": " + gettext("Node is offline")));} + } + let jobTotalCount = nodes.length, jobsStarted = 0; + + Ext.Msg.show({ + title: gettext('Please wait...'), + closable: false, + progress: true, + progressText: '0/' + jobTotalCount, + }); + + let postRequest = function() { + jobsStarted++; + Ext.Msg.updateProgress(jobsStarted / jobTotalCount, jobsStarted + '/' + jobTotalCount); + + if (jobsStarted === jobTotalCount) { + Ext.Msg.hide(); + if (errors.length > 0) { + Ext.Msg.alert('Error', 'Some errors have been encountered:
' + errors.join('
')); + } + } + }; + + nodes.forEach(node => Proxmox.Utils.API2Request({ + url: '/nodes/' + node + '/vzdump', + method: 'POST', + params: job, + failure: function(response, opts) { + errors.push(node + ': ' + response.htmlStatus); + postRequest(); + }, + success: postRequest, + })); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + var run_btn = new Proxmox.button.Button({ + text: gettext('Run now'), + disabled: true, + selModel: sm, + handler: function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + Ext.Msg.show({ + title: gettext('Confirm'), + icon: Ext.Msg.QUESTION, + msg: gettext('Start the selected backup job now?'), + buttons: Ext.Msg.YESNO, + callback: function(btn) { + if (btn !== 'yes') { + return; + } + run_backup_now(rec.data); + }, + }); + }, + }); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/backup', + callback: function() { + reload(); + }, + }); + + var detail_btn = new Proxmox.button.Button({ + text: gettext('Job Detail'), + disabled: true, + tooltip: gettext('Show job details and which guests and volumes are affected by the backup job'), + selModel: sm, + handler: run_detail, + }); + + noBackupJobInfoButton = new Proxmox.button.Button({ + text: `${gettext('Show')}: ${gettext('Guests Without Backup Job')}`, + tooltip: gettext('Some guests are not covered by any backup job.'), + iconCls: 'fa fa-fw fa-exclamation-circle', + hidden: true, + handler: () => { + Ext.create('Ext.window.Window', { + autoShow: true, + modal: true, + width: 600, + height: 500, + resizable: true, + layout: 'fit', + title: gettext('Guests Without Backup Job'), + items: [ + { + xtype: 'panel', + region: 'center', + layout: { + type: 'vbox', + align: 'stretch', + }, + items: [ + { + xtype: 'pveBackedGuests', + flex: 1, + layout: 'fit', + store: not_backed_store, + }, + ], + }, + ], + }); + }, + }); + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + stateful: true, + stateId: 'grid-dc-backup', + viewConfig: { + trackOver: false, + }, + dockedItems: [{ + xtype: 'toolbar', + overflowHandler: 'scroller', + dock: 'top', + items: [ + { + text: gettext('Add'), + handler: function() { + var win = Ext.create('PVE.dc.BackupEdit', {}); + win.on('destroy', reload); + win.show(); + }, + }, + '-', + remove_btn, + edit_btn, + detail_btn, + '-', + run_btn, + '->', + noBackupJobInfoButton, + '-', + { + xtype: 'proxmoxButton', + selModel: null, + text: gettext('Schedule Simulator'), + handler: () => { + let record = sm.getSelection()[0]; + let schedule; + if (record) { + schedule = record.data.schedule; + } + Ext.create('PVE.window.ScheduleSimulator', { + autoShow: true, + schedule, + }); + }, + }, + ], + }], + columns: [ + { + header: gettext('Enabled'), + width: 80, + dataIndex: 'enabled', + align: 'center', + // TODO: switch to Proxmox.Utils.renderEnabledIcon once available + renderer: enabled => ``, + sortable: true, + }, + { + header: gettext('ID'), + dataIndex: 'id', + hidden: true, + }, + { + header: gettext('Node'), + width: 100, + sortable: true, + dataIndex: 'node', + renderer: function(value) { + if (value) { + return value; + } + return me.allText; + }, + }, + { + header: gettext('Schedule'), + width: 150, + dataIndex: 'schedule', + }, + { + text: gettext('Next Run'), + dataIndex: 'next-run', + width: 150, + renderer: PVE.Utils.render_next_event, + }, + { + header: gettext('Storage'), + width: 100, + sortable: true, + dataIndex: 'storage', + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.htmlEncode, + sorter: (a, b) => (a.data.comment || '').localeCompare(b.data.comment || ''), + flex: 1, + }, + { + header: gettext('Retention'), + dataIndex: 'prune-backups', + renderer: v => v ? PVE.Parser.printPropertyString(v) : gettext('Fallback from storage config'), + flex: 2, + }, + { + header: gettext('Selection'), + flex: 4, + sortable: false, + dataIndex: 'vmid', + renderer: PVE.Utils.render_backup_selection, + }, + ], + listeners: { + activate: reload, + itemdblclick: run_editor, + }, + }); + + me.callParent(); + }, +}, function() { + Ext.define('pve-cluster-backup', { + extend: 'Ext.data.Model', + fields: [ + 'id', + 'compress', + 'dow', + 'exclude', + 'mailto', + 'mode', + 'node', + 'pool', + 'prune-backups', + 'starttime', + 'storage', + 'vmid', + { name: 'enabled', type: 'boolean' }, + { name: 'all', type: 'boolean' }, + ], + }); +}); +Ext.define('pve-cluster-nodes', { + extend: 'Ext.data.Model', + fields: [ + 'node', { type: 'integer', name: 'nodeid' }, 'ring0_addr', 'ring1_addr', + { type: 'integer', name: 'quorum_votes' }, + ], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/config/nodes", + }, + idProperty: 'nodeid', +}); + +Ext.define('pve-cluster-info', { + extend: 'Ext.data.Model', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/config/join", + }, +}); + +Ext.define('PVE.ClusterAdministration', { + extend: 'Ext.panel.Panel', + xtype: 'pveClusterAdministration', + + title: gettext('Cluster Administration'), + onlineHelp: 'chapter_pvecm', + + border: false, + defaults: { border: false }, + + viewModel: { + parent: null, + data: { + totem: {}, + nodelist: [], + preferred_node: { + name: '', + fp: '', + addr: '', + }, + isInCluster: false, + nodecount: 0, + }, + }, + + items: [ + { + xtype: 'panel', + title: gettext('Cluster Information'), + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.store = Ext.create('Proxmox.data.UpdateStore', { + autoStart: true, + interval: 15 * 1000, + storeid: 'pve-cluster-info', + model: 'pve-cluster-info', + }); + view.store.on('load', this.onLoad, this); + view.on('destroy', view.store.stopUpdate); + }, + + onLoad: function(store, records, success, operation) { + let vm = this.getViewModel(); + + let data = records?.[0]?.data; + if (!success || !data || !data.nodelist?.length) { + let error = operation.getError(); + if (error) { + let msg = Proxmox.Utils.getResponseErrorMessage(error); + if (error.status !== 424 && !msg.match(/node is not in a cluster/i)) { + // an actual error, not just the "not in a cluster one", so show it! + Proxmox.Utils.setErrorMask(this.getView(), msg); + } + } + vm.set('totem', {}); + vm.set('isInCluster', false); + vm.set('nodelist', []); + vm.set('preferred_node', { + name: '', + addr: '', + fp: '', + }); + return; + } + vm.set('totem', data.totem); + vm.set('isInCluster', !!data.totem.cluster_name); + vm.set('nodelist', data.nodelist); + + let nodeinfo = data.nodelist.find(el => el.name === data.preferred_node); + + let links = {}; + let ring_addr = []; + PVE.Utils.forEachCorosyncLink(nodeinfo, (num, link) => { + links[num] = link; + ring_addr.push(link); + }); + + vm.set('preferred_node', { + name: data.preferred_node, + addr: nodeinfo.pve_addr, + peerLinks: links, + ring_addr: ring_addr, + fp: nodeinfo.pve_fp, + }); + }, + + onCreate: function() { + let view = this.getView(); + view.store.stopUpdate(); + Ext.create('PVE.ClusterCreateWindow', { + autoShow: true, + listeners: { + destroy: function() { + view.store.startUpdate(); + }, + }, + }); + }, + + onClusterInfo: function() { + let vm = this.getViewModel(); + Ext.create('PVE.ClusterInfoWindow', { + autoShow: true, + joinInfo: { + ipAddress: vm.get('preferred_node.addr'), + fingerprint: vm.get('preferred_node.fp'), + peerLinks: vm.get('preferred_node.peerLinks'), + ring_addr: vm.get('preferred_node.ring_addr'), + totem: vm.get('totem'), + }, + }); + }, + + onJoin: function() { + let view = this.getView(); + view.store.stopUpdate(); + Ext.create('PVE.ClusterJoinNodeWindow', { + autoShow: true, + listeners: { + destroy: function() { + view.store.startUpdate(); + }, + }, + }); + }, + }, + tbar: [ + { + text: gettext('Create Cluster'), + reference: 'createButton', + handler: 'onCreate', + bind: { + disabled: '{isInCluster}', + }, + }, + { + text: gettext('Join Information'), + reference: 'addButton', + handler: 'onClusterInfo', + bind: { + disabled: '{!isInCluster}', + }, + }, + { + text: gettext('Join Cluster'), + reference: 'joinButton', + handler: 'onJoin', + bind: { + disabled: '{isInCluster}', + }, + }, + ], + layout: 'hbox', + bodyPadding: 5, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Cluster Name'), + bind: { + value: '{totem.cluster_name}', + hidden: '{!isInCluster}', + }, + flex: 1, + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Config Version'), + bind: { + value: '{totem.config_version}', + hidden: '{!isInCluster}', + }, + flex: 1, + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Number of Nodes'), + labelWidth: 120, + bind: { + value: '{nodecount}', + hidden: '{!isInCluster}', + }, + flex: 1, + }, + { + xtype: 'displayfield', + value: gettext('Standalone node - no cluster defined'), + bind: { + hidden: '{isInCluster}', + }, + flex: 1, + }, + ], + }, + { + xtype: 'grid', + title: gettext('Cluster Nodes'), + autoScroll: true, + enableColumnHide: false, + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.rstore = Ext.create('Proxmox.data.UpdateStore', { + autoLoad: true, + xtype: 'update', + interval: 5 * 1000, + autoStart: true, + storeid: 'pve-cluster-nodes', + model: 'pve-cluster-nodes', + }); + view.setStore(Ext.create('Proxmox.data.DiffStore', { + rstore: view.rstore, + sorters: { + property: 'nodeid', + direction: 'ASC', + }, + })); + Proxmox.Utils.monStoreErrors(view, view.rstore); + view.rstore.on('load', this.onLoad, this); + view.on('destroy', view.rstore.stopUpdate); + }, + + onLoad: function(store, records, success) { + let view = this.getView(); + let vm = this.getViewModel(); + + if (!success || !records || !records.length) { + vm.set('nodecount', 0); + return; + } + vm.set('nodecount', records.length); + + // show/hide columns according to used links + let linkIndex = view.columns.length; + Ext.each(view.columns, (col, i) => { + if (col.linkNumber !== undefined) { + col.setHidden(true); + // save offset at which link columns start, so we can address them directly below + if (i < linkIndex) { + linkIndex = i; + } + } + }); + + PVE.Utils.forEachCorosyncLink(records[0].data, + (linknum, val) => { + if (linknum > 7) { + return; + } + view.columns[linkIndex + linknum].setHidden(false); + }, + ); + }, + }, + columns: { + items: [ + { + header: gettext('Nodename'), + hidden: false, + dataIndex: 'name', + }, + { + header: gettext('ID'), + minWidth: 100, + width: 100, + flex: 0, + hidden: false, + dataIndex: 'nodeid', + }, + { + header: gettext('Votes'), + minWidth: 100, + width: 100, + flex: 0, + hidden: false, + dataIndex: 'quorum_votes', + }, + { + header: Ext.String.format(gettext('Link {0}'), 0), + dataIndex: 'ring0_addr', + linkNumber: 0, + }, + { + header: Ext.String.format(gettext('Link {0}'), 1), + dataIndex: 'ring1_addr', + linkNumber: 1, + }, + { + header: Ext.String.format(gettext('Link {0}'), 2), + dataIndex: 'ring2_addr', + linkNumber: 2, + }, + { + header: Ext.String.format(gettext('Link {0}'), 3), + dataIndex: 'ring3_addr', + linkNumber: 3, + }, + { + header: Ext.String.format(gettext('Link {0}'), 4), + dataIndex: 'ring4_addr', + linkNumber: 4, + }, + { + header: Ext.String.format(gettext('Link {0}'), 5), + dataIndex: 'ring5_addr', + linkNumber: 5, + }, + { + header: Ext.String.format(gettext('Link {0}'), 6), + dataIndex: 'ring6_addr', + linkNumber: 6, + }, + { + header: Ext.String.format(gettext('Link {0}'), 7), + dataIndex: 'ring7_addr', + linkNumber: 7, + }, + ], + defaults: { + flex: 1, + hidden: true, + minWidth: 150, + }, + }, + }, + ], +}); +Ext.define('PVE.ClusterCreateWindow', { + extend: 'Proxmox.window.Edit', + xtype: 'pveClusterCreateWindow', + + title: gettext('Create Cluster'), + width: 600, + + method: 'POST', + url: '/cluster/config', + + isCreate: true, + subject: gettext('Cluster'), + showTaskViewer: true, + + onlineHelp: 'pvecm_create_cluster', + + items: { + xtype: 'inputpanel', + items: [{ + xtype: 'textfield', + fieldLabel: gettext('Cluster Name'), + allowBlank: false, + maxLength: 15, + name: 'clustername', + }, + { + xtype: 'fieldcontainer', + fieldLabel: gettext("Cluster Network"), + items: [ + { + xtype: 'pveCorosyncLinkEditor', + infoText: gettext("Multiple links are used as failover, lower numbers have higher priority."), + name: 'links', + }, + ], + }], + }, +}); + +Ext.define('PVE.ClusterInfoWindow', { + extend: 'Ext.window.Window', + xtype: 'pveClusterInfoWindow', + mixins: ['Proxmox.Mixin.CBind'], + + width: 800, + modal: true, + resizable: false, + title: gettext('Cluster Join Information'), + + joinInfo: { + ipAddress: undefined, + fingerprint: undefined, + totem: {}, + }, + + items: [ + { + xtype: 'component', + border: false, + padding: '10 10 10 10', + html: gettext("Copy the Join Information here and use it on the node you want to add."), + }, + { + xtype: 'container', + layout: 'form', + border: false, + padding: '0 10 10 10', + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('IP Address'), + cbind: { + value: '{joinInfo.ipAddress}', + }, + editable: false, + }, + { + xtype: 'textfield', + fieldLabel: gettext('Fingerprint'), + cbind: { + value: '{joinInfo.fingerprint}', + }, + editable: false, + }, + { + xtype: 'textarea', + inputId: 'pveSerializedClusterInfo', + fieldLabel: gettext('Join Information'), + grow: true, + cbind: { + joinInfo: '{joinInfo}', + }, + editable: false, + listeners: { + afterrender: function(field) { + if (!field.joinInfo) { + return; + } + var jsons = Ext.JSON.encode(field.joinInfo); + var base64s = Ext.util.Base64.encode(jsons); + field.setValue(base64s); + }, + }, + }, + ], + }, + ], + dockedItems: [{ + dock: 'bottom', + xtype: 'toolbar', + items: [{ + xtype: 'button', + handler: function(b) { + var el = document.getElementById('pveSerializedClusterInfo'); + el.select(); + document.execCommand("copy"); + }, + text: gettext('Copy Information'), + }], + }], +}); + +Ext.define('PVE.ClusterJoinNodeWindow', { + extend: 'Proxmox.window.Edit', + xtype: 'pveClusterJoinNodeWindow', + + title: gettext('Cluster Join'), + width: 800, + + method: 'POST', + url: '/cluster/config/join', + + defaultFocus: 'textarea[name=serializedinfo]', + isCreate: true, + bind: { + submitText: '{submittxt}', + }, + showTaskViewer: true, + + onlineHelp: 'pvecm_join_node_to_cluster', + + viewModel: { + parent: null, + data: { + info: { + fp: '', + ip: '', + clusterName: '', + }, + hasAssistedInfo: false, + }, + formulas: { + submittxt: function(get) { + let cn = get('info.clusterName'); + if (cn) { + return Ext.String.format(gettext('Join {0}'), `'${cn}'`); + } + return gettext('Join'); + }, + showClusterFields: (get) => { + let manualMode = !get('assistedEntry.checked'); + return get('hasAssistedInfo') || manualMode; + }, + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + '#': { + close: function() { + delete PVE.Utils.silenceAuthFailures; + }, + }, + 'proxmoxcheckbox[name=assistedEntry]': { + change: 'onInputTypeChange', + }, + 'textarea[name=serializedinfo]': { + change: 'recomputeSerializedInfo', + enable: 'resetField', + }, + 'textfield': { + disable: 'resetField', + }, + }, + resetField: function(field) { + field.reset(); + }, + onInputTypeChange: function(field, assistedInput) { + let linkEditor = this.lookup('linkEditor'); + + // this also clears all links + linkEditor.setAllowNumberEdit(!assistedInput); + + if (!assistedInput) { + linkEditor.setInfoText(); + linkEditor.setDefaultLinks(); + } + }, + recomputeSerializedInfo: function(field, value) { + let vm = this.getViewModel(); + + let assistedEntryBox = this.lookup('assistedEntry'); + + if (!assistedEntryBox.getValue()) { + // not in assisted entry mode, nothing to do + vm.set('hasAssistedInfo', false); + return; + } + + let linkEditor = this.lookup('linkEditor'); + + let jsons = Ext.util.Base64.decode(value); + let joinInfo = Ext.JSON.decode(jsons, true); + + let info = { + fp: '', + ip: '', + clusterName: '', + }; + + if (!(joinInfo && joinInfo.totem)) { + field.valid = false; + linkEditor.setLinks([]); + linkEditor.setInfoText(); + vm.set('hasAssistedInfo', false); + } else { + let interfaces = joinInfo.totem.interface; + let links = Object.values(interfaces).map(iface => { + let linkNumber = iface.linknumber; + let peerLink; + if (joinInfo.peerLinks) { + peerLink = joinInfo.peerLinks[linkNumber]; + } + return { + number: linkNumber, + value: '', + text: peerLink ? Ext.String.format(gettext("peer's link address: {0}"), peerLink) : '', + allowBlank: false, + }; + }); + + linkEditor.setInfoText(); + if (links.length === 1 && joinInfo.ring_addr !== undefined && + joinInfo.ring_addr[0] === joinInfo.ipAddress + ) { + links[0].allowBlank = true; + links[0].emptyText = gettext("IP resolved by node's hostname"); + } + + linkEditor.setLinks(links); + + info = { + ip: joinInfo.ipAddress, + fp: joinInfo.fingerprint, + clusterName: joinInfo.totem.cluster_name, + }; + field.valid = true; + vm.set('hasAssistedInfo', true); + } + vm.set('info', info); + }, + }, + + submit: function() { + // joining may produce temporarily auth failures, ignore as long the task runs + PVE.Utils.silenceAuthFailures = true; + this.callParent(); + }, + + taskDone: function(success) { + delete PVE.Utils.silenceAuthFailures; + if (success) { + // reload always (if user wasn't faster), but wait a bit for pveproxy + Ext.defer(function() { + window.location.reload(true); + }, 5000); + let txt = gettext('Cluster join task finished, node certificate may have changed, reload GUI!'); + // ensure user cannot do harm + Ext.getBody().mask(txt, ['pve-static-mask']); + // TaskView may hide above mask, so tell him directly + Ext.Msg.show({ + title: gettext('Join Task Finished'), + icon: Ext.Msg.INFO, + msg: txt, + }); + } + }, + + items: [{ + xtype: 'proxmoxcheckbox', + reference: 'assistedEntry', + name: 'assistedEntry', + itemId: 'assistedEntry', + submitValue: false, + value: true, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Select if join information should be extracted from pasted cluster information, deselect for manual entering'), + }, + boxLabel: gettext('Assisted join: Paste encoded cluster join information and enter password.'), + }, + { + xtype: 'textarea', + name: 'serializedinfo', + submitValue: false, + allowBlank: false, + fieldLabel: gettext('Information'), + emptyText: gettext('Paste encoded Cluster Information here'), + validator: function(val) { + return val === '' || this.valid || gettext('Does not seem like a valid encoded Cluster Information!'); + }, + bind: { + disabled: '{!assistedEntry.checked}', + hidden: '{!assistedEntry.checked}', + }, + value: '', + }, + { + xtype: 'panel', + width: 776, + layout: { + type: 'hbox', + align: 'center', + }, + bind: { + hidden: '{!showClusterFields}', + }, + items: [ + { + xtype: 'textfield', + flex: 1, + margin: '0 5px 0 0', + fieldLabel: gettext('Peer Address'), + allowBlank: false, + bind: { + value: '{info.ip}', + readOnly: '{assistedEntry.checked}', + }, + name: 'hostname', + }, + { + xtype: 'textfield', + flex: 1, + margin: '0 0 10px 5px', + inputType: 'password', + emptyText: gettext("Peer's root password"), + fieldLabel: gettext('Password'), + allowBlank: false, + name: 'password', + }, + ], + }, + { + xtype: 'textfield', + fieldLabel: gettext('Fingerprint'), + allowBlank: false, + bind: { + value: '{info.fp}', + readOnly: '{assistedEntry.checked}', + hidden: '{!showClusterFields}', + }, + name: 'fingerprint', + }, + { + xtype: 'fieldcontainer', + fieldLabel: gettext("Cluster Network"), + bind: { + hidden: '{!showClusterFields}', + }, + items: [ + { + xtype: 'pveCorosyncLinkEditor', + itemId: 'linkEditor', + reference: 'linkEditor', + allowNumberEdit: false, + }, + ], + }], +}); +/* + * Datacenter config panel, located in the center of the ViewPort after the Datacenter view is selected + */ + +Ext.define('PVE.dc.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.dc.Config', + + onlineHelp: 'pve_admin_guide', + + initComponent: function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + me.items = []; + + Ext.apply(me, { + title: gettext("Datacenter"), + hstateid: 'dctab', + }); + + if (caps.dc['Sys.Audit']) { + me.items.push({ + title: gettext('Summary'), + xtype: 'pveDcSummary', + iconCls: 'fa fa-book', + itemId: 'summary', + }, + { + xtype: 'pmxNotesView', + title: gettext('Notes'), + iconCls: 'fa fa-sticky-note-o', + itemId: 'notes', + }, + { + title: gettext('Cluster'), + xtype: 'pveClusterAdministration', + iconCls: 'fa fa-server', + itemId: 'cluster', + }, + { + title: 'Ceph', + itemId: 'ceph', + iconCls: 'fa fa-ceph', + xtype: 'pveNodeCephStatus', + }, + { + xtype: 'pveDcOptionView', + title: gettext('Options'), + iconCls: 'fa fa-gear', + itemId: 'options', + }); + } + + if (caps.storage['Datastore.Allocate'] || caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveStorageView', + title: gettext('Storage'), + iconCls: 'fa fa-database', + itemId: 'storage', + }); + } + + + if (caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveDcBackupView', + iconCls: 'fa fa-floppy-o', + title: gettext('Backup'), + itemId: 'backup', + }, + { + xtype: 'pveReplicaView', + iconCls: 'fa fa-retweet', + title: gettext('Replication'), + itemId: 'replication', + }, + { + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + expandedOnInit: true, + }); + } + + me.items.push({ + xtype: 'pveUserView', + groups: ['permissions'], + iconCls: 'fa fa-user', + title: gettext('Users'), + itemId: 'users', + }); + + me.items.push({ + xtype: 'pveTokenView', + groups: ['permissions'], + iconCls: 'fa fa-user-o', + title: gettext('API Tokens'), + itemId: 'apitokens', + }); + + me.items.push({ + xtype: 'pmxTfaView', + title: gettext('Two Factor'), + groups: ['permissions'], + iconCls: 'fa fa-key', + itemId: 'tfa', + yubicoEnabled: true, + issuerName: `Proxmox VE - ${PVE.ClusterName || Proxmox.NodeName}`, + }); + + if (caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveGroupView', + title: gettext('Groups'), + iconCls: 'fa fa-users', + groups: ['permissions'], + itemId: 'groups', + }, + { + xtype: 'pvePoolView', + title: gettext('Pools'), + iconCls: 'fa fa-tags', + groups: ['permissions'], + itemId: 'pools', + }, + { + xtype: 'pveRoleView', + title: gettext('Roles'), + iconCls: 'fa fa-male', + groups: ['permissions'], + itemId: 'roles', + }, + { + xtype: 'pveAuthView', + title: gettext('Realms'), + groups: ['permissions'], + iconCls: 'fa fa-address-book-o', + itemId: 'domains', + }, + { + xtype: 'pveHAStatus', + title: 'HA', + iconCls: 'fa fa-heartbeat', + itemId: 'ha', + }, + { + title: gettext('Groups'), + groups: ['ha'], + xtype: 'pveHAGroupsView', + iconCls: 'fa fa-object-group', + itemId: 'ha-groups', + }, + { + title: gettext('Fencing'), + groups: ['ha'], + iconCls: 'fa fa-bolt', + xtype: 'pveFencingView', + itemId: 'ha-fencing', + }); + // always show on initial load, will be hiddea later if the SDN API calls don't exist, + // else it won't be shown at first if the user initially loads with DC selected + if (PVE.SDNInfo || PVE.SDNInfo === undefined) { + me.items.push({ + xtype: 'pveSDNStatus', + title: gettext('SDN'), + iconCls: 'fa fa-sdn', + hidden: true, + itemId: 'sdn', + expandedOnInit: true, + }, + { + xtype: 'pveSDNZoneView', + groups: ['sdn'], + title: gettext('Zones'), + hidden: true, + iconCls: 'fa fa-th', + itemId: 'sdnzone', + }, + { + xtype: 'pveSDNVnet', + groups: ['sdn'], + title: gettext('Vnets'), + hidden: true, + iconCls: 'fa fa-network-wired', + itemId: 'sdnvnet', + }, + { + xtype: 'pveSDNOptions', + groups: ['sdn'], + title: gettext('Options'), + hidden: true, + iconCls: 'fa fa-gear', + itemId: 'sdnoptions', + }); + } + + if (Proxmox.UserName === 'root@pam') { + me.items.push({ + xtype: 'pveACMEClusterView', + title: 'ACME', + iconCls: 'fa fa-certificate', + itemId: 'acme', + }); + } + + me.items.push({ + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + allow_iface: true, + base_url: '/cluster/firewall/rules', + list_refs_url: '/cluster/firewall/refs', + iconCls: 'fa fa-shield', + itemId: 'firewall', + }, + { + xtype: 'pveFirewallOptions', + title: gettext('Options'), + groups: ['firewall'], + iconCls: 'fa fa-gear', + base_url: '/cluster/firewall/options', + onlineHelp: 'pve_firewall_cluster_wide_setup', + fwtype: 'dc', + itemId: 'firewall-options', + }, + { + xtype: 'pveSecurityGroups', + title: gettext('Security Group'), + groups: ['firewall'], + iconCls: 'fa fa-group', + itemId: 'firewall-sg', + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: '/cluster/firewall/aliases', + itemId: 'firewall-aliases', + }, + { + xtype: 'pveIPSet', + title: 'IPSet', + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: '/cluster/firewall/ipset', + list_refs_url: '/cluster/firewall/refs', + itemId: 'firewall-ipset', + }, + { + xtype: 'pveMetricServerView', + title: gettext('Metric Server'), + iconCls: 'fa fa-bar-chart', + itemId: 'metricservers', + onlineHelp: 'external_metric_server', + }, + { + xtype: 'pveDcSupport', + title: gettext('Support'), + itemId: 'support', + iconCls: 'fa fa-comments-o', + }); + } + + me.callParent(); + }, +}); +Ext.define('PVE.form.CorosyncLinkEditorController', { + extend: 'Ext.app.ViewController', + alias: 'controller.pveCorosyncLinkEditorController', + + addLinkIfEmpty: function() { + let view = this.getView(); + if (view.items || view.items.length === 0) { + this.addLink(); + } + }, + + addEmptyLink: function() { + this.addLink(); // discard parameters to allow being called from 'handler' + }, + + addLink: function(link) { + let me = this; + let view = me.getView(); + let vm = view.getViewModel(); + + let linkCount = vm.get('linkCount'); + if (linkCount >= vm.get('maxLinkCount')) { + return; + } + + link = link || {}; + + if (link.number === undefined) { + link.number = me.getNextFreeNumber(); + } + if (link.value === undefined) { + link.value = me.getNextFreeNetwork(); + } + + let linkSelector = Ext.create('PVE.form.CorosyncLinkSelector', { + maxLinkNumber: vm.get('maxLinkCount') - 1, + allowNumberEdit: vm.get('allowNumberEdit'), + allowBlankNetwork: link.allowBlank, + initNumber: link.number, + initNetwork: link.value, + text: link.text, + emptyText: link.emptyText, + + // needs to be set here, because we need to update the viewmodel + removeBtnHandler: function() { + let curLinkCount = vm.get('linkCount'); + + if (curLinkCount <= 1) { + return; + } + + vm.set('linkCount', curLinkCount - 1); + + // 'this' is the linkSelector here + view.remove(this); + + me.updateDeleteButtonState(); + }, + }); + + view.add(linkSelector); + + linkCount++; + vm.set('linkCount', linkCount); + + me.updateDeleteButtonState(); + }, + + // ExtJS trips on binding this for some reason, so do it manually + updateDeleteButtonState: function() { + let view = this.getView(); + let vm = view.getViewModel(); + + let disabled = vm.get('linkCount') <= 1; + + let deleteButtons = view.query('button[cls=removeLinkBtn]'); + Ext.Array.each(deleteButtons, btn => { + btn.setDisabled(disabled); + }); + }, + + getNextFreeNetwork: function() { + let view = this.getView(); + let vm = view.getViewModel(); + + let networksInUse = view.query('proxmoxNetworkSelector').map(selector => selector.value); + + for (const network of vm.get('networks')) { + if (!networksInUse.includes(network)) { + return network; + } + } + return undefined; // default to empty field, user has to set up link manually + }, + + getNextFreeNumber: function() { + let view = this.getView(); + let vm = view.getViewModel(); + + let numbersInUse = view.query('numberfield').map(field => field.value); + + for (let i = 0; i < vm.get('maxLinkCount'); i++) { + if (!numbersInUse.includes(i)) { + return i; + } + } + // all numbers in use, this should never happen since add button is disabled automatically + return 0; + }, +}); + +Ext.define('PVE.form.CorosyncLinkSelector', { + extend: 'Ext.panel.Panel', + xtype: 'pveCorosyncLinkSelector', + + mixins: ['Proxmox.Mixin.CBind'], + cbindData: [], + + // config + maxLinkNumber: 7, + allowNumberEdit: true, + allowBlankNetwork: false, + removeBtnHandler: undefined, + emptyText: '', + + // values + initNumber: 0, + initNetwork: '', + text: '', + + layout: 'hbox', + bodyPadding: 5, + border: 0, + + items: [ + { + xtype: 'displayfield', + fieldLabel: 'Link', + cbind: { + hidden: '{allowNumberEdit}', + value: '{initNumber}', + }, + width: 45, + labelWidth: 30, + allowBlank: false, + }, + { + xtype: 'numberfield', + fieldLabel: 'Link', + cbind: { + maxValue: '{maxLinkNumber}', + hidden: '{!allowNumberEdit}', + value: '{initNumber}', + }, + width: 80, + labelWidth: 30, + minValue: 0, + submitValue: false, // see getSubmitValue of network selector + allowBlank: false, + }, + { + xtype: 'proxmoxNetworkSelector', + cbind: { + allowBlank: '{allowBlankNetwork}', + value: '{initNetwork}', + emptyText: '{emptyText}', + }, + autoSelect: false, + valueField: 'address', + displayField: 'address', + width: 220, + margin: '0 5px 0 5px', + getSubmitValue: function() { + let me = this; + // link number is encoded into key, so we need to set field name before value retrieval + let linkNumber = me.prev('numberfield').getValue(); // always the correct one + me.name = 'link' + linkNumber; + return me.getValue(); + }, + }, + { + xtype: 'button', + iconCls: 'fa fa-trash-o', + cls: 'removeLinkBtn', + cbind: { + hidden: '{!allowNumberEdit}', + }, + handler: function() { + let me = this; + let parent = me.up('pveCorosyncLinkSelector'); + if (parent.removeBtnHandler !== undefined) { + parent.removeBtnHandler(); + } + }, + }, + { + xtype: 'label', + margin: '-1px 0 0 5px', + + // for muted effect + cls: 'x-form-item-label-default', + + cbind: { + text: '{text}', + }, + }, + ], + + initComponent: function() { + let me = this; + + me.callParent(); + + let numSelect = me.down('numberfield'); + let netSelect = me.down('proxmoxNetworkSelector'); + + numSelect.validator = me.createNoDuplicatesValidator( + 'numberfield', + gettext("Duplicate link number not allowed."), + ); + + netSelect.validator = me.createNoDuplicatesValidator( + 'proxmoxNetworkSelector', + gettext("Duplicate link address not allowed."), + ); + }, + + createNoDuplicatesValidator: function(queryString, errorMsg) { // linkSelector generator + let view = this; // eslint-disable-line consistent-this + /** @this is the field itself, as the validator this is called from scopes it that way */ + return function(val) { + let me = this; + let form = view.up('form'); + let linkEditor = view.up('pveCorosyncLinkEditor'); + + if (!form.validating) { + // avoid recursion/double validation by setting temporary states + me.validating = true; + form.validating = true; + + // validate all other fields as well, to always mark both + // parties involved in a 'duplicate' error + form.isValid(); + + form.validating = false; + me.validating = false; + } else if (me.validating) { + // we'll be validated by the original call in the other if-branch, avoid double work + return true; + } + + if (val === undefined || (val instanceof String && val.length === 0)) { + return true; // let this be caught by allowBlank, if at all + } + + let allFields = linkEditor.query(queryString); + for (const field of allFields) { + if (field !== me && String(field.getValue()) === String(val)) { + return errorMsg; + } + } + return true; + }; + }, +}); + +Ext.define('PVE.form.CorosyncLinkEditor', { + extend: 'Ext.panel.Panel', + xtype: 'pveCorosyncLinkEditor', + + controller: 'pveCorosyncLinkEditorController', + + // only initial config, use setter otherwise + allowNumberEdit: true, + + viewModel: { + data: { + linkCount: 0, + maxLinkCount: 8, + networks: null, + allowNumberEdit: true, + infoText: '', + }, + formulas: { + addDisabled: function(get) { + return !get('allowNumberEdit') || + get('linkCount') >= get('maxLinkCount'); + }, + dockHidden: function(get) { + return !(get('allowNumberEdit') || get('infoText')); + }, + }, + }, + + dockedItems: [{ + xtype: 'toolbar', + dock: 'bottom', + defaultButtonUI: 'default', + border: false, + padding: '6 0 6 0', + bind: { + hidden: '{dockHidden}', + }, + items: [ + { + xtype: 'button', + text: gettext('Add'), + bind: { + disabled: '{addDisabled}', + hidden: '{!allowNumberEdit}', + }, + handler: 'addEmptyLink', + }, + { + xtype: 'label', + bind: { + text: '{infoText}', + }, + }, + ], + }], + + setInfoText: function(text) { + let me = this; + let vm = me.getViewModel(); + + vm.set('infoText', text || ''); + }, + + setLinks: function(links) { + let me = this; + let controller = me.getController(); + let vm = me.getViewModel(); + + me.removeAll(); + vm.set('linkCount', 0); + + Ext.Array.each(links, link => controller.addLink(link)); + }, + + setDefaultLinks: function() { + let me = this; + let controller = me.getController(); + let vm = me.getViewModel(); + + me.removeAll(); + vm.set('linkCount', 0); + controller.addLink(); + }, + + // clears all links + setAllowNumberEdit: function(allow) { + let me = this; + let vm = me.getViewModel(); + vm.set('allowNumberEdit', allow); + me.removeAll(); + vm.set('linkCount', 0); + }, + + items: [{ + // No links is never a valid scenario, but can occur during a slow load + xtype: 'hiddenfield', + submitValue: false, + isValid: function() { + let me = this; + let vm = me.up('pveCorosyncLinkEditor').getViewModel(); + return vm.get('linkCount') > 0; + }, + }], + + initComponent: function() { + let me = this; + let vm = me.getViewModel(); + let controller = me.getController(); + + vm.set('allowNumberEdit', me.allowNumberEdit); + vm.set('infoText', me.infoText || ''); + + me.callParent(); + + // Request local node networks to pre-populate first link. + Proxmox.Utils.API2Request({ + url: '/nodes/localhost/network', + method: 'GET', + waitMsgTarget: me, + success: response => { + let data = response.result.data; + if (data.length > 0) { + data.sort((a, b) => a.iface.localeCompare(b.iface)); + let addresses = []; + for (let net of data) { + if (net.address) { + addresses.push(net.address); + } + if (net.address6) { + addresses.push(net.address6); + } + } + + vm.set('networks', addresses); + } + + // Always have at least one link, but account for delay in API, + // someone might have called 'setLinks' in the meantime - + // except if 'allowNumberEdit' is false, in which case we're + // probably waiting for the user to input the join info + if (vm.get('allowNumberEdit')) { + controller.addLinkIfEmpty(); + } + }, + failure: () => { + if (vm.get('allowNumberEdit')) { + controller.addLinkIfEmpty(); + } + }, + }); + }, +}); + +Ext.define('PVE.dc.GroupEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcGroupEdit'], + + initComponent: function() { + var me = this; + + me.isCreate = !me.groupid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/access/groups'; + method = 'POST'; + } else { + url = '/api2/extjs/access/groups/' + me.groupid; + method = 'PUT'; + } + + Ext.applyIf(me, { + subject: gettext('Group'), + url: url, + method: method, + items: [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + fieldLabel: gettext('Name'), + name: 'groupid', + value: me.groupid, + allowBlank: false, + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment', + allowBlank: true, + }, + ], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load(); + } + }, +}); +Ext.define('PVE.dc.GroupView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveGroupView'], + + onlineHelp: 'pveum_groups', + + stateful: true, + stateId: 'grid-groups', + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-groups', + sorters: { + property: 'groupid', + direction: 'ASC', + }, + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + callback: function() { + reload(); + }, + baseurl: '/access/groups/', + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.GroupEdit', { + groupid: rec.data.groupid, + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + var tbar = [ + { + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.dc.GroupEdit', {}); + win.on('destroy', reload); + win.show(); + }, + }, + edit_btn, remove_btn, + ]; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false, + }, + columns: [ + { + header: gettext('Name'), + width: 200, + sortable: true, + dataIndex: 'groupid', + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1, + }, + { + header: gettext('Users'), + sortable: false, + dataIndex: 'users', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + listeners: { + activate: reload, + itemdblclick: run_editor, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.dc.Guests', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcGuests', + + + title: gettext('Guests'), + height: 250, + layout: { + type: 'table', + columns: 2, + tableAttrs: { + style: { + width: '100%', + }, + }, + }, + bodyPadding: '0 20 20 20', + + defaults: { + xtype: 'box', + padding: '0 50 0 50', + style: { + 'text-align': 'center', + 'line-height': '1.5em', + 'font-size': '14px', + }, + }, + items: [ + { + itemId: 'qemu', + data: { + running: 0, + paused: 0, + stopped: 0, + template: 0, + }, + cls: 'centered-flex-column', + tpl: [ + '

' + gettext("Virtual Machines") + '

', + '
', + '
', + ' ', + gettext('Running'), + '
', + '
{running}
', + '
', + '', + '
', + '
', + ' ', + gettext('Paused'), + '
', + '
{paused}
', + '
', + '
', + '
', + '
', + ' ', + gettext('Stopped'), + '
', + '
{stopped}
', + '
', + '', + '
', + '
', + ' ', + gettext('Templates'), + '
', + '
{template}
', + '
', + '
', + ], + }, + { + itemId: 'lxc', + data: { + running: 0, + paused: 0, + stopped: 0, + template: 0, + }, + cls: 'centered-flex-column', + tpl: [ + '

' + gettext("LXC Container") + '

', + '
', + '
', + ' ', + gettext('Running'), + '
', + '
{running}
', + '
', + '', + '
', + '
', + ' ', + gettext('Paused'), + '
', + '
{paused}
', + '
', + '
', + '
', + '
', + ' ', + gettext('Stopped'), + '
', + '
{stopped}
', + '
', + '', + '
', + '
', + ' ', + gettext('Templates'), + '
', + '
{template}
', + '
', + '
', + ], + }, + { + itemId: 'error', + colspan: 2, + data: { + num: 0, + }, + columnWidth: 1, + padding: '10 250 0 250', + tpl: [ + '', + '
', + ' ', + gettext('Error'), + '
', + '
{num}
', + '
', + ], + }, + ], + + updateValues: function(qemu, lxc, error) { + let me = this; + + let lazyUpdate = (query, newData) => { + let el = me.getComponent(query); + let currentData = el.data; + + let keys = Object.keys(newData); + if (keys.length === Object.keys(currentData).length) { + if (keys.every(k => newData[k] === currentData[k])) { + return; // all stayed the same here, return early to avoid bogus regeneration + } + } + el.update(newData); + }; + lazyUpdate('qemu', qemu); + lazyUpdate('lxc', lxc); + lazyUpdate('error', { num: error }); + }, +}); +Ext.define('PVE.dc.Health', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcHealth', + + title: gettext('Health'), + + bodyPadding: 10, + height: 250, + layout: { + type: 'hbox', + align: 'stretch', + }, + + defaults: { + flex: 1, + xtype: 'box', + style: { + 'text-align': 'center', + }, + }, + + nodeList: [], + nodeIndex: 0, + + updateStatus: function(store, records, success) { + let me = this; + if (!success) { + return; + } + + let cluster = { + iconCls: PVE.Utils.get_health_icon('good', true), + text: gettext("Standalone node - no cluster defined"), + }; + let nodes = { + online: 0, + offline: 0, + }; + let numNodes = 1; // by default we have one node + for (const { data } of records) { + if (data.type === 'node') { + nodes[data.online === 1 ? 'online':'offline']++; + } else if (data.type === 'cluster') { + cluster.text = `${gettext("Cluster")}: ${data.name}, ${gettext("Quorate")}: `; + cluster.text += Proxmox.Utils.format_boolean(data.quorate); + if (data.quorate !== 1) { + cluster.iconCls = PVE.Utils.get_health_icon('critical', true); + } + numNodes = data.nodes; + } + } + + if (numNodes !== nodes.online + nodes.offline) { + nodes.offline = numNodes - nodes.online; + } + + me.getComponent('clusterstatus').updateHealth(cluster); + me.getComponent('nodestatus').update(nodes); + }, + + updateCeph: function(store, records, success) { + let me = this; + let cephstatus = me.getComponent('ceph'); + if (!success || records.length < 1) { + if (cephstatus.isVisible()) { + return; // if ceph status is already visible don't stop to update + } + // try all nodes until we either get a successful api call, or we tried all nodes + if (++me.nodeIndex >= me.nodeList.length) { + me.cephstore.stopUpdate(); + } else { + store.getProxy().setUrl(`/api2/json/nodes/${me.nodeList[me.nodeIndex].node}/ceph/status`); + } + return; + } + + let state = PVE.Utils.render_ceph_health(records[0].data.health || {}); + cephstatus.updateHealth(state); + cephstatus.setVisible(true); + }, + + listeners: { + destroy: function() { + let me = this; + me.cephstore.stopUpdate(); + }, + }, + + items: [ + { + itemId: 'clusterstatus', + xtype: 'pveHealthWidget', + title: gettext('Status'), + }, + { + itemId: 'nodestatus', + data: { + online: 0, + offline: 0, + }, + tpl: [ + '

' + gettext('Nodes') + '


', + '
', + '
', + ' ', + gettext('Online'), + '
', + '
{online}
', + '

', + '
', + ' ', + gettext('Offline'), + '
', + '
{offline}
', + '
', + ], + }, + { + itemId: 'ceph', + width: 250, + columnWidth: undefined, + userCls: 'pointer', + title: 'Ceph', + xtype: 'pveHealthWidget', + hidden: true, + listeners: { + element: 'el', + click: function() { + Ext.state.Manager.getProvider().set('dctab', { value: 'ceph' }, true); + }, + }, + }, + ], + + initComponent: function() { + let me = this; + + me.nodeList = PVE.data.ResourceStore.getNodes(); + me.nodeIndex = 0; + me.cephstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 3000, + storeid: 'pve-cluster-ceph', + proxy: { + type: 'proxmox', + url: `/api2/json/nodes/${me.nodeList[me.nodeIndex].node}/ceph/status`, + }, + }); + me.callParent(); + me.mon(me.cephstore, 'load', me.updateCeph, me); + me.cephstore.startUpdate(); + }, +}); +/* This class defines the "Cluster log" tab of the bottom status panel + * A log entry is a timestamp associated with an action on a cluster + */ + +Ext.define('PVE.dc.Log', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveClusterLog'], + + initComponent: function() { + let me = this; + + let logstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'pve-cluster-log', + model: 'proxmox-cluster-log', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/log', + }, + }); + let store = Ext.create('Proxmox.data.DiffStore', { + rstore: logstore, + appendAtStart: true, + }); + + Ext.apply(me, { + store: store, + stateful: false, + + viewConfig: { + trackOver: false, + stripeRows: true, + getRowClass: function(record, index) { + let pri = record.get('pri'); + if (pri && pri <= 3) { + return "proxmox-invalid-row"; + } + return undefined; + }, + }, + sortableColumns: false, + columns: [ + { + header: gettext("Time"), + dataIndex: 'time', + width: 150, + renderer: function(value) { + return Ext.Date.format(value, "M d H:i:s"); + }, + }, + { + header: gettext("Node"), + dataIndex: 'node', + width: 150, + }, + { + header: gettext("Service"), + dataIndex: 'tag', + width: 100, + }, + { + header: "PID", + dataIndex: 'pid', + width: 100, + }, + { + header: gettext("User name"), + dataIndex: 'user', + renderer: Ext.String.htmlEncode, + width: 150, + }, + { + header: gettext("Severity"), + dataIndex: 'pri', + renderer: PVE.Utils.render_serverity, + width: 100, + }, + { + header: gettext("Message"), + dataIndex: 'msg', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + listeners: { + activate: () => logstore.startUpdate(), + deactivate: () => logstore.stopUpdate(), + destroy: () => logstore.stopUpdate(), + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.dc.NodeView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveDcNodeView', + + title: gettext('Nodes'), + disableSelection: true, + scrollable: true, + + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + dataIndex: 'name', + }, + { + header: 'ID', + width: 40, + sortable: true, + dataIndex: 'nodeid', + }, + { + header: gettext('Online'), + width: 60, + sortable: true, + dataIndex: 'online', + renderer: function(value) { + var cls = value?'good':'critical'; + return ''; + }, + }, + { + header: gettext('Support'), + width: 100, + sortable: true, + dataIndex: 'level', + renderer: PVE.Utils.render_support_level, + }, + { + header: gettext('Server Address'), + width: 115, + sortable: true, + dataIndex: 'ip', + }, + { + header: gettext('CPU usage'), + sortable: true, + width: 110, + dataIndex: 'cpuusage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar', + }, + }, + { + header: gettext('Memory usage'), + width: 110, + sortable: true, + tdCls: 'x-progressbar-default-cell', + dataIndex: 'memoryusage', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar', + }, + }, + { + header: gettext('Uptime'), + sortable: true, + dataIndex: 'uptime', + align: 'right', + renderer: Proxmox.Utils.render_uptime, + }, + ], + + stateful: true, + stateId: 'grid-cluster-nodes', + tools: [ + { + type: 'up', + handler: function() { + let view = this.up('grid'); + view.setHeight(Math.max(view.getHeight() - 50, 250)); + }, + }, + { + type: 'down', + handler: function() { + let view = this.up('grid'); + view.setHeight(view.getHeight() + 50); + }, + }, + ], +}, function() { + Ext.define('pve-dc-nodes', { + extend: 'Ext.data.Model', + fields: ['id', 'type', 'name', 'nodeid', 'ip', 'level', 'local', 'online'], + idProperty: 'id', + }); +}); + +Ext.define('PVE.widget.ProgressBar', { + extend: 'Ext.Progress', + alias: 'widget.pveProgressBar', + + animate: true, + textTpl: [ + '{percent}%', + ], + + setValue: function(value) { + let me = this; + + me.callParent([value]); + + me.removeCls(['warning', 'critical']); + + if (value > 0.89) { + me.addCls('critical'); + } else if (value > 0.75) { + me.addCls('warning'); + } + }, +}); +Ext.define('PVE.dc.OptionView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveDcOptionView'], + + onlineHelp: 'datacenter_configuration_file', + + monStoreErrors: true, + userCls: 'proxmox-tags-full', + + add_inputpanel_row: function(name, text, opts) { + var me = this; + + opts = opts || {}; + me.rows = me.rows || {}; + + let canEdit = !Object.prototype.hasOwnProperty.call(opts, 'caps') || opts.caps; + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue, + header: text, + renderer: opts.renderer, + editor: canEdit ? { + xtype: 'proxmoxWindowEdit', + width: opts.width || 350, + subject: text, + onlineHelp: opts.onlineHelp, + fieldDefaults: { + labelWidth: opts.labelWidth || 100, + }, + setValues: function(values) { + var edit_value = values[name]; + + if (opts.parseBeforeSet) { + edit_value = PVE.Parser.parsePropertyString(edit_value); + } + + Ext.Array.each(this.query('inputpanel'), function(panel) { + panel.setValues(edit_value); + }); + }, + url: opts.url, + items: [{ + xtype: 'inputpanel', + onGetValues: function(values) { + if (values === undefined || Object.keys(values).length === 0) { + return { 'delete': name }; + } + var ret_val = {}; + ret_val[name] = PVE.Parser.printPropertyString(values); + return ret_val; + }, + items: opts.items, + }], + } : undefined, + }; + }, + + render_bwlimits: function(value) { + if (!value) { + return gettext("None"); + } + + let parsed = PVE.Parser.parsePropertyString(value); + return Object.entries(parsed) + .map(([k, v]) => k + ": " + Proxmox.Utils.format_size(v * 1024) + "/s") + .join(','); + }, + + initComponent: function() { + var me = this; + + me.add_combobox_row('keyboard', gettext('Keyboard Layout'), { + renderer: PVE.Utils.render_kvm_language, + comboItems: Object.entries(PVE.Utils.kvm_keymaps), + defaultValue: '__default__', + deleteEmpty: true, + }); + me.add_text_row('http_proxy', gettext('HTTP proxy'), { + defaultValue: Proxmox.Utils.noneText, + vtype: 'HttpProxy', + deleteEmpty: true, + }); + me.add_combobox_row('console', gettext('Console Viewer'), { + renderer: PVE.Utils.render_console_viewer, + comboItems: Object.entries(PVE.Utils.console_map), + defaultValue: '__default__', + deleteEmpty: true, + }); + me.add_text_row('email_from', gettext('Email from address'), { + deleteEmpty: true, + vtype: 'proxmoxMail', + defaultValue: 'root@$hostname', + }); + me.add_inputpanel_row('notify', gettext('Notify'), { + renderer: v => !v ? 'package-updates=auto' : PVE.Parser.printPropertyString(v), + labelWidth: 120, + url: "/api2/extjs/cluster/options", + //onlineHelp: 'ha_manager_shutdown_policy', + items: [{ + xtype: 'proxmoxKVComboBox', + name: 'package-updates', + fieldLabel: gettext('Package Updates'), + deleteEmpty: false, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (auto)'], + ['auto', gettext('Automatically')], + ['always', gettext('Always')], + ['never', gettext('Never')], + ], + defaultValue: '__default__', + }], + }); + me.add_text_row('mac_prefix', gettext('MAC address prefix'), { + deleteEmpty: true, + vtype: 'MacPrefix', + defaultValue: Proxmox.Utils.noneText, + }); + me.add_inputpanel_row('migration', gettext('Migration Settings'), { + renderer: PVE.Utils.render_as_property_string, + labelWidth: 120, + url: "/api2/extjs/cluster/options", + defaultKey: 'type', + items: [{ + xtype: 'displayfield', + name: 'type', + fieldLabel: gettext('Type'), + value: 'secure', + submitValue: true, + }, { + xtype: 'proxmoxNetworkSelector', + name: 'network', + fieldLabel: gettext('Network'), + value: null, + emptyText: Proxmox.Utils.defaultText, + autoSelect: false, + skipEmptyText: true, + }], + }); + me.add_inputpanel_row('ha', gettext('HA Settings'), { + renderer: PVE.Utils.render_dc_ha_opts, + labelWidth: 120, + url: "/api2/extjs/cluster/options", + onlineHelp: 'ha_manager_shutdown_policy', + items: [{ + xtype: 'proxmoxKVComboBox', + name: 'shutdown_policy', + fieldLabel: gettext('Shutdown Policy'), + deleteEmpty: false, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (conditional)'], + ['freeze', 'freeze'], + ['failover', 'failover'], + ['migrate', 'migrate'], + ['conditional', 'conditional'], + ], + defaultValue: '__default__', + }], + }); + me.add_inputpanel_row('crs', gettext('Cluster Resource Scheduling'), { + renderer: PVE.Utils.render_as_property_string, + width: 450, + labelWidth: 120, + url: "/api2/extjs/cluster/options", + onlineHelp: 'ha_manager_crs', + items: [{ + xtype: 'proxmoxKVComboBox', + name: 'ha', + fieldLabel: gettext('HA Scheduling'), + deleteEmpty: false, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (basic)'], + ['basic', 'Basic (Resource Count)'], + ['static', 'Static Load'], + ], + defaultValue: '__default__', + }, { + xtype: 'proxmoxcheckbox', + name: 'ha-rebalance-on-start', + fieldLabel: gettext('Rebalance on Start'), + boxLabel: gettext('Use CRS to select the least loaded node when starting an HA service'), + value: 0, + }], + }); + me.add_inputpanel_row('u2f', gettext('U2F Settings'), { + renderer: v => !v ? Proxmox.Utils.NoneText : PVE.Parser.printPropertyString(v), + width: 450, + url: "/api2/extjs/cluster/options", + onlineHelp: 'pveum_configure_u2f', + items: [{ + xtype: 'textfield', + name: 'appid', + fieldLabel: gettext('U2F AppID URL'), + emptyText: gettext('Defaults to origin'), + value: '', + deleteEmpty: true, + skipEmptyText: true, + submitEmptyText: false, + }, { + xtype: 'textfield', + name: 'origin', + fieldLabel: gettext('U2F Origin'), + emptyText: gettext('Defaults to requesting host URI'), + value: '', + deleteEmpty: true, + skipEmptyText: true, + submitEmptyText: false, + }, + { + xtype: 'box', + height: 25, + html: `${gettext('Note:')} ` + + Ext.String.format(gettext('{0} is deprecated, use {1}'), 'U2F', 'WebAuthn'), + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext('NOTE: Changing an AppID breaks existing U2F registrations!'), + }], + }); + me.add_inputpanel_row('webauthn', gettext('WebAuthn Settings'), { + renderer: v => !v ? Proxmox.Utils.NoneText : PVE.Parser.printPropertyString(v), + width: 450, + url: "/api2/extjs/cluster/options", + onlineHelp: 'pveum_configure_webauthn', + items: [{ + xtype: 'textfield', + fieldLabel: gettext('Name'), + name: 'rp', // NOTE: relying party consists of name and id, this is the name + allowBlank: false, + }, + { + xtype: 'textfield', + fieldLabel: gettext('Origin'), + emptyText: Ext.String.format(gettext("Domain Lockdown (e.g., {0})"), document.location.origin), + name: 'origin', + allowBlank: true, + }, + { + xtype: 'textfield', + fieldLabel: 'ID', + name: 'id', + allowBlank: false, + listeners: { + dirtychange: (f, isDirty) => + f.up('panel').down('box[id=idChangeWarning]').setHidden(!f.originalValue || !isDirty), + }, + }, + { + xtype: 'container', + layout: 'hbox', + items: [ + { + xtype: 'box', + flex: 1, + }, + { + xtype: 'button', + text: gettext('Auto-fill'), + iconCls: 'fa fa-fw fa-pencil-square-o', + handler: function(button, ev) { + let panel = this.up('panel'); + let fqdn = document.location.hostname; + + panel.down('field[name=rp]').setValue(fqdn); + + let idField = panel.down('field[name=id]'); + let currentID = idField.getValue(); + if (!currentID || currentID.length === 0) { + idField.setValue(fqdn); + } + }, + }, + ], + }, + { + xtype: 'box', + height: 25, + html: `${gettext('Note:')} ` + + gettext('WebAuthn requires using a trusted certificate.'), + }, + { + xtype: 'box', + id: 'idChangeWarning', + hidden: true, + padding: '5 0 0 0', + html: ' ' + + gettext('Changing the ID breaks existing WebAuthn TFA entries.'), + }], + }); + me.add_inputpanel_row('bwlimit', gettext('Bandwidth Limits'), { + renderer: me.render_bwlimits, + width: 450, + url: "/api2/extjs/cluster/options", + parseBeforeSet: true, + labelWidth: 120, + items: [{ + xtype: 'pveBandwidthField', + name: 'default', + fieldLabel: gettext('Default'), + emptyText: gettext('none'), + backendUnit: "KiB", + }, + { + xtype: 'pveBandwidthField', + name: 'restore', + fieldLabel: gettext('Backup Restore'), + emptyText: gettext('default'), + backendUnit: "KiB", + }, + { + xtype: 'pveBandwidthField', + name: 'migration', + fieldLabel: gettext('Migration'), + emptyText: gettext('default'), + backendUnit: "KiB", + }, + { + xtype: 'pveBandwidthField', + name: 'clone', + fieldLabel: gettext('Clone'), + emptyText: gettext('default'), + backendUnit: "KiB", + }, + { + xtype: 'pveBandwidthField', + name: 'move', + fieldLabel: gettext('Disk Move'), + emptyText: gettext('default'), + backendUnit: "KiB", + }], + }); + me.add_integer_row('max_workers', gettext('Maximal Workers/bulk-action'), { + deleteEmpty: true, + defaultValue: 4, + minValue: 1, + maxValue: 64, // arbitrary but generous limit as limits are good + }); + me.add_inputpanel_row('next-id', gettext('Next Free VMID Range'), { + renderer: PVE.Utils.render_as_property_string, + url: "/api2/extjs/cluster/options", + items: [{ + xtype: 'proxmoxintegerfield', + name: 'lower', + fieldLabel: gettext('Lower'), + emptyText: '100', + minValue: 100, + maxValue: 1000 * 1000 * 1000 - 1, + submitValue: true, + }, { + xtype: 'proxmoxintegerfield', + name: 'upper', + fieldLabel: gettext('Upper'), + emptyText: '1.000.000', + minValue: 100, + maxValue: 1000 * 1000 * 1000 - 1, + submitValue: true, + }], + }); + me.rows['tag-style'] = { + required: true, + renderer: (value) => { + if (value === undefined) { + return gettext('No Overrides'); + } + let colors = PVE.UIOptions.parseTagOverrides(value?.['color-map']); + let shape = value.shape; + let shapeText = PVE.UIOptions.tagTreeStyles[shape ?? '__default__']; + let txt = Ext.String.format(gettext("Tree Shape: {0}"), shapeText); + let orderText = PVE.UIOptions.tagOrderOptions[value.ordering ?? '__default__']; + txt += `, ${Ext.String.format(gettext("Ordering: {0}"), orderText)}`; + if (value['case-sensitive']) { + txt += `, ${gettext('Case-Sensitive')}`; + } + if (Object.keys(colors).length > 0) { + txt += `, ${gettext('Color Overrides')}: `; + for (const tag of Object.keys(colors)) { + txt += Proxmox.Utils.getTagElement(tag, colors); + } + } + return txt; + }, + header: gettext('Tag Style Override'), + editor: { + xtype: 'proxmoxWindowEdit', + width: 800, + subject: gettext('Tag Color Override'), + onlineHelp: 'datacenter_configuration_file', + fieldDefaults: { + labelWidth: 100, + }, + url: '/api2/extjs/cluster/options', + items: [ + { + xtype: 'inputpanel', + setValues: function(values) { + if (values === undefined) { + return undefined; + } + values = values?.['tag-style'] ?? {}; + values.shape = values.shape || '__default__'; + values.colors = values['color-map']; + return Proxmox.panel.InputPanel.prototype.setValues.call(this, values); + }, + onGetValues: function(values) { + let style = {}; + if (values.colors) { + style['color-map'] = values.colors; + } + if (values.shape && values.shape !== '__default__') { + style.shape = values.shape; + } + if (values.ordering) { + style.ordering = values.ordering; + } + if (values['case-sensitive']) { + style['case-sensitive'] = 1; + } + let value = PVE.Parser.printPropertyString(style); + if (value === '') { + return { + 'delete': 'tag-style', + }; + } + return { + 'tag-style': value, + }; + }, + items: [ + { + + name: 'shape', + xtype: 'proxmoxComboGrid', + fieldLabel: gettext('Tree Shape'), + valueField: 'value', + displayField: 'display', + allowBlank: false, + listConfig: { + columns: [ + { + header: gettext('Option'), + dataIndex: 'display', + flex: 1, + }, + { + header: gettext('Preview'), + dataIndex: 'value', + renderer: function(value) { + let cls = value ?? '__default__'; + if (value === '__default__') { + cls = 'circle'; + } + let tags = PVE.Utils.renderTags('preview'); + return `
${tags}
`; + }, + flex: 1, + }, + ], + }, + store: { + data: Object.entries(PVE.UIOptions.tagTreeStyles).map(v => ({ + value: v[0], + display: v[1], + })), + }, + deleteDefault: true, + defaultValue: '__default__', + deleteEmpty: true, + }, + { + name: 'ordering', + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Ordering'), + comboItems: Object.entries(PVE.UIOptions.tagOrderOptions), + defaultValue: '__default__', + value: '__default__', + deleteEmpty: true, + }, + { + name: 'case-sensitive', + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Case-Sensitive'), + boxLabel: gettext('Applies to new edits'), + value: 0, + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Color Overrides'), + }, + { + name: 'colors', + xtype: 'pveTagColorGrid', + deleteEmpty: true, + height: 300, + }, + ], + }, + ], + }, + }; + + me.rows['user-tag-access'] = { + required: true, + renderer: (value) => { + if (value === undefined) { + return Ext.String.format(gettext('Mode: {0}'), 'free'); + } + let mode = value?.['user-allow'] ?? 'free'; + let list = value?.['user-allow-list']?.join(',') ?? ''; + let modeTxt = Ext.String.format(gettext('Mode: {0}'), mode); + let overrides = PVE.UIOptions.tagOverrides; + let tags = PVE.Utils.renderTags(list, overrides); + let listTxt = tags !== '' ? `, ${gettext('Pre-defined:')} ${tags}` : ''; + return `${modeTxt}${listTxt}`; + }, + header: gettext('User Tag Access'), + editor: { + xtype: 'pveUserTagAccessEdit', + }, + }; + + me.rows['registered-tags'] = { + required: true, + renderer: (value) => { + if (value === undefined) { + return gettext('No Registered Tags'); + } + let overrides = PVE.UIOptions.tagOverrides; + return PVE.Utils.renderTags(value.join(','), overrides); + }, + header: gettext('Registered Tags'), + editor: { + xtype: 'pveRegisteredTagEdit', + }, + }; + + me.selModel = Ext.create('Ext.selection.RowModel', {}); + + Ext.apply(me, { + tbar: [{ + text: gettext('Edit'), + xtype: 'proxmoxButton', + disabled: true, + handler: function() { me.run_editor(); }, + selModel: me.selModel, + }], + url: "/api2/json/cluster/options", + editorConfig: { + url: "/api2/extjs/cluster/options", + }, + interval: 5000, + cwidth1: 200, + listeners: { + itemdblclick: me.run_editor, + }, + }); + + me.callParent(); + + // set the new value for the default console + me.mon(me.rstore, 'load', function(store, records, success) { + if (!success) { + return; + } + + var rec = store.getById('console'); + PVE.UIOptions.options.console = rec.data.value; + if (rec.data.value === '__default__') { + delete PVE.UIOptions.options.console; + } + + PVE.UIOptions.options['tag-style'] = store.getById('tag-style')?.data?.value; + PVE.UIOptions.updateTagSettings(PVE.UIOptions.options['tag-style']); + PVE.UIOptions.fireUIConfigChanged(); + }); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + }, +}); +Ext.define('pve-permissions', { + extend: 'Ext.data.TreeModel', + fields: [ + 'text', 'type', + { + type: 'boolean', name: 'propagate', + }, + ], +}); + +Ext.define('PVE.dc.PermissionGridPanel', { + extend: 'Ext.tree.Panel', + alias: 'widget.pveUserPermissionGrid', + + onlineHelp: 'chapter_user_management', + + scrollable: true, + layout: 'fit', + rootVisible: false, + animate: false, + sortableColumns: false, + + columns: [ + { + xtype: 'treecolumn', + header: gettext('Path') + '/' + gettext('Permission'), + dataIndex: 'text', + flex: 6, + }, + { + header: gettext('Propagate'), + dataIndex: 'propagate', + flex: 1, + renderer: function(value) { + if (Ext.isDefined(value)) { + return Proxmox.Utils.format_boolean(value); + } + return ''; + }, + }, + ], + + initComponent: function() { + let me = this; + + Proxmox.Utils.API2Request({ + url: '/access/permissions?userid=' + me.userid, + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + me.load_task.delay(me.load_delay); + }, + success: function(response, opts) { + Proxmox.Utils.setErrorMask(me, false); + let result = Ext.decode(response.responseText); + let data = result.data || {}; + + let root = { + name: '__root', + expanded: true, + children: [], + }; + let idhash = { + '/': { + children: [], + text: '/', + type: 'path', + }, + }; + Ext.Object.each(data, function(path, perms) { + let path_item = { + text: path, + type: 'path', + children: [], + }; + Ext.Object.each(perms, function(perm, propagate) { + let perm_item = { + text: perm, + type: 'perm', + propagate: propagate === 1, + iconCls: 'fa fa-fw fa-unlock', + leaf: true, + }; + path_item.children.push(perm_item); + path_item.expandable = true; + }); + idhash[path] = path_item; + }); + + Ext.Object.each(idhash, function(path, item) { + let parent_item = idhash['/']; + if (path === '/') { + parent_item = root; + item.expanded = true; + } else { + let split_path = path.split('/'); + while (split_path.pop()) { + let parent_path = split_path.join('/'); + if (idhash[parent_path]) { + parent_item = idhash[parent_path]; + break; + } + } + } + parent_item.children.push(item); + }); + + me.setRootNode(root); + }, + }); + + me.callParent(); + + me.store.sorters.add(new Ext.util.Sorter({ + sorterFn: function(rec1, rec2) { + let v1 = rec1.data.text, + v2 = rec2.data.text; + if (rec1.data.type !== rec2.data.type) { + v2 = rec1.data.type; + v1 = rec2.data.type; + } + if (v1 > v2) { + return 1; + } else if (v1 < v2) { + return -1; + } + return 0; + }, + })); + }, +}); + +Ext.define('PVE.dc.PermissionView', { + extend: 'Ext.window.Window', + alias: 'widget.userShowPermissionWindow', + mixins: ['Proxmox.Mixin.CBind'], + + scrollable: true, + width: 800, + height: 600, + layout: 'fit', + cbind: { + title: (get) => Ext.String.htmlEncode(get('userid')) + + ` - ${gettext('Granted Permissions')}`, + }, + items: [{ + xtype: 'pveUserPermissionGrid', + cbind: { + userid: '{userid}', + }, + }], +}); +Ext.define('PVE.dc.PoolEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcPoolEdit'], + mixins: ['Proxmox.Mixin.CBind'], + + subject: gettext('Pool'), + + cbindData: { + poolid: '', + isCreate: (cfg) => !cfg.poolid, + }, + + cbind: { + autoLoad: get => !get('isCreate'), + url: get => `/api2/extjs/pools/${get('poolid')}`, + method: get => get('isCreate') ? 'POST' : 'PUT', + }, + + items: [ + { + xtype: 'pmxDisplayEditField', + fieldLabel: gettext('Name'), + cbind: { + editable: '{isCreate}', + value: '{poolid}', + }, + name: 'poolid', + allowBlank: false, + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment', + allowBlank: true, + }, + ], +}); +Ext.define('PVE.dc.PoolView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pvePoolView'], + + onlineHelp: 'pveum_pools', + + stateful: true, + stateId: 'grid-pools', + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-pools', + sorters: { + property: 'poolid', + direction: 'ASC', + }, + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/pools/', + callback: function() { + reload(); + }, + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.PoolEdit', { + poolid: rec.data.poolid, + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + var tbar = [ + { + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.dc.PoolEdit', {}); + win.on('destroy', reload); + win.show(); + }, + }, + edit_btn, remove_btn, + ]; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false, + }, + columns: [ + { + header: gettext('Name'), + width: 200, + sortable: true, + dataIndex: 'poolid', + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1, + }, + ], + listeners: { + activate: reload, + itemdblclick: run_editor, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.dc.RoleEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveDcRoleEdit', + + width: 400, + + initComponent: function() { + var me = this; + + me.isCreate = !me.roleid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/access/roles'; + method = 'POST'; + } else { + url = '/api2/extjs/access/roles/' + me.roleid; + method = 'PUT'; + } + + Ext.applyIf(me, { + subject: gettext('Role'), + url: url, + method: method, + items: [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + name: 'roleid', + value: me.roleid, + allowBlank: false, + fieldLabel: gettext('Name'), + }, + { + xtype: 'pvePrivilegesSelector', + name: 'privs', + value: me.privs, + allowBlank: false, + fieldLabel: gettext('Privileges'), + }, + ], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response) { + var data = response.result.data; + var keys = Ext.Object.getKeys(data); + + me.setValues({ + privs: keys, + roleid: me.roleid, + }); + }, + }); + } + }, +}); +Ext.define('PVE.dc.RoleView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveRoleView'], + + onlineHelp: 'pveum_roles', + + stateful: true, + stateId: 'grid-roles', + + initComponent: function() { + let me = this; + + let store = new Ext.data.Store({ + model: 'pmx-roles', + sorters: { + property: 'roleid', + direction: 'ASC', + }, + }); + Proxmox.Utils.monStoreErrors(me, store); + + let sm = Ext.create('Ext.selection.RowModel', {}); + let run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + if (rec.data.special) { + return; + } + Ext.create('PVE.dc.RoleEdit', { + roleid: rec.data.roleid, + privs: rec.data.privs, + listeners: { + destroy: () => store.load(), + }, + autoShow: true, + }); + }; + + Ext.apply(me, { + store: store, + selModel: sm, + + viewConfig: { + trackOver: false, + }, + columns: [ + { + header: gettext('Built-In'), + width: 65, + sortable: true, + dataIndex: 'special', + renderer: Proxmox.Utils.format_boolean, + }, + { + header: gettext('Name'), + width: 150, + sortable: true, + dataIndex: 'roleid', + }, + { + itemid: 'privs', + header: gettext('Privileges'), + sortable: false, + renderer: (value, metaData) => { + if (!value) { + return '-'; + } + metaData.style = 'white-space:normal;'; // allow word wrap + return value.replace(/,/g, ' '); + }, + variableRowHeight: true, + dataIndex: 'privs', + flex: 1, + }, + ], + listeners: { + activate: function() { + store.load(); + }, + itemdblclick: run_editor, + }, + tbar: [ + { + text: gettext('Create'), + handler: function() { + Ext.create('PVE.dc.RoleEdit', { + listeners: { + destroy: () => store.load(), + }, + autoShow: true, + }); + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + enableFn: (rec) => !rec.data.special, + }, + { + xtype: 'proxmoxStdRemoveButton', + selModel: sm, + callback: () => store.load(), + baseurl: '/access/roles/', + enableFn: (rec) => !rec.data.special, + }, + ], + }); + + me.callParent(); + }, +}); +Ext.define('pve-security-groups', { + extend: 'Ext.data.Model', + + fields: ['group', 'comment', 'digest'], + idProperty: 'group', +}); + +Ext.define('PVE.SecurityGroupEdit', { + extend: 'Proxmox.window.Edit', + + base_url: "/cluster/firewall/groups", + + allow_iface: false, + + initComponent: function() { + var me = this; + + me.isCreate = me.group_name === undefined; + + var subject; + + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + + var items = [ + { + xtype: 'textfield', + name: 'group', + value: me.group_name || '', + fieldLabel: gettext('Name'), + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'comment', + value: me.group_comment || '', + fieldLabel: gettext('Comment'), + }, + ]; + + if (me.isCreate) { + subject = gettext('Security Group'); + } else { + subject = gettext('Security Group') + " '" + me.group_name + "'"; + items.push({ + xtype: 'hiddenfield', + name: 'rename', + value: me.group_name, + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + // InputPanel does not have a 'create' property, does it need a 'isCreate' + isCreate: me.isCreate, + items: items, + }); + + + Ext.apply(me, { + subject: subject, + items: [ipanel], + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.SecurityGroupList', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveSecurityGroupList', + + stateful: true, + stateId: 'grid-securitygroups', + + rulePanel: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + base_url: "/cluster/firewall/groups", + + initComponent: function() { + let me = this; + if (!me.base_url) { + throw "no base_url specified"; + } + + let store = new Ext.data.Store({ + model: 'pve-security-groups', + proxy: { + type: 'proxmox', + url: '/api2/json' + me.base_url, + }, + sorters: { + property: 'group', + direction: 'ASC', + }, + }); + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let reload = function() { + let oldrec = sm.getSelection()[0]; + store.load((records, operation, success) => { + if (oldrec) { + let rec = store.findRecord('group', oldrec.data.group, 0, false, true, true); + if (rec) { + sm.select(rec); + } + } + }); + }; + + let run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + Ext.create('PVE.SecurityGroupEdit', { + digest: rec.data.digest, + group_name: rec.data.group, + group_comment: rec.data.comment, + listeners: { + destroy: () => reload(), + }, + autoShow: true, + }); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + me.addBtn = new Proxmox.button.Button({ + text: gettext('Create'), + handler: function() { + sm.deselectAll(); + var win = Ext.create('PVE.SecurityGroupEdit', {}); + win.show(); + win.on('destroy', reload); + }, + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + enableFn: function(rec) { + return rec && me.base_url; + }, + callback: () => reload(), + }); + + Ext.apply(me, { + store: store, + tbar: ['' + gettext('Group') + ':', me.addBtn, me.removeBtn, me.editBtn], + selModel: sm, + columns: [ + { + header: gettext('Group'), + dataIndex: 'group', + width: '100', + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + listeners: { + itemdblclick: run_editor, + select: function(_sm, rec) { + if (!me.rulePanel) { + me.rulePanel = me.up('panel').down('pveFirewallRules'); + } + me.rulePanel.setBaseUrl(`/cluster/firewall/groups/${rec.data.group}`); + }, + deselect: function() { + if (!me.rulePanel) { + me.rulePanel = me.up('panel').down('pveFirewallRules'); + } + me.rulePanel.setBaseUrl(undefined); + }, + show: reload, + }, + }); + + me.callParent(); + + store.load(); + }, +}); + +Ext.define('PVE.SecurityGroups', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveSecurityGroups', + + title: 'Security Groups', + onlineHelp: 'pve_firewall_security_groups', + + layout: 'border', + + items: [ + { + xtype: 'pveFirewallRules', + region: 'center', + allow_groups: false, + list_refs_url: '/cluster/firewall/refs', + tbar_prefix: '' + gettext('Rules') + ':', + border: false, + }, + { + xtype: 'pveSecurityGroupList', + region: 'west', + width: '25%', + border: false, + split: true, + }, + ], + listeners: { + show: function() { + let sglist = this.down('pveSecurityGroupList'); + sglist.fireEvent('show', sglist); + }, + }, +}); +Ext.define('PVE.dc.StorageView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveStorageView'], + + onlineHelp: 'chapter_storage', + + stateful: true, + stateId: 'grid-dc-storage', + + createStorageEditWindow: function(type, sid) { + let schema = PVE.Utils.storageSchema[type]; + if (!schema || !schema.ipanel) { + throw "no editor registered for storage type: " + type; + } + + Ext.create('PVE.storage.BaseEdit', { + paneltype: 'PVE.storage.' + schema.ipanel, + type: type, + storageId: sid, + canDoBackups: schema.backups, + autoShow: true, + listeners: { + destroy: this.reloadStore, + }, + }); + }, + + initComponent: function() { + let me = this; + + let store = new Ext.data.Store({ + model: 'pve-storage', + proxy: { + type: 'proxmox', + url: "/api2/json/storage", + }, + sorters: { + property: 'storage', + direction: 'ASC', + }, + }); + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + let { type, storage } = rec.data; + me.createStorageEditWindow(type, storage); + }; + + let edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/storage/', + callback: () => store.load(), + }); + + // else we cannot dynamically generate the add menu handlers + let addHandleGenerator = function(type) { + return function() { me.createStorageEditWindow(type); }; + }; + let addMenuItems = []; + for (const [type, storage] of Object.entries(PVE.Utils.storageSchema)) { + if (storage.hideAdd) { + continue; + } + addMenuItems.push({ + text: PVE.Utils.format_storage_type(type), + iconCls: 'fa fa-fw fa-' + storage.faIcon, + handler: addHandleGenerator(type), + }); + } + + Ext.apply(me, { + store: store, + reloadStore: () => store.load(), + selModel: sm, + viewConfig: { + trackOver: false, + }, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: addMenuItems, + }), + }, + remove_btn, + edit_btn, + ], + columns: [ + { + header: 'ID', + flex: 2, + sortable: true, + dataIndex: 'storage', + }, + { + header: gettext('Type'), + flex: 1, + sortable: true, + dataIndex: 'type', + renderer: PVE.Utils.format_storage_type, + }, + { + header: gettext('Content'), + flex: 3, + sortable: true, + dataIndex: 'content', + renderer: PVE.Utils.format_content_types, + }, + { + header: gettext('Path') + '/' + gettext('Target'), + flex: 2, + sortable: true, + dataIndex: 'path', + renderer: function(value, metaData, record) { + if (record.data.target) { + return record.data.target; + } + return value; + }, + }, + { + header: gettext('Shared'), + flex: 1, + sortable: true, + dataIndex: 'shared', + renderer: Proxmox.Utils.format_boolean, + }, + { + header: gettext('Enabled'), + flex: 1, + sortable: true, + dataIndex: 'disable', + renderer: Proxmox.Utils.format_neg_boolean, + }, + { + header: gettext('Bandwidth Limit'), + flex: 2, + sortable: true, + dataIndex: 'bwlimit', + }, + ], + listeners: { + activate: () => store.load(), + itemdblclick: run_editor, + }, + }); + + me.callParent(); + }, +}, function() { + Ext.define('pve-storage', { + extend: 'Ext.data.Model', + fields: [ + 'path', 'type', 'content', 'server', 'portal', 'target', 'export', 'storage', + { name: 'shared', type: 'boolean' }, + { name: 'disable', type: 'boolean' }, + ], + idProperty: 'storage', + }); +}); +Ext.define('PVE.dc.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcSummary', + + scrollable: true, + + bodyPadding: 5, + + layout: 'column', + + defaults: { + padding: 5, + columnWidth: 1, + }, + + items: [ + { + itemId: 'dcHealth', + xtype: 'pveDcHealth', + }, + { + itemId: 'dcGuests', + xtype: 'pveDcGuests', + }, + { + title: gettext('Resources'), + xtype: 'panel', + minHeight: 250, + bodyPadding: 5, + layout: 'hbox', + defaults: { + xtype: 'proxmoxGauge', + flex: 1, + }, + items: [ + { + title: gettext('CPU'), + itemId: 'cpu', + }, + { + title: gettext('Memory'), + itemId: 'memory', + }, + { + title: gettext('Storage'), + itemId: 'storage', + }, + ], + }, + { + itemId: 'nodeview', + xtype: 'pveDcNodeView', + height: 250, + }, + { + title: gettext('Subscriptions'), + height: 220, + items: [ + { + itemId: 'subscriptions', + xtype: 'pveHealthWidget', + userCls: 'pointer', + listeners: { + element: 'el', + click: function() { + if (this.component.userCls === 'pointer') { + window.open('https://www.proxmox.com/en/proxmox-ve/pricing', '_blank'); + } + }, + }, + }, + ], + }, + ], + + listeners: { + resize: function(panel) { + Proxmox.Utils.updateColumns(panel); + }, + }, + + initComponent: function() { + var me = this; + + var rstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 3000, + storeid: 'pve-cluster-status', + model: 'pve-dc-nodes', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/status", + }, + }); + + var gridstore = Ext.create('Proxmox.data.DiffStore', { + rstore: rstore, + filters: { + property: 'type', + value: 'node', + }, + sorters: { + property: 'id', + direction: 'ASC', + }, + }); + + me.callParent(); + + me.getComponent('nodeview').setStore(gridstore); + + var gueststatus = me.getComponent('dcGuests'); + + var cpustat = me.down('#cpu'); + var memorystat = me.down('#memory'); + var storagestat = me.down('#storage'); + var sp = Ext.state.Manager.getProvider(); + + me.mon(PVE.data.ResourceStore, 'load', function(curstore, results) { + me.suspendLayout = true; + + let cpu = 0, maxcpu = 0; + let memory = 0, maxmem = 0; + + let used = 0, total = 0; + let countedStorage = {}, usableStorages = {}; + let storages = sp.get('dash-storages') || ''; + storages.split(',').filter(v => v !== '').forEach(storage => { + usableStorages[storage] = true; + }); + + let qemu = { + running: 0, + paused: 0, + stopped: 0, + template: 0, + }; + let lxc = { + running: 0, + paused: 0, + stopped: 0, + template: 0, + }; + let error = 0; + + for (const { data } of results) { + switch (data.type) { + case 'node': + cpu += data.cpu * data.maxcpu; + maxcpu += data.maxcpu || 0; + memory += data.mem || 0; + maxmem += data.maxmem || 0; + + if (gridstore.getById(data.id)) { + let griditem = gridstore.getById(data.id); + griditem.set('cpuusage', data.cpu); + let max = data.maxmem || 1; + let val = data.mem || 0; + griditem.set('memoryusage', val / max); + griditem.set('uptime', data.uptime); + griditem.commit(); // else the store marks the field as dirty + } + break; + case 'storage': { + let sid = !data.shared || data.storage === 'local' ? data.id : data.storage; + if (!Ext.Object.isEmpty(usableStorages)) { + if (usableStorages[data.id] !== true) { + break; + } + sid = data.id; + } else if (countedStorage[sid]) { + break; + } + used += data.disk; + total += data.maxdisk; + countedStorage[sid] = true; + break; + } + case 'qemu': + qemu[data.template ? 'template' : data.status]++; + if (data.hastate === 'error') { + error++; + } + break; + case 'lxc': + lxc[data.template ? 'template' : data.status]++; + if (data.hastate === 'error') { + error++; + } + break; + default: break; + } + } + + let text = Ext.String.format(gettext('of {0} CPU(s)'), maxcpu); + cpustat.updateValue(cpu/maxcpu, text); + + text = Ext.String.format(gettext('{0} of {1}'), Proxmox.Utils.render_size(memory), Proxmox.Utils.render_size(maxmem)); + memorystat.updateValue(memory/maxmem, text); + + text = Ext.String.format(gettext('{0} of {1}'), Proxmox.Utils.render_size(used), Proxmox.Utils.render_size(total)); + storagestat.updateValue(used/total, text); + + gueststatus.updateValues(qemu, lxc, error); + + me.suspendLayout = false; + me.updateLayout(true); + }); + + let dcHealth = me.getComponent('dcHealth'); + me.mon(rstore, 'load', dcHealth.updateStatus, dcHealth); + + let subs = me.down('#subscriptions'); + me.mon(rstore, 'load', function(store, records, success) { + var level; + var mixed = false; + for (let i = 0; i < records.length; i++) { + let node = records[i]; + if (node.get('type') !== 'node' || node.get('status') === 'offline') { + continue; + } + + let curlevel = node.get('level'); + if (curlevel === '') { // no subscription beats all, set it and break the loop + level = ''; + break; + } + + if (level === undefined) { // save level + level = curlevel; + } else if (level !== curlevel) { // detect different levels + mixed = true; + } + } + + let data = { + title: Proxmox.Utils.unknownText, + text: Proxmox.Utils.unknownText, + iconCls: PVE.Utils.get_health_icon(undefined, true), + }; + if (level === '') { + data = { + title: gettext('No Subscription'), + iconCls: PVE.Utils.get_health_icon('critical', true), + text: gettext('You have at least one node without subscription.'), + }; + subs.setUserCls('pointer'); + } else if (mixed) { + data = { + title: gettext('Mixed Subscriptions'), + iconCls: PVE.Utils.get_health_icon('warning', true), + text: gettext('Warning: Your subscription levels are not the same.'), + }; + subs.setUserCls('pointer'); + } else if (level) { + data = { + title: PVE.Utils.render_support_level(level), + iconCls: PVE.Utils.get_health_icon('good', true), + text: gettext('Your subscription status is valid.'), + }; + subs.setUserCls(''); + } + + subs.setData(data); + }); + + me.on('destroy', function() { + rstore.stopUpdate(); + }); + + me.mon(sp, 'statechange', function(provider, key, value) { + if (key !== 'summarycolumns') { + return; + } + Proxmox.Utils.updateColumns(me); + }); + + rstore.startUpdate(); + }, + +}); +Ext.define('PVE.dc.Support', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcSupport', + pveGuidePath: '/pve-docs/index.html', + onlineHelp: 'getting_help', + + invalidHtml: '

No valid subscription

' + PVE.Utils.noSubKeyHtml, + + communityHtml: 'Please use the public community forum for any questions.', + + activeHtml: 'Please use our support portal for any questions. You can also use the public community forum to get additional information.', + + bugzillaHtml: '

Bug Tracking

Our bug tracking system is available here.', + + docuHtml: function() { + var me = this; + var guideUrl = window.location.origin + me.pveGuidePath; + var text = Ext.String.format('

Documentation

' + + 'The official Proxmox VE Administration Guide' + + ' is included with this installation and can be browsed at ' + + '{0}', guideUrl); + return text; + }, + + updateActive: function(data) { + var me = this; + + var html = '

' + data.productname + '

' + me.activeHtml; + html += '

' + me.docuHtml(); + html += '

' + me.bugzillaHtml; + + me.update(html); + }, + + updateCommunity: function(data) { + var me = this; + + var html = '

' + data.productname + '

' + me.communityHtml; + html += '

' + me.docuHtml(); + html += '

' + me.bugzillaHtml; + + me.update(html); + }, + + updateInactive: function(data) { + var me = this; + me.update(me.invalidHtml); + }, + + initComponent: function() { + let me = this; + + let reload = function() { + Proxmox.Utils.API2Request({ + url: '/nodes/localhost/subscription', + method: 'GET', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + me.update(`${gettext('Unable to load subscription status')}: ${response.htmlStatus}`); + }, + success: function(response, opts) { + let data = response.result.data; + if (data?.status.toLowerCase() === 'active') { + if (data.level === 'c') { + me.updateCommunity(data); + } else { + me.updateActive(data); + } + } else { + me.updateInactive(data); + } + }, + }); + }; + + Ext.apply(me, { + autoScroll: true, + bodyStyle: 'padding:10px', + listeners: { + activate: reload, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.dc.SyncWindow', { + extend: 'Ext.window.Window', + + title: gettext('Realm Sync'), + + width: 600, + bodyPadding: 10, + modal: true, + resizable: false, + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + 'form': { + validitychange: function(field, valid) { + let me = this; + me.lookup('preview_btn').setDisabled(!valid); + me.lookup('sync_btn').setDisabled(!valid); + }, + }, + 'button': { + click: function(btn) { + if (btn.reference === 'help_btn') return; + this.sync_realm(btn.reference === 'preview_btn'); + }, + }, + }, + + sync_realm: function(is_preview) { + let me = this; + let view = me.getView(); + let ipanel = me.lookup('ipanel'); + let params = ipanel.getValues(); + + let vanished_opts = []; + ['acl', 'entry', 'properties'].forEach((prop) => { + if (params[`remove-vanished-${prop}`]) { + vanished_opts.push(prop); + } + delete params[`remove-vanished-${prop}`]; + }); + if (vanished_opts.length > 0) { + params['remove-vanished'] = vanished_opts.join(';'); + } else { + params['remove-vanished'] = 'none'; + } + + params['dry-run'] = is_preview ? 1 : 0; + Proxmox.Utils.API2Request({ + url: `/access/domains/${view.realm}/sync`, + waitMsgTarget: view, + method: 'POST', + params, + failure: function(response) { + view.show(); + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response) { + view.hide(); + Ext.create('Proxmox.window.TaskViewer', { + upid: response.result.data, + listeners: { + destroy: function() { + if (is_preview) { + view.show(); + } else { + view.close(); + } + }, + }, + }).show(); + }, + }); + }, + }, + + items: [ + { + xtype: 'form', + reference: 'form', + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%', + }, + items: [{ + xtype: 'inputpanel', + reference: 'ipanel', + column1: [ + { + xtype: 'proxmoxKVComboBox', + name: 'scope', + fieldLabel: gettext('Scope'), + value: '', + emptyText: gettext('No default available'), + deleteEmpty: false, + allowBlank: false, + comboItems: [ + ['users', gettext('Users')], + ['groups', gettext('Groups')], + ['both', gettext('Users and Groups')], + ], + }, + ], + + column2: [ + { + xtype: 'proxmoxKVComboBox', + value: '1', + deleteEmpty: false, + allowBlank: false, + comboItems: [ + ['1', Proxmox.Utils.yesText], + ['0', Proxmox.Utils.noText], + ], + name: 'enable-new', + fieldLabel: gettext('Enable new'), + }, + ], + + columnB: [ + { + xtype: 'fieldset', + title: gettext('Remove Vanished Options'), + items: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('ACL'), + name: 'remove-vanished-acl', + boxLabel: gettext('Remove ACLs of vanished users and groups.'), + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Entry'), + name: 'remove-vanished-entry', + boxLabel: gettext('Remove vanished user and group entries.'), + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Properties'), + name: 'remove-vanished-properties', + boxLabel: gettext('Remove vanished properties from synced users.'), + }, + ], + }, + { + xtype: 'displayfield', + reference: 'defaulthint', + value: gettext('Default sync options can be set by editing the realm.'), + userCls: 'pmx-hint', + hidden: true, + }, + ], + }], + }, + ], + + buttons: [ + { + xtype: 'proxmoxHelpButton', + reference: 'help_btn', + onlineHelp: 'pveum_ldap_sync', + hidden: false, + }, + '->', + { + text: gettext('Preview'), + reference: 'preview_btn', + }, + { + text: gettext('Sync'), + reference: 'sync_btn', + }, + ], + + initComponent: function() { + let me = this; + + if (!me.realm) { + throw "no realm defined"; + } + + me.callParent(); + + Proxmox.Utils.API2Request({ + url: `/access/domains/${me.realm}`, + waitMsgTarget: me, + method: 'GET', + failure: function(response) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + me.close(); + }, + success: function(response) { + let default_options = response.result.data['sync-defaults-options']; + if (default_options) { + let options = PVE.Parser.parsePropertyString(default_options); + if (options['remove-vanished']) { + let opts = options['remove-vanished'].split(';'); + for (const opt of opts) { + options[`remove-vanished-${opt}`] = 1; + } + } + let ipanel = me.lookup('ipanel'); + ipanel.setValues(options); + } else { + me.lookup('defaulthint').setVisible(true); + } + + // check validity for button state + me.lookup('form').isValid(); + }, + }); + }, +}); +/* This class defines the "Tasks" tab of the bottom status panel + * Tasks are jobs with a start, end and log output + */ + +Ext.define('PVE.dc.Tasks', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveClusterTasks'], + + initComponent: function() { + let me = this; + + let taskstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'pve-cluster-tasks', + model: 'proxmox-tasks', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/tasks', + }, + }); + let store = Ext.create('Proxmox.data.DiffStore', { + rstore: taskstore, + sortAfterUpdate: true, + appendAtStart: true, + sorters: [ + { + property: 'pid', + direction: 'DESC', + }, + { + property: 'starttime', + direction: 'DESC', + }, + ], + + }); + + let run_task_viewer = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: rec.data.upid, + endtime: rec.data.endtime, + }); + win.show(); + }; + + Ext.apply(me, { + store: store, + stateful: false, + viewConfig: { + trackOver: false, + stripeRows: true, // does not work with getRowClass() + getRowClass: function(record, index) { + let taskState = record.get('status'); + if (taskState) { + let parsed = Proxmox.Utils.parse_task_status(taskState); + if (parsed === 'warning') { + return "proxmox-warning-row"; + } else if (parsed !== 'ok') { + return "proxmox-invalid-row"; + } + } + return ''; + }, + }, + sortableColumns: false, + columns: [ + { + header: gettext("Start Time"), + dataIndex: 'starttime', + width: 150, + renderer: function(value) { + return Ext.Date.format(value, "M d H:i:s"); + }, + }, + { + header: gettext("End Time"), + dataIndex: 'endtime', + width: 150, + renderer: function(value, metaData, record) { + if (record.data.pid) { + if (record.data.type === "vncproxy" || + record.data.type === "vncshell" || + record.data.type === "spiceproxy") { + metaData.tdCls = "x-grid-row-console"; + } else { + metaData.tdCls = "x-grid-row-loading"; + } + return ""; + } + return Ext.Date.format(value, "M d H:i:s"); + }, + }, + { + header: gettext("Node"), + dataIndex: 'node', + width: 100, + }, + { + header: gettext("User name"), + dataIndex: 'user', + renderer: Ext.String.htmlEncode, + width: 150, + }, + { + header: gettext("Description"), + dataIndex: 'upid', + flex: 1, + renderer: Proxmox.Utils.render_upid, + }, + { + header: gettext("Status"), + dataIndex: 'status', + width: 200, + renderer: function(value, metaData, record) { + if (record.data.pid) { + if (record.data.type !== "vncproxy") { + metaData.tdCls = "x-grid-row-loading"; + } + return ""; + } + return Proxmox.Utils.format_task_status(value); + }, + }, + ], + listeners: { + itemdblclick: run_task_viewer, + show: () => taskstore.startUpdate(), + destroy: () => taskstore.stopUpdate(), + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.dc.TokenEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcTokenEdit'], + mixins: ['Proxmox.Mixin.CBind'], + + subject: gettext('Token'), + onlineHelp: 'pveum_tokens', + + isAdd: true, + isCreate: false, + fixedUser: false, + + method: 'POST', + url: '/api2/extjs/access/users/', + + defaultFocus: 'field[disabled=false][hidden=false][name=tokenid]', + + items: { + xtype: 'inputpanel', + onGetValues: function(values) { + let me = this; + let win = me.up('pveDcTokenEdit'); + win.url = '/api2/extjs/access/users/'; + let uid = encodeURIComponent(values.userid); + let tid = encodeURIComponent(values.tokenid); + delete values.userid; + delete values.tokenid; + + win.url += `${uid}/token/${tid}`; + return values; + }, + column1: [ + { + xtype: 'pmxDisplayEditField', + cbind: { + editable: (get) => get('isCreate') && !get('fixedUser'), + }, + submitValue: true, + editConfig: { + xtype: 'pmxUserSelector', + allowBlank: false, + }, + name: 'userid', + value: Proxmox.UserName, + renderer: Ext.String.htmlEncode, + fieldLabel: gettext('User'), + }, + { + xtype: 'pmxDisplayEditField', + cbind: { + editable: '{isCreate}', + }, + name: 'tokenid', + fieldLabel: gettext('Token ID'), + submitValue: true, + minLength: 2, + allowBlank: false, + }, + ], + column2: [ + { + xtype: 'proxmoxcheckbox', + name: 'privsep', + checked: true, + uncheckedValue: 0, + fieldLabel: gettext('Privilege Separation'), + }, + { + xtype: 'pmxExpireDate', + name: 'expire', + }, + ], + columnB: [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment'), + }, + ], + }, + + initComponent: function() { + let me = this; + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + me.setValues(response.result.data); + }, + }); + } + }, + apiCallDone: function(success, response, options) { + let res = response.result.data; + if (!success || !res.value) { + return; + } + + Ext.create('PVE.dc.TokenShow', { + autoShow: true, + tokenid: res['full-tokenid'], + secret: res.value, + }); + }, +}); + +Ext.define('PVE.dc.TokenShow', { + extend: 'Ext.window.Window', + alias: ['widget.pveTokenShow'], + mixins: ['Proxmox.Mixin.CBind'], + + width: 600, + modal: true, + resizable: false, + title: gettext('Token Secret'), + + items: [ + { + xtype: 'container', + layout: 'form', + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%', + }, + padding: '0 10 10 10', + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Token ID'), + cbind: { + value: '{tokenid}', + }, + editable: false, + }, + { + xtype: 'textfield', + fieldLabel: gettext('Secret'), + inputId: 'token-secret-value', + cbind: { + value: '{secret}', + }, + editable: false, + }, + ], + }, + { + xtype: 'component', + border: false, + padding: '10 10 10 10', + userCls: 'pmx-hint', + html: gettext('Please record the API token secret - it will only be displayed now'), + }, + ], + buttons: [ + { + handler: function(b) { + document.getElementById('token-secret-value').select(); + document.execCommand("copy"); + }, + text: gettext('Copy Secret Value'), + iconCls: 'fa fa-clipboard', + }, + ], +}); +Ext.define('PVE.dc.TokenView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveTokenView'], + + onlineHelp: 'chapter_user_management', + + stateful: true, + stateId: 'grid-tokens', + + // use fixed user + fixedUser: undefined, + + initComponent: function() { + let me = this; + + let caps = Ext.state.Manager.get('GuiCap'); + + let store = new Ext.data.Store({ + id: "tokens", + model: 'pve-tokens', + sorters: 'id', + }); + + let reload = function() { + if (me.fixedUser) { + Proxmox.Utils.API2Request({ + url: `/access/users/${encodeURIComponent(me.fixedUser)}/token`, + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + me.load_task.delay(me.load_delay); + }, + success: function(response, opts) { + Proxmox.Utils.setErrorMask(me, false); + let result = Ext.decode(response.responseText); + let data = result.data || []; + let records = []; + Ext.Array.each(data, function(token) { + let r = {}; + r.id = me.fixedUser + '!' + token.tokenid; + r.userid = me.fixedUser; + r.tokenid = token.tokenid; + r.comment = token.comment; + r.expire = token.expire; + r.privsep = token.privsep === 1; + records.push(r); + }); + store.loadData(records); + }, + }); + return; + } + Proxmox.Utils.API2Request({ + url: '/access/users/?full=1', + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + me.load_task.delay(me.load_delay); + }, + success: function(response, opts) { + Proxmox.Utils.setErrorMask(me, false); + let result = Ext.decode(response.responseText); + let data = result.data || []; + let records = []; + Ext.Array.each(data, function(user) { + let tokens = user.tokens || []; + Ext.Array.each(tokens, function(token) { + let r = {}; + r.id = user.userid + '!' + token.tokenid; + r.userid = user.userid; + r.tokenid = token.tokenid; + r.comment = token.comment; + r.expire = token.expire; + r.privsep = token.privsep === 1; + records.push(r); + }); + }); + store.loadData(records); + }, + }); + }; + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let urlFromRecord = (rec) => { + let uid = encodeURIComponent(rec.data.userid); + let tid = encodeURIComponent(rec.data.tokenid); + return `/access/users/${uid}/token/${tid}`; + }; + + let run_editor = function(rec) { + if (!caps.access['User.Modify']) { + return; + } + + let win = Ext.create('PVE.dc.TokenEdit', { + method: 'PUT', + url: urlFromRecord(rec), + }); + win.setValues(rec.data); + win.on('destroy', reload); + win.show(); + }; + + let tbar = [ + { + text: gettext('Add'), + disabled: !caps.access['User.Modify'], + handler: function(btn, e, rec) { + let data = {}; + if (me.fixedUser) { + data.userid = me.fixedUser; + data.fixedUser = true; + } else if (rec && rec.data) { + data.userid = rec.data.userid; + } + let win = Ext.create('PVE.dc.TokenEdit', { + isCreate: true, + fixedUser: me.fixedUser, + }); + win.setValues(data); + win.on('destroy', reload); + win.show(); + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + disabled: true, + enableFn: (rec) => !!caps.access['User.Modify'], + selModel: sm, + handler: (btn, e, rec) => run_editor(rec), + }, + { + xtype: 'proxmoxStdRemoveButton', + selModel: sm, + enableFn: (rec) => !!caps.access['User.Modify'], + callback: reload, + getUrl: urlFromRecord, + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Show Permissions'), + disabled: true, + selModel: sm, + handler: function(btn, event, rec) { + Ext.create('PVE.dc.PermissionView', { + autoShow: true, + userid: rec.data.id, + }); + }, + }, + ]; + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false, + }, + columns: [ + { + header: gettext('User name'), + dataIndex: 'userid', + renderer: (uid) => { + let realmIndex = uid.lastIndexOf('@'); + let user = Ext.String.htmlEncode(uid.substr(0, realmIndex)); + let realm = Ext.String.htmlEncode(uid.substr(realmIndex)); + return `${user} ${realm}`; + }, + hidden: !!me.fixedUser, + flex: 2, + }, + { + header: gettext('Token Name'), + dataIndex: 'tokenid', + hideable: false, + flex: 1, + }, + { + header: gettext('Expire'), + dataIndex: 'expire', + hideable: false, + renderer: Proxmox.Utils.format_expire, + flex: 1, + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 3, + }, + { + header: gettext('Privilege Separation'), + dataIndex: 'privsep', + hideable: false, + renderer: Proxmox.Utils.format_boolean, + flex: 1, + }, + ], + listeners: { + activate: reload, + itemdblclick: (view, rec) => run_editor(rec), + }, + }); + + if (me.fixedUser) { + reload(); + } + + me.callParent(); + }, +}); + +Ext.define('PVE.window.TokenView', { + extend: 'Ext.window.Window', + mixins: ['Proxmox.Mixin.CBind'], + + modal: true, + subject: gettext('API Tokens'), + scrollable: true, + layout: 'fit', + width: 800, + height: 400, + cbind: { + title: gettext('API Tokens') + ' - {userid}', + }, + items: [{ + xtype: 'pveTokenView', + cbind: { + fixedUser: '{userid}', + }, + }], +}); +Ext.define('PVE.dc.UserEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcUserEdit'], + + isAdd: true, + + initComponent: function() { + let me = this; + + me.isCreate = !me.userid; + + let url = '/api2/extjs/access/users'; + let method = 'POST'; + if (!me.isCreate) { + url += '/' + encodeURIComponent(me.userid); + method = 'PUT'; + } + + let verifypw, pwfield; + let validate_pw = function() { + if (verifypw.getValue() !== pwfield.getValue()) { + return gettext("Passwords do not match"); + } + return true; + }; + verifypw = Ext.createWidget('textfield', { + inputType: 'password', + fieldLabel: gettext('Confirm password'), + name: 'verifypassword', + submitValue: false, + disabled: true, + hidden: true, + validator: validate_pw, + }); + + pwfield = Ext.createWidget('textfield', { + inputType: 'password', + fieldLabel: gettext('Password'), + minLength: 5, + name: 'password', + disabled: true, + hidden: true, + validator: validate_pw, + }); + + let column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'userid', + fieldLabel: gettext('User name'), + value: me.userid, + renderer: Ext.String.htmlEncode, + allowBlank: false, + submitValue: !!me.isCreate, + }, + pwfield, + verifypw, + { + xtype: 'pveGroupSelector', + name: 'groups', + multiSelect: true, + allowBlank: true, + fieldLabel: gettext('Group'), + }, + { + xtype: 'pmxExpireDate', + name: 'expire', + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enabled'), + name: 'enable', + uncheckedValue: 0, + defaultValue: 1, + checked: true, + }, + ]; + + let column2 = [ + { + xtype: 'textfield', + name: 'firstname', + fieldLabel: gettext('First Name'), + }, + { + xtype: 'textfield', + name: 'lastname', + fieldLabel: gettext('Last Name'), + }, + { + xtype: 'textfield', + name: 'email', + fieldLabel: gettext('E-Mail'), + vtype: 'proxmoxMail', + }, + ]; + + if (me.isCreate) { + column1.splice(1, 0, { + xtype: 'pmxRealmComboBox', + name: 'realm', + fieldLabel: gettext('Realm'), + allowBlank: false, + matchFieldWidth: false, + listConfig: { width: 300 }, + listeners: { + change: function(combo, realm) { + me.realm = realm; + pwfield.setVisible(realm === 'pve'); + pwfield.setDisabled(realm !== 'pve'); + verifypw.setVisible(realm === 'pve'); + verifypw.setDisabled(realm !== 'pve'); + }, + }, + submitValue: false, + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + column1: column1, + column2: column2, + columnB: [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment'), + }, + ], + advancedItems: [ + { + xtype: 'textfield', + name: 'keys', + fieldLabel: gettext('Key IDs'), + }, + ], + onGetValues: function(values) { + if (me.realm) { + values.userid = values.userid + '@' + me.realm; + } + if (!values.password) { + delete values.password; + } + return values; + }, + }); + + Ext.applyIf(me, { + subject: gettext('User'), + url: url, + method: method, + fieldDefaults: { + labelWidth: 110, // some translation are quite long (e.g., Spanish) + }, + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var data = response.result.data; + me.setValues(data); + if (data.keys) { + if (data.keys === 'x!oath' || data.keys === 'x!u2f') { + me.down('[name="keys"]').setDisabled(1); + } + } + }, + }); + } + }, +}); +Ext.define('PVE.dc.UserView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveUserView'], + + onlineHelp: 'pveum_users', + + stateful: true, + stateId: 'grid-users', + + initComponent: function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + var store = new Ext.data.Store({ + id: "users", + model: 'pmx-users', + sorters: { + property: 'userid', + direction: 'ASC', + }, + }); + let reload = () => store.load(); + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/access/users/', + dangerous: true, + enableFn: rec => caps.access['User.Modify'] && rec.data.userid !== 'root@pam', + callback: () => reload(), + }); + let run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec || !caps.access['User.Modify']) { + return; + } + Ext.create('PVE.dc.UserEdit', { + userid: rec.data.userid, + autoShow: true, + listeners: { + destroy: () => reload(), + }, + }); + }; + let edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + enableFn: function(rec) { + return !!caps.access['User.Modify']; + }, + selModel: sm, + handler: run_editor, + }); + let pwchange_btn = new Proxmox.button.Button({ + text: gettext('Password'), + disabled: true, + selModel: sm, + enableFn: function(record) { + let type = record.data['realm-type']; + if (type) { + if (PVE.Utils.authSchema[type]) { + return !!PVE.Utils.authSchema[type].pwchange; + } + } + return false; + }, + handler: function(btn, event, rec) { + Ext.create('Proxmox.window.PasswordEdit', { + userid: rec.data.userid, + autoShow: true, + listeners: { + destroy: () => reload(), + }, + }); + }, + }); + + var perm_btn = new Proxmox.button.Button({ + text: gettext('Permissions'), + disabled: true, + selModel: sm, + handler: function(btn, event, rec) { + Ext.create('PVE.dc.PermissionView', { + userid: rec.data.userid, + autoShow: true, + listeners: { + destroy: () => reload(), + }, + }); + }, + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + disabled: !caps.access['User.Modify'], + handler: function() { + Ext.create('PVE.dc.UserEdit', { + autoShow: true, + listeners: { + destroy: () => reload(), + }, + }); + }, + }, + '-', + edit_btn, + remove_btn, + '-', + pwchange_btn, + '-', + perm_btn, + ], + viewConfig: { + trackOver: false, + }, + columns: [ + { + header: gettext('User name'), + width: 200, + sortable: true, + renderer: Proxmox.Utils.render_username, + dataIndex: 'userid', + }, + { + header: gettext('Realm'), + width: 100, + sortable: true, + renderer: Proxmox.Utils.render_realm, + dataIndex: 'userid', + }, + { + header: gettext('Enabled'), + width: 80, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'enable', + }, + { + header: gettext('Expire'), + width: 80, + sortable: true, + renderer: Proxmox.Utils.format_expire, + dataIndex: 'expire', + }, + { + header: gettext('Name'), + width: 150, + sortable: true, + renderer: PVE.Utils.render_full_name, + dataIndex: 'firstname', + }, + { + header: 'TFA', + width: 50, + sortable: true, + renderer: function(v) { + let tfa_type = PVE.Parser.parseTfaType(v); + if (tfa_type === undefined) { + return Proxmox.Utils.noText; + } else if (tfa_type === 1) { + return Proxmox.Utils.yesText; + } else { + return tfa_type; + } + }, + dataIndex: 'keys', + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1, + }, + ], + listeners: { + activate: reload, + itemdblclick: run_editor, + }, + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, store); + }, +}); +Ext.define('PVE.dc.MetricServerView', { + extend: 'Ext.grid.Panel', + alias: ['widget.pveMetricServerView'], + + stateful: true, + stateId: 'grid-metricserver', + + controller: { + xclass: 'Ext.app.ViewController', + + render_type: function(value) { + switch (value) { + case 'influxdb': return "InfluxDB"; + case 'graphite': return "Graphite"; + default: return Proxmox.Utils.unknownText; + } + }, + + editWindow: function(xtype, id) { + let me = this; + Ext.create(`PVE.dc.${xtype}Edit`, { + serverid: id, + autoShow: true, + listeners: { + destroy: () => me.reload(), + }, + }); + }, + + addServer: function(button) { + this.editWindow(button.text); + }, + + editServer: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (!selection || selection.length < 1) { + return; + } + + let cfg = selection[0].data; + + let xtype = me.render_type(cfg.type); + me.editWindow(xtype, cfg.id); + }, + + reload: function() { + this.getView().getStore().load(); + }, + }, + + store: { + autoLoad: true, + id: 'metricservers', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/metrics/server', + }, + }, + + columns: [ + { + text: gettext('Name'), + flex: 2, + dataIndex: 'id', + }, + { + text: gettext('Type'), + flex: 1, + dataIndex: 'type', + renderer: 'render_type', + }, + { + text: gettext('Enabled'), + dataIndex: 'disable', + width: 100, + renderer: Proxmox.Utils.format_neg_boolean, + }, + { + text: gettext('Server'), + width: 200, + dataIndex: 'server', + }, + { + text: gettext('Port'), + width: 100, + dataIndex: 'port', + }, + ], + + tbar: [ + { + text: gettext('Add'), + menu: [ + { + text: 'Graphite', + iconCls: 'fa fa-fw fa-bar-chart', + handler: 'addServer', + }, + { + text: 'InfluxDB', + iconCls: 'fa fa-fw fa-bar-chart', + handler: 'addServer', + }, + ], + }, + { + text: gettext('Edit'), + xtype: 'proxmoxButton', + handler: 'editServer', + disabled: true, + }, + { + xtype: 'proxmoxStdRemoveButton', + baseurl: `/api2/extjs/cluster/metrics/server`, + callback: 'reload', + }, + ], + + listeners: { + itemdblclick: 'editServer', + }, + + initComponent: function() { + var me = this; + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore()); + }, +}); + +Ext.define('PVE.dc.MetricServerBaseEdit', { + extend: 'Proxmox.window.Edit', + mixins: ['Proxmox.Mixin.CBind'], + + cbindData: function() { + let me = this; + me.isCreate = !me.serverid; + me.serverid = me.serverid || ""; + me.url = `/api2/extjs/cluster/metrics/server/${me.serverid}`; + me.method = me.isCreate ? 'POST' : 'PUT'; + if (!me.isCreate) { + me.subject = `${me.subject}: ${me.serverid}`; + } + return {}; + }, + + submitUrl: function(url, values) { + return this.isCreate ? `${url}/${values.id}` : url; + }, + + initComponent: function() { + let me = this; + + me.callParent(); + + if (me.serverid) { + me.load({ + success: function(response, options) { + let values = response.result.data; + values.enable = !values.disable; + me.down('inputpanel').setValues(values); + }, + }); + } + }, +}); + +Ext.define('PVE.dc.InfluxDBEdit', { + extend: 'PVE.dc.MetricServerBaseEdit', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'metric_server_influxdb', + + subject: 'InfluxDB', + + cbindData: function() { + let me = this; + me.callParent(); + me.tokenEmptyText = me.isCreate ? '' : gettext('unchanged'); + return {}; + }, + + items: [ + { + xtype: 'inputpanel', + cbind: { + isCreate: '{isCreate}', + }, + onGetValues: function(values) { + let me = this; + values.disable = values.enable ? 0 : 1; + delete values.enable; + PVE.Utils.delete_if_default(values, 'verify-certificate', '1', me.isCreate); + return values; + }, + + column1: [ + { + xtype: 'hidden', + name: 'type', + value: 'influxdb', + cbind: { + submitValue: '{isCreate}', + }, + }, + { + xtype: 'pmxDisplayEditField', + name: 'id', + fieldLabel: gettext('Name'), + allowBlank: false, + cbind: { + editable: '{isCreate}', + value: '{serverid}', + }, + }, + { + xtype: 'proxmoxtextfield', + name: 'server', + fieldLabel: gettext('Server'), + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'port', + fieldLabel: gettext('Port'), + value: 8089, + minValue: 1, + maximum: 65536, + allowBlank: false, + }, + { + xtype: 'proxmoxKVComboBox', + name: 'influxdbproto', + fieldLabel: gettext('Protocol'), + value: '__default__', + cbind: { + deleteEmpty: '{!isCreate}', + }, + comboItems: [ + ['__default__', 'UDP'], + ['http', 'HTTP'], + ['https', 'HTTPS'], + ], + listeners: { + change: function(field, value) { + let me = this; + let view = me.up('inputpanel'); + let isUdp = value !== 'http' && value !== 'https'; + view.down('field[name=organization]').setDisabled(isUdp); + view.down('field[name=bucket]').setDisabled(isUdp); + view.down('field[name=token]').setDisabled(isUdp); + view.down('field[name=api-path-prefix]').setDisabled(isUdp); + view.down('field[name=mtu]').setDisabled(!isUdp); + view.down('field[name=timeout]').setDisabled(isUdp); + view.down('field[name=max-body-size]').setDisabled(isUdp); + view.down('field[name=verify-certificate]').setDisabled(value !== 'https'); + }, + }, + }, + ], + + column2: [ + { + xtype: 'checkbox', + name: 'enable', + fieldLabel: gettext('Enabled'), + inputValue: 1, + uncheckedValue: 0, + checked: true, + }, + { + xtype: 'proxmoxtextfield', + name: 'organization', + fieldLabel: gettext('Organization'), + emptyText: 'proxmox', + disabled: true, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'proxmoxtextfield', + name: 'bucket', + fieldLabel: gettext('Bucket'), + emptyText: 'proxmox', + disabled: true, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'proxmoxtextfield', + name: 'token', + fieldLabel: gettext('Token'), + disabled: true, + allowBlank: true, + deleteEmpty: false, + submitEmpty: false, + cbind: { + disabled: '{!isCreate}', + emptyText: '{tokenEmptyText}', + }, + }, + ], + + advancedColumn1: [ + { + xtype: 'proxmoxtextfield', + name: 'api-path-prefix', + fieldLabel: gettext('API Path Prefix'), + allowBlank: true, + disabled: true, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'proxmoxintegerfield', + name: 'timeout', + fieldLabel: gettext('Timeout (s)'), + disabled: true, + cbind: { + deleteEmpty: '{!isCreate}', + }, + minValue: 1, + emptyText: 1, + }, + { + xtype: 'proxmoxcheckbox', + name: 'verify-certificate', + fieldLabel: gettext('Verify Certificate'), + value: 1, + uncheckedValue: 0, + disabled: true, + }, + ], + + advancedColumn2: [ + { + xtype: 'proxmoxintegerfield', + name: 'max-body-size', + fieldLabel: gettext('Batch Size (b)'), + minValue: 1, + emptyText: '25000000', + submitEmpty: false, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'proxmoxintegerfield', + name: 'mtu', + fieldLabel: 'MTU', + minValue: 1, + emptyText: '1500', + submitEmpty: false, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + ], + }, + ], +}); + +Ext.define('PVE.dc.GraphiteEdit', { + extend: 'PVE.dc.MetricServerBaseEdit', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'metric_server_graphite', + + subject: 'Graphite', + + items: [ + { + xtype: 'inputpanel', + + onGetValues: function(values) { + values.disable = values.enable ? 0 : 1; + delete values.enable; + return values; + }, + + column1: [ + { + xtype: 'hidden', + name: 'type', + value: 'graphite', + cbind: { + submitValue: '{isCreate}', + }, + }, + { + xtype: 'pmxDisplayEditField', + name: 'id', + fieldLabel: gettext('Name'), + allowBlank: false, + cbind: { + editable: '{isCreate}', + value: '{serverid}', + }, + }, + { + xtype: 'proxmoxtextfield', + name: 'server', + fieldLabel: gettext('Server'), + allowBlank: false, + }, + ], + + column2: [ + { + xtype: 'checkbox', + name: 'enable', + fieldLabel: gettext('Enabled'), + inputValue: 1, + uncheckedValue: 0, + checked: true, + }, + { + xtype: 'proxmoxintegerfield', + name: 'port', + fieldLabel: gettext('Port'), + value: 2003, + minimum: 1, + maximum: 65536, + allowBlank: false, + }, + { + fieldLabel: gettext('Path'), + xtype: 'proxmoxtextfield', + emptyText: 'proxmox', + name: 'path', + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + ], + + advancedColumn1: [ + { + xtype: 'proxmoxKVComboBox', + name: 'proto', + fieldLabel: gettext('Protocol'), + value: '__default__', + cbind: { + deleteEmpty: '{!isCreate}', + }, + comboItems: [ + ['__default__', 'UDP'], + ['tcp', 'TCP'], + ], + listeners: { + change: function(field, value) { + let me = this; + me.up('inputpanel').down('field[name=timeout]').setDisabled(value !== 'tcp'); + me.up('inputpanel').down('field[name=mtu]').setDisabled(value === 'tcp'); + }, + }, + }, + ], + + advancedColumn2: [ + { + xtype: 'proxmoxintegerfield', + name: 'mtu', + fieldLabel: 'MTU', + minimum: 1, + emptyText: '1500', + submitEmpty: false, + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'proxmoxintegerfield', + name: 'timeout', + fieldLabel: gettext('TCP Timeout'), + disabled: true, + cbind: { + deleteEmpty: '{!isCreate}', + }, + minValue: 1, + emptyText: 1, + }, + ], + }, + ], +}); +Ext.define('PVE.dc.UserTagAccessEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveUserTagAccessEdit', + + subject: gettext('User Tag Access'), + onlineHelp: 'datacenter_configuration_file', + + url: '/api2/extjs/cluster/options', + + hintText: gettext('NOTE: The following tags are also defined as registered tags.'), + + controller: { + xclass: 'Ext.app.ViewController', + + tagChange: function(field, value) { + let me = this; + let view = me.getView(); + let also_registered = []; + value = Ext.isArray(value) ? value : value.split(';'); + value.forEach(tag => { + if (view.registered_tags.indexOf(tag) !== -1) { + also_registered.push(tag); + } + }); + let hint_field = me.lookup('hintField'); + hint_field.setVisible(also_registered.length > 0); + if (also_registered.length > 0) { + hint_field.setValue(`${view.hintText} ${also_registered.join(', ')}`); + } + }, + }, + + items: [ + { + xtype: 'inputpanel', + setValues: function(values) { + this.up('pveUserTagAccessEdit').registered_tags = values?.['registered-tags'] ?? []; + let data = values?.['user-tag-access'] ?? {}; + return Proxmox.panel.InputPanel.prototype.setValues.call(this, data); + }, + onGetValues: function(values) { + if (values === undefined || Object.keys(values).length === 0) { + return { 'delete': 'user-tag-access' }; + } + return { + 'user-tag-access': PVE.Parser.printPropertyString(values), + }; + }, + items: [ + { + name: 'user-allow', + fieldLabel: gettext('Mode'), + xtype: 'proxmoxKVComboBox', + deleteEmpty: false, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (free)'], + ['free', 'free'], + ['existing', 'existing'], + ['list', 'list'], + ['none', 'none'], + ], + defaultValue: '__default__', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Predefined Tags'), + }, + { + name: 'user-allow-list', + xtype: 'pveListField', + emptyText: gettext('No Tags defined'), + fieldTitle: gettext('Tag'), + maskRe: PVE.Utils.tagCharRegex, + gridConfig: { + height: 200, + scrollable: true, + }, + listeners: { + change: 'tagChange', + }, + }, + { + hidden: true, + xtype: 'displayfield', + reference: 'hintField', + userCls: 'pmx-hint', + }, + ], + }, + ], +}); +Ext.define('PVE.dc.RegisteredTagsEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveRegisteredTagEdit', + + subject: gettext('Registered Tags'), + onlineHelp: 'datacenter_configuration_file', + + url: '/api2/extjs/cluster/options', + + hintText: gettext('NOTE: The following tags are also defined in the user allow list.'), + + controller: { + xclass: 'Ext.app.ViewController', + + tagChange: function(field, value) { + let me = this; + let view = me.getView(); + let also_allowed = []; + value = Ext.isArray(value) ? value : value.split(';'); + value.forEach(tag => { + if (view.allowed_tags.indexOf(tag) !== -1) { + also_allowed.push(tag); + } + }); + let hint_field = me.lookup('hintField'); + hint_field.setVisible(also_allowed.length > 0); + if (also_allowed.length > 0) { + hint_field.setValue(`${view.hintText} ${also_allowed.join(', ')}`); + } + }, + }, + + items: [ + { + xtype: 'inputpanel', + setValues: function(values) { + let allowed_tags = values?.['user-tag-access']?.['user-allow-list'] ?? []; + this.up('pveRegisteredTagEdit').allowed_tags = allowed_tags; + let tags = values?.['registered-tags']; + return Proxmox.panel.InputPanel.prototype.setValues.call(this, { tags }); + }, + onGetValues: function(values) { + if (!values.tags) { + return { + 'delete': 'registered-tags', + }; + } else { + return { + 'registered-tags': values.tags, + }; + } + }, + items: [ + { + name: 'tags', + xtype: 'pveListField', + maskRe: PVE.Utils.tagCharRegex, + gridConfig: { + height: 200, + scrollable: true, + emptyText: gettext('No Tags defined'), + }, + listeners: { + change: 'tagChange', + }, + }, + { + hidden: true, + xtype: 'displayfield', + reference: 'hintField', + userCls: 'pmx-hint', + }, + ], + }, + ], +}); +Ext.define('PVE.lxc.CmdMenu', { + extend: 'Ext.menu.Menu', + + showSeparator: false, + initComponent: function() { + let me = this; + + let info = me.pveSelNode.data; + if (!info.node) { + throw "no node name specified"; + } + if (!info.vmid) { + throw "no CT ID specified"; + } + + let vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: `/nodes/${info.node}/${info.type}/${info.vmid}/status/${cmd}`, + method: 'POST', + failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + }; + let confirmedVMCommand = (cmd, params) => { + let msg = Proxmox.Utils.format_task_description(`vz${cmd}`, info.vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, btn => { + if (btn === 'yes') { + vm_command(cmd, params); + } + }); + }; + + let caps = Ext.state.Manager.get('GuiCap'); + let standalone = PVE.data.ResourceStore.getNodes().length < 2; + + let running = false, stopped = true, suspended = false; + switch (info.status) { + case 'running': + running = true; + stopped = false; + break; + case 'paused': + stopped = false; + suspended = true; + break; + default: break; + } + + me.title = 'CT ' + info.vmid; + + me.items = [ + { + text: gettext('Start'), + iconCls: 'fa fa-fw fa-play', + disabled: running, + handler: () => vm_command('start'), + }, + { + text: gettext('Shutdown'), + iconCls: 'fa fa-fw fa-power-off', + disabled: stopped || suspended, + handler: () => confirmedVMCommand('shutdown'), + }, + { + text: gettext('Stop'), + iconCls: 'fa fa-fw fa-stop', + disabled: stopped, + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'), + handler: () => confirmedVMCommand('stop'), + }, + { + text: gettext('Reboot'), + iconCls: 'fa fa-fw fa-refresh', + disabled: stopped, + tooltip: Ext.String.format(gettext('Reboot {0}'), 'CT'), + handler: () => confirmedVMCommand('reboot'), + }, + { + xtype: 'menuseparator', + hidden: (standalone || !caps.vms['VM.Migrate']) && !caps.vms['VM.Allocate'] && !caps.vms['VM.Clone'], + }, + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: !caps.vms['VM.Clone'], + handler: () => PVE.window.Clone.wrap(info.node, info.vmid, me.isTemplate, 'lxc'), + }, + { + text: gettext('Migrate'), + iconCls: 'fa fa-fw fa-send-o', + hidden: standalone || !caps.vms['VM.Migrate'], + handler: function() { + Ext.create('PVE.window.Migrate', { + vmtype: 'lxc', + nodename: info.node, + vmid: info.vmid, + autoShow: true, + }); + }, + }, + { + text: gettext('Convert to template'), + iconCls: 'fa fa-fw fa-file-o', + handler: function() { + let msg = Proxmox.Utils.format_task_description('vztemplate', info.vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn === 'yes') { + Proxmox.Utils.API2Request({ + url: `/nodes/${info.node}/lxc/${info.vmid}/template`, + method: 'POST', + failure: (response, opts) => Ext.Msg.alert('Error', response.htmlStatus), + }); + } + }); + }, + }, + { xtype: 'menuseparator' }, + { + text: gettext('Console'), + iconCls: 'fa fa-fw fa-terminal', + handler: () => + PVE.Utils.openDefaultConsoleWindow(true, 'lxc', info.vmid, info.node, info.vmname), + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.lxc.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.pveLXCConfig', + + onlineHelp: 'chapter_pct', + + userCls: 'proxmox-tags-full', + + initComponent: function() { + var me = this; + var vm = me.pveSelNode.data; + + var nodename = vm.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = vm.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var template = !!vm.template; + + var running = !!vm.uptime; + + var caps = Ext.state.Manager.get('GuiCap'); + + var base_url = '/nodes/' + nodename + '/lxc/' + vmid; + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json' + base_url + '/status/current', + interval: 1000, + }); + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: base_url + "/status/" + cmd, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + }); + }; + + var startBtn = Ext.create('Ext.Button', { + text: gettext('Start'), + disabled: !caps.vms['VM.PowerMgmt'] || running, + hidden: template, + handler: function() { + vm_command('start'); + }, + iconCls: 'fa fa-play', + }); + + var shutdownBtn = Ext.create('PVE.button.Split', { + text: gettext('Shutdown'), + disabled: !caps.vms['VM.PowerMgmt'] || !running, + hidden: template, + confirmMsg: Proxmox.Utils.format_task_description('vzshutdown', vmid), + handler: function() { + vm_command('shutdown'); + }, + menu: { + items: [{ + text: gettext('Reboot'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('vzreboot', vmid), + tooltip: Ext.String.format(gettext('Reboot {0}'), 'CT'), + handler: function() { + vm_command("reboot"); + }, + iconCls: 'fa fa-refresh', + }, + { + text: gettext('Stop'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('vzstop', vmid), + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'), + dangerous: true, + handler: function() { + vm_command("stop"); + }, + iconCls: 'fa fa-stop', + }], + }, + iconCls: 'fa fa-power-off', + }); + + var migrateBtn = Ext.create('Ext.Button', { + text: gettext('Migrate'), + disabled: !caps.vms['VM.Migrate'], + hidden: PVE.data.ResourceStore.getNodes().length < 2, + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: 'lxc', + nodename: nodename, + vmid: vmid, + }); + win.show(); + }, + iconCls: 'fa fa-send-o', + }); + + var moreBtn = Ext.create('Proxmox.button.Button', { + text: gettext('More'), + menu: { + items: [ + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: !caps.vms['VM.Clone'], + handler: function() { + PVE.window.Clone.wrap(nodename, vmid, template, 'lxc'); + }, + }, + { + text: gettext('Convert to template'), + disabled: template, + xtype: 'pveMenuItem', + iconCls: 'fa fa-fw fa-file-o', + hidden: !caps.vms['VM.Allocate'], + confirmMsg: Proxmox.Utils.format_task_description('vztemplate', vmid), + handler: function() { + Proxmox.Utils.API2Request({ + url: base_url + '/template', + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + }); + }, + }, + { + iconCls: 'fa fa-heartbeat ', + hidden: !caps.nodes['Sys.Console'], + text: gettext('Manage HA'), + handler: function() { + var ha = vm.hastate; + Ext.create('PVE.ha.VMResourceEdit', { + vmid: vmid, + guestType: 'ct', + isCreate: !ha || ha === 'unmanaged', + }).show(); + }, + }, + { + text: gettext('Remove'), + disabled: !caps.vms['VM.Allocate'], + itemId: 'removeBtn', + handler: function() { + Ext.create('PVE.window.SafeDestroyGuest', { + url: base_url, + item: { type: 'CT', id: vmid }, + taskName: 'vzdestroy', + }).show(); + }, + iconCls: 'fa fa-trash-o', + }, + ], +}, + }); + + var consoleBtn = Ext.create('PVE.button.ConsoleButton', { + disabled: !caps.vms['VM.Console'], + consoleType: 'lxc', + consoleName: vm.name, + hidden: template, + nodename: nodename, + vmid: vmid, + }); + + var statusTxt = Ext.create('Ext.toolbar.TextItem', { + data: { + lock: undefined, + }, + tpl: [ + '', + ' ({lock})', + '', + ], + }); + + let tagsContainer = Ext.create('PVE.panel.TagEditContainer', { + tags: vm.tags, + canEdit: !!caps.vms['VM.Config.Options'], + listeners: { + change: function(tags) { + Proxmox.Utils.API2Request({ + url: base_url + '/config', + method: 'PUT', + params: { + tags, + }, + success: function() { + me.statusStore.load(); + }, + failure: function(response) { + Ext.Msg.alert('Error', response.htmlStatus); + me.statusStore.load(); + }, + }); + }, + }, + }); + + let vm_text = `${vm.vmid} (${vm.name})`; + + Ext.apply(me, { + title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm_text, nodename), + hstateid: 'lxctab', + tbarSpacing: false, + tbar: [statusTxt, tagsContainer, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn], + defaults: { statusStore: me.statusStore }, + items: [ + { + title: gettext('Summary'), + xtype: 'pveGuestSummary', + iconCls: 'fa fa-book', + itemId: 'summary', + }, + ], + }); + + if (caps.vms['VM.Console'] && !template) { + me.items.push( + { + title: gettext('Console'), + itemId: 'consolejs', + iconCls: 'fa fa-terminal', + xtype: 'pveNoVncConsole', + vmid: vmid, + consoleType: 'lxc', + xtermjs: true, + nodename: nodename, + }, + ); + } + + me.items.push( + { + title: gettext('Resources'), + itemId: 'resources', + expandedOnInit: true, + iconCls: 'fa fa-cube', + xtype: 'pveLxcRessourceView', + }, + { + title: gettext('Network'), + iconCls: 'fa fa-exchange', + itemId: 'network', + xtype: 'pveLxcNetworkView', + }, + { + title: gettext('DNS'), + iconCls: 'fa fa-globe', + itemId: 'dns', + xtype: 'pveLxcDNS', + }, + { + title: gettext('Options'), + itemId: 'options', + iconCls: 'fa fa-gear', + xtype: 'pveLxcOptions', + }, + { + title: gettext('Task History'), + itemId: 'tasks', + iconCls: 'fa fa-list-alt', + xtype: 'proxmoxNodeTasks', + nodename: nodename, + preFilter: { + vmid, + }, + }, + ); + + if (caps.vms['VM.Backup']) { + me.items.push({ + title: gettext('Backup'), + iconCls: 'fa fa-floppy-o', + xtype: 'pveBackupView', + itemId: 'backup', + }, + { + title: gettext('Replication'), + iconCls: 'fa fa-retweet', + xtype: 'pveReplicaView', + itemId: 'replication', + }); + } + + if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback'] || + caps.vms['VM.Audit']) && !template) { + me.items.push({ + title: gettext('Snapshots'), + iconCls: 'fa fa-history', + xtype: 'pveGuestSnapshotTree', + type: 'lxc', + itemId: 'snapshot', + }); + } + + if (caps.vms['VM.Console']) { + me.items.push( + { + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + iconCls: 'fa fa-shield', + allow_iface: true, + base_url: base_url + '/firewall/rules', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall', + }, + { + xtype: 'pveFirewallOptions', + groups: ['firewall'], + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_vm_container_configuration', + title: gettext('Options'), + base_url: base_url + '/firewall/options', + fwtype: 'vm', + itemId: 'firewall-options', + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: base_url + '/firewall/aliases', + itemId: 'firewall-aliases', + }, + { + xtype: 'pveIPSet', + title: gettext('IPSet'), + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: base_url + '/firewall/ipset', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall-ipset', + }, + { + title: gettext('Log'), + groups: ['firewall'], + iconCls: 'fa fa-list', + onlineHelp: 'chapter_pve_firewall', + itemId: 'firewall-fwlog', + xtype: 'proxmoxLogView', + url: '/api2/extjs' + base_url + '/firewall/log', + }, + ); + } + + if (caps.vms['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + itemId: 'permissions', + iconCls: 'fa fa-unlock', + path: '/vms/' + vmid, + }); + } + + me.callParent(); + + var prevStatus = 'unknown'; + me.mon(me.statusStore, 'load', function(s, records, success) { + var status; + var lock; + var rec; + + if (!success) { + status = 'unknown'; + } else { + rec = s.data.get('status'); + status = rec ? rec.data.value : 'unknown'; + rec = s.data.get('template'); + template = rec ? rec.data.value : false; + rec = s.data.get('lock'); + lock = rec ? rec.data.value : undefined; + } + + statusTxt.update({ lock: lock }); + + rec = s.data.get('tags'); + tagsContainer.loadTags(rec?.data?.value); + + startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template); + shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running'); + me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped'); + consoleBtn.setDisabled(template); + + if (prevStatus === 'stopped' && status === 'running') { + let con = me.down('#consolejs'); + if (con) { + con.reload(); + } + } + + prevStatus = status; + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + }, +}); +Ext.define('PVE.lxc.CreateWizard', { + extend: 'PVE.window.Wizard', + mixins: ['Proxmox.Mixin.CBind'], + + viewModel: { + data: { + nodename: '', + storage: '', + unprivileged: true, + }, + formulas: { + cgroupMode: function(get) { + const nodeInfo = PVE.data.ResourceStore.getNodes().find( + node => node.node === get('nodename'), + ); + return nodeInfo ? nodeInfo['cgroup-mode'] : 2; + }, + }, + }, + + cbindData: { + nodename: undefined, + }, + + subject: gettext('LXC Container'), + + items: [ + { + xtype: 'inputpanel', + title: gettext('General'), + onlineHelp: 'pct_general', + column1: [ + { + xtype: 'pveNodeSelector', + name: 'nodename', + cbind: { + selectCurNode: '{!nodename}', + preferredValue: '{nodename}', + }, + bind: { + value: '{nodename}', + }, + fieldLabel: gettext('Node'), + allowBlank: false, + onlineValidator: true, + }, + { + xtype: 'pveGuestIDSelector', + name: 'vmid', // backend only knows vmid + guestType: 'lxc', + value: '', + loadNextFreeID: true, + validateExists: false, + }, + { + xtype: 'proxmoxtextfield', + name: 'hostname', + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Hostname'), + skipEmptyText: true, + allowBlank: true, + }, + { + xtype: 'proxmoxcheckbox', + name: 'unprivileged', + value: true, + bind: { + value: '{unprivileged}', + }, + fieldLabel: gettext('Unprivileged container'), + }, + { + xtype: 'proxmoxcheckbox', + name: 'features', + inputValue: 'nesting=1', + value: true, + bind: { + disabled: '{!unprivileged}', + }, + fieldLabel: gettext('Nesting'), + }, + ], + column2: [ + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true, + }, + { + xtype: 'textfield', + inputType: 'password', + name: 'password', + value: '', + fieldLabel: gettext('Password'), + allowBlank: false, + minLength: 5, + change: function(f, value) { + if (f.rendered) { + f.up().down('field[name=confirmpw]').validate(); + } + }, + }, + { + xtype: 'textfield', + inputType: 'password', + name: 'confirmpw', + value: '', + fieldLabel: gettext('Confirm password'), + allowBlank: true, + submitValue: false, + validator: function(value) { + var pw = this.up().down('field[name=password]').getValue(); + if (pw !== value) { + return "Passwords do not match!"; + } + return true; + }, + }, + { + xtype: 'proxmoxtextfield', + name: 'ssh-public-keys', + value: '', + fieldLabel: gettext('SSH public key'), + allowBlank: true, + validator: function(value) { + let pwfield = this.up().down('field[name=password]'); + if (value.length) { + let key = PVE.Parser.parseSSHKey(value); + if (!key) { + return "Failed to recognize ssh key"; + } + pwfield.allowBlank = true; + } else { + pwfield.allowBlank = false; + } + pwfield.validate(); + return true; + }, + afterRender: function() { + if (!window.FileReader) { + return; // No FileReader support in this browser + } + let cancelEvent = ev => { + ev = ev.event; + if (ev.preventDefault) { + ev.preventDefault(); + } + }; + this.inputEl.on('dragover', cancelEvent); + this.inputEl.on('dragenter', cancelEvent); + this.inputEl.on('drop', ev => { + cancelEvent(ev); + let files = ev.event.dataTransfer.files; + PVE.Utils.loadSSHKeyFromFile(files[0], v => this.setValue(v)); + }); + }, + }, + { + xtype: 'filebutton', + name: 'file', + hidden: !window.FileReader, + text: gettext('Load SSH Key File'), + listeners: { + change: function(btn, e, value) { + e = e.event; + let field = this.up().down('proxmoxtextfield[name=ssh-public-keys]'); + PVE.Utils.loadSSHKeyFromFile(e.target.files[0], v => field.setValue(v)); + btn.reset(); + }, + }, + }, + ], + }, + { + xtype: 'inputpanel', + title: gettext('Template'), + onlineHelp: 'pct_container_images', + column1: [ + { + xtype: 'pveStorageSelector', + name: 'tmplstorage', + fieldLabel: gettext('Storage'), + storageContent: 'vztmpl', + autoSelect: true, + allowBlank: false, + bind: { + value: '{storage}', + nodename: '{nodename}', + }, + }, + { + xtype: 'pveFileSelector', + name: 'ostemplate', + storageContent: 'vztmpl', + fieldLabel: gettext('Template'), + bind: { + storage: '{storage}', + nodename: '{nodename}', + }, + allowBlank: false, + }, + ], + }, + { + xtype: 'pveMultiMPPanel', + title: gettext('Disks'), + insideWizard: true, + isCreate: true, + unused: false, + confid: 'rootfs', + }, + { + xtype: 'pveLxcCPUInputPanel', + title: gettext('CPU'), + insideWizard: true, + }, + { + xtype: 'pveLxcMemoryInputPanel', + title: gettext('Memory'), + insideWizard: true, + }, + { + xtype: 'pveLxcNetworkInputPanel', + title: gettext('Network'), + insideWizard: true, + bind: { + nodename: '{nodename}', + }, + isCreate: true, + }, + { + xtype: 'pveLxcDNSInputPanel', + title: gettext('DNS'), + insideWizard: true, + }, + { + title: gettext('Confirm'), + layout: 'fit', + items: [ + { + xtype: 'grid', + store: { + model: 'KeyValue', + sorters: [{ + property: 'key', + direction: 'ASC', + }], + }, + columns: [ + { header: 'Key', width: 150, dataIndex: 'key' }, + { header: 'Value', flex: 1, dataIndex: 'value' }, + ], + }, + ], + dockedItems: [ + { + xtype: 'proxmoxcheckbox', + name: 'start', + dock: 'bottom', + margin: '5 0 0 0', + boxLabel: gettext('Start after created'), + }, + ], + listeners: { + show: function(panel) { + let wizard = this.up('window'); + let kv = wizard.getValues(); + let data = []; + Ext.Object.each(kv, function(key, value) { + if (key === 'delete' || key === 'tmplstorage') { // ignore + return; + } + if (key === 'password') { // don't show pw + return; + } + data.push({ key: key, value: value }); + }); + + let summaryStore = panel.down('grid').getStore(); + summaryStore.suspendEvents(); + summaryStore.removeAll(); + summaryStore.add(data); + summaryStore.sort(); + summaryStore.resumeEvents(); + summaryStore.fireEvent('refresh'); + }, + }, + onSubmit: function() { + let wizard = this.up('window'); + let kv = wizard.getValues(); + delete kv.delete; + + let nodename = kv.nodename; + delete kv.nodename; + delete kv.tmplstorage; + + if (!kv.pool.length) { + delete kv.pool; + } + if (!kv.password.length && kv['ssh-public-keys']) { + delete kv.password; + } + + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/lxc`, + waitMsgTarget: wizard, + method: 'POST', + params: kv, + success: function(response, opts) { + Ext.create('Proxmox.window.TaskViewer', { + autoShow: true, + upid: response.result.data, + }); + wizard.close(); + }, + failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + }, + }, + ], +}); +Ext.define('PVE.lxc.DNSInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcDNSInputPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + var deletes = []; + if (!values.searchdomain && !me.insideWizard) { + deletes.push('searchdomain'); + } + + if (values.nameserver) { + let list = values.nameserver.split(/[ ,;]+/); + values.nameserver = list.join(' '); + } else if (!me.insideWizard) { + deletes.push('nameserver'); + } + + if (deletes.length) { + values.delete = deletes.join(','); + } + + return values; + }, + + initComponent: function() { + var me = this; + + var items = [ + { + xtype: 'proxmoxtextfield', + name: 'searchdomain', + skipEmptyText: true, + fieldLabel: gettext('DNS domain'), + emptyText: gettext('use host settings'), + allowBlank: true, + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('DNS servers'), + vtype: 'IP64AddressWithSuffixList', + allowBlank: true, + emptyText: gettext('use host settings'), + name: 'nameserver', + itemId: 'nameserver', + }, + ]; + + if (me.insideWizard) { + me.column1 = items; + } else { + me.items = items; + } + + me.callParent(); + }, +}); + +Ext.define('PVE.lxc.DNSEdit', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + var ipanel = Ext.create('PVE.lxc.DNSInputPanel'); + + Ext.apply(me, { + subject: gettext('Resources'), + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + + if (values.nameserver) { + values.nameserver.replace(/[,;]/, ' '); + values.nameserver.replace(/^\s+/, ''); + } + + ipanel.setValues(values); + }, + }); + } + }, +}); + +Ext.define('PVE.lxc.DNS', { + extend: 'Proxmox.grid.PendingObjectGrid', + alias: ['widget.pveLxcDNS'], + + onlineHelp: 'pct_container_network', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + var rows = { + hostname: { + required: true, + defaultValue: me.pveSelNode.data.name, + header: gettext('Hostname'), + editor: caps.vms['VM.Config.Network'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Hostname'), + items: { + xtype: 'inputpanel', + items: { + fieldLabel: gettext('Hostname'), + xtype: 'textfield', + name: 'hostname', + vtype: 'DnsName', + allowBlank: true, + emptyText: 'CT' + vmid.toString(), + }, + onGetValues: function(values) { + var params = values; + if (values.hostname === undefined || + values.hostname === null || + values.hostname === '') { + params = { hostname: 'CT'+vmid.toString() }; + } + return params; + }, + }, + } : undefined, + }, + searchdomain: { + header: gettext('DNS domain'), + defaultValue: '', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + renderer: function(value) { + return value || gettext('use host settings'); + }, + }, + nameserver: { + header: gettext('DNS server'), + defaultValue: '', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + renderer: function(value) { + return value || gettext('use host settings'); + }, + }, + }; + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + var reload = function() { + me.rstore.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rowdef = rows[rec.data.key]; + if (!rowdef.editor) { + return; + } + + var win; + if (Ext.isString(rowdef.editor)) { + win = Ext.create(rowdef.editor, { + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/nodes/' + nodename + '/lxc/' + vmid + '/config', + }); + } else { + var config = Ext.apply({ + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/nodes/' + nodename + '/lxc/' + vmid + '/config', + }, rowdef.editor); + win = Ext.createWidget(rowdef.editor.xtype, config); + win.load(); + } + //win.load(); + win.show(); + win.on('destroy', reload); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: run_editor, + }); + + var revert_btn = new PVE.button.PendingRevert(); + + var set_button_status = function() { + let button_sm = me.getSelectionModel(); + let rec = button_sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + let key = rec.data.key; + + let rowdef = rows[key]; + edit_btn.setDisabled(!rowdef.editor); + + let pending = rec.data.delete || me.hasPendingChanges(key); + revert_btn.setDisabled(!pending); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/pending", + selModel: sm, + cwidth1: 150, + interval: 5000, + run_editor: run_editor, + tbar: [edit_btn, revert_btn], + rows: rows, + editorConfig: { + url: "/api2/extjs/" + baseurl, + }, + listeners: { + itemdblclick: run_editor, + selectionchange: set_button_status, + activate: reload, + }, + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + + me.mon(me.getStore(), 'datachanged', function() { + set_button_status(); + }); + }, +}); +Ext.define('PVE.lxc.FeaturesInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveLxcFeaturesInputPanel', + + // used to save the mounts fstypes until sending + mounts: [], + + fstypes: ['nfs', 'cifs'], + + viewModel: { + parent: null, + data: { + unprivileged: false, + }, + formulas: { + privilegedOnly: function(get) { + return get('unprivileged') ? gettext('privileged only') : ''; + }, + unprivilegedOnly: function(get) { + return !get('unprivileged') ? gettext('unprivileged only') : ''; + }, + }, + }, + + items: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('keyctl'), + name: 'keyctl', + bind: { + disabled: '{!unprivileged}', + boxLabel: '{unprivilegedOnly}', + }, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Nesting'), + name: 'nesting', + }, + { + xtype: 'proxmoxcheckbox', + name: 'nfs', + fieldLabel: 'NFS', + bind: { + disabled: '{unprivileged}', + boxLabel: '{privilegedOnly}', + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'cifs', + fieldLabel: 'SMB/CIFS', + bind: { + disabled: '{unprivileged}', + boxLabel: '{privilegedOnly}', + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'fuse', + fieldLabel: 'FUSE', + }, + { + xtype: 'proxmoxcheckbox', + name: 'mknod', + fieldLabel: gettext('Create Device Nodes'), + boxLabel: gettext('Experimental'), + }, + ], + + onGetValues: function(values) { + var me = this; + var mounts = me.mounts; + me.fstypes.forEach(function(fs) { + if (values[fs]) { + mounts.push(fs); + } + delete values[fs]; + }); + + if (mounts.length) { + values.mount = mounts.join(';'); + } + + var featuresstring = PVE.Parser.printPropertyString(values, undefined); + if (featuresstring === '') { + return { 'delete': 'features' }; + } + return { features: featuresstring }; + }, + + setValues: function(values) { + var me = this; + + me.viewModel.set('unprivileged', values.unprivileged); + + if (values.features) { + var res = PVE.Parser.parsePropertyString(values.features); + me.mounts = []; + if (res.mount) { + res.mount.split(/[; ]/).forEach(function(item) { + if (me.fstypes.indexOf(item) === -1) { + me.mounts.push(item); + } else { + res[item] = 1; + } + }); + } + this.callParent([res]); + } + }, + + initComponent: function() { + let me = this; + me.mounts = []; // reset state + me.callParent(); + }, +}); + +Ext.define('PVE.lxc.FeaturesEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveLxcFeaturesEdit', + + subject: gettext('Features'), + autoLoad: true, + width: 350, + + items: [{ + xtype: 'pveLxcFeaturesInputPanel', + }], +}); +Ext.define('PVE.lxc.MountPointInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveLxcMountPointInputPanel', + + onlineHelp: 'pct_container_storage', + + insideWizard: false, + + unused: false, // add unused disk imaged + unprivileged: false, + + vmconfig: {}, // used to select unused disks + + setUnprivileged: function(unprivileged) { + var me = this; + var vm = me.getViewModel(); + me.unprivileged = unprivileged; + vm.set('unpriv', unprivileged); + }, + + onGetValues: function(values) { + var me = this; + + var confid = me.confid || "mp"+values.mpid; + me.mp.file = me.down('field[name=file]').getValue(); + + if (me.unused) { + confid = "mp"+values.mpid; + } else if (me.isCreate) { + me.mp.file = values.hdstorage + ':' + values.disksize; + } + + // delete unnecessary fields + delete values.mpid; + delete values.hdstorage; + delete values.disksize; + delete values.diskformat; + + let setMPOpt = (k, src, v) => PVE.Utils.propertyStringSet(me.mp, src, k, v); + + setMPOpt('mp', values.mp); + let mountOpts = (values.mountoptions || []).join(';'); + setMPOpt('mountoptions', values.mountoptions, mountOpts); + setMPOpt('mp', values.mp); + setMPOpt('backup', values.backup); + setMPOpt('quota', values.quota); + setMPOpt('ro', values.ro); + setMPOpt('acl', values.acl); + setMPOpt('replicate', values.replicate); + + let res = {}; + res[confid] = PVE.Parser.printLxcMountPoint(me.mp); + return res; + }, + + setMountPoint: function(mp) { + let me = this; + let vm = me.getViewModel(); + vm.set('mptype', mp.type); + if (mp.mountoptions) { + mp.mountoptions = mp.mountoptions.split(';'); + } + me.mp = mp; + me.filterMountOptions(); + me.setValues(mp); + }, + + filterMountOptions: function() { + let me = this; + if (me.confid === 'rootfs') { + let field = me.down('field[name=mountoptions]'); + let exclude = ['nodev', 'noexec']; + let filtered = field.comboItems.filter(v => !exclude.includes(v[0])); + field.setComboItems(filtered); + } + }, + + updateVMConfig: function(vmconfig) { + let me = this; + let vm = me.getViewModel(); + me.vmconfig = vmconfig; + vm.set('unpriv', vmconfig.unprivileged); + me.down('field[name=mpid]').validate(); + }, + + setVMConfig: function(vmconfig) { + let me = this; + + me.updateVMConfig(vmconfig); + PVE.Utils.forEachMP((bus, i) => { + let name = "mp" + i.toString(); + if (!Ext.isDefined(vmconfig[name])) { + me.down('field[name=mpid]').setValue(i); + return false; + } + return undefined; + }); + }, + + setNodename: function(nodename) { + let me = this; + let vm = me.getViewModel(); + vm.set('node', nodename); + me.down('#diskstorage').setNodename(nodename); + }, + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + 'field[name=mpid]': { + change: function(field, value) { + let me = this; + let view = this.getView(); + if (view.confid !== 'rootfs') { + view.fireEvent('diskidchange', view, `mp${value}`); + } + field.validate(); + }, + }, + '#hdstorage': { + change: function(field, newValue) { + let me = this; + if (!newValue) { + return; + } + + let rec = field.store.getById(newValue); + if (!rec) { + return; + } + me.getViewModel().set('type', rec.data.type); + }, + }, + }, + init: function(view) { + let me = this; + let vm = this.getViewModel(); + view.mp = {}; + vm.set('confid', view.confid); + vm.set('unused', view.unused); + vm.set('node', view.nodename); + vm.set('unpriv', view.unprivileged); + vm.set('hideStorSelector', view.unused || !view.isCreate); + + if (view.isCreate) { // can be array if created from unused disk + vm.set('isIncludedInBackup', true); + if (view.insideWizard) { + view.filterMountOptions(); + } + } + if (view.selectFree) { + view.setVMConfig(view.vmconfig); + } + }, + }, + + viewModel: { + data: { + unpriv: false, + unused: false, + showStorageSelector: false, + mptype: '', + type: '', + confid: '', + node: '', + }, + + formulas: { + quota: function(get) { + return !(get('type') === 'zfs' || + get('type') === 'zfspool' || + get('unpriv') || + get('isBind')); + }, + hasMP: function(get) { + return !!get('confid') && !get('unused'); + }, + isRoot: function(get) { + return get('confid') === 'rootfs'; + }, + isBind: function(get) { + return get('mptype') === 'bind'; + }, + isBindOrRoot: function(get) { + return get('isBind') || get('isRoot'); + }, + }, + }, + + column1: [ + { + xtype: 'proxmoxintegerfield', + name: 'mpid', + fieldLabel: gettext('Mount Point ID'), + minValue: 0, + maxValue: PVE.Utils.mp_counts.mp - 1, + hidden: true, + allowBlank: false, + disabled: true, + bind: { + hidden: '{hasMP}', + disabled: '{hasMP}', + }, + validator: function(value) { + let view = this.up('inputpanel'); + if (!view.rendered) { + return undefined; + } + if (Ext.isDefined(view.vmconfig["mp"+value])) { + return "Mount point is already in use."; + } + return true; + }, + }, + { + xtype: 'pveDiskStorageSelector', + itemId: 'diskstorage', + storageContent: 'rootdir', + hidden: true, + autoSelect: true, + selectformat: false, + defaultSize: 8, + bind: { + hidden: '{hideStorSelector}', + disabled: '{hideStorSelector}', + nodename: '{node}', + }, + }, + { + xtype: 'textfield', + disabled: true, + submitValue: false, + fieldLabel: gettext('Disk image'), + name: 'file', + bind: { + hidden: '{!hideStorSelector}', + }, + }, + ], + + column2: [ + { + xtype: 'textfield', + name: 'mp', + value: '', + emptyText: gettext('/some/path'), + allowBlank: false, + disabled: true, + fieldLabel: gettext('Path'), + bind: { + hidden: '{isRoot}', + disabled: '{isRoot}', + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'backup', + fieldLabel: gettext('Backup'), + autoEl: { + tag: 'div', + 'data-qtip': gettext('Include volume in backup job'), + }, + bind: { + hidden: '{isRoot}', + disabled: '{isBindOrRoot}', + value: '{isIncludedInBackup}', + }, + }, + ], + + advancedColumn1: [ + { + xtype: 'proxmoxcheckbox', + name: 'quota', + defaultValue: 0, + bind: { + disabled: '{!quota}', + }, + fieldLabel: gettext('Enable quota'), + listeners: { + disable: function() { + this.reset(); + }, + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'ro', + defaultValue: 0, + bind: { + hidden: '{isRoot}', + disabled: '{isRoot}', + }, + fieldLabel: gettext('Read-only'), + }, + { + xtype: 'proxmoxKVComboBox', + name: 'mountoptions', + fieldLabel: gettext('Mount options'), + deleteEmpty: false, + comboItems: [ + ['lazytime', 'lazytime'], + ['noatime', 'noatime'], + ['nodev', 'nodev'], + ['noexec', 'noexec'], + ['nosuid', 'nosuid'], + ], + multiSelect: true, + value: [], + allowBlank: true, + }, + ], + + advancedColumn2: [ + { + xtype: 'proxmoxKVComboBox', + name: 'acl', + fieldLabel: 'ACLs', + deleteEmpty: false, + comboItems: [ + ['__default__', Proxmox.Utils.defaultText], + ['1', Proxmox.Utils.enabledText], + ['0', Proxmox.Utils.disabledText], + ], + value: '__default__', + bind: { + disabled: '{isBind}', + }, + allowBlank: true, + }, + { + xtype: 'proxmoxcheckbox', + inputValue: '0', // reverses the logic + name: 'replicate', + fieldLabel: gettext('Skip replication'), + }, + ], +}); + +Ext.define('PVE.lxc.MountPointEdit', { + extend: 'Proxmox.window.Edit', + + unprivileged: false, + + initComponent: function() { + let me = this; + + let nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + let unused = me.confid && me.confid.match(/^unused\d+$/); + + me.isCreate = me.confid ? unused : true; + + let ipanel = Ext.create('PVE.lxc.MountPointInputPanel', { + confid: me.confid, + nodename: nodename, + unused: unused, + unprivileged: me.unprivileged, + isCreate: me.isCreate, + }); + + let subject; + if (unused) { + subject = gettext('Unused Disk'); + } else if (me.isCreate) { + subject = gettext('Mount Point'); + } else { + subject = gettext('Mount Point') + ' (' + me.confid + ')'; + } + + Ext.apply(me, { + subject: subject, + defaultFocus: me.confid !== 'rootfs' ? 'textfield[name=mp]' : 'tool', + items: ipanel, + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + + if (me.confid) { + let value = response.result.data[me.confid]; + let mp = PVE.Parser.parseLxcMountPoint(value); + if (!mp) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse mount point options'); + me.close(); + return; + } + ipanel.setMountPoint(mp); + me.isValid(); // trigger validation + } + }, + }); + }, +}); +Ext.define('PVE.window.MPResize', { + extend: 'Ext.window.Window', + + resizable: false, + + resize_disk: function(disk, size) { + var me = this; + var params = { disk: disk, size: '+' + size + 'G' }; + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/resize', + waitMsgTarget: me, + method: 'PUT', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskViewer', { upid: upid }); + win.show(); + me.close(); + }, + }); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + var items = [ + { + xtype: 'displayfield', + name: 'disk', + value: me.disk, + fieldLabel: gettext('Disk'), + vtype: 'StorageId', + allowBlank: false, + }, + ]; + + me.hdsizesel = Ext.createWidget('numberfield', { + name: 'size', + minValue: 0, + maxValue: 128*1024, + decimalPrecision: 3, + value: '0', + fieldLabel: gettext('Size Increment') + ' (GiB)', + allowBlank: false, + }); + + items.push(me.hdsizesel); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 120, + anchor: '100%', + }, + items: items, + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + me.title = gettext('Resize disk'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Resize disk'), + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.resize_disk(me.disk, values.size); + } + }, + }); + + Ext.apply(me, { + modal: true, + border: false, + layout: 'fit', + buttons: [submitBtn], + items: [me.formPanel], + }); + + me.callParent(); + }, +}); +Ext.define('PVE.lxc.NetworkInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcNetworkInputPanel', + + insideWizard: false, + + onlineHelp: 'pct_container_network', + + setNodename: function(nodename) { + let me = this; + + if (!nodename || me.nodename === nodename) { + return; + } + me.nodename = nodename; + + let bridgeSelector = me.query("[isFormField][name=bridge]")[0]; + bridgeSelector.setNodename(nodename); + }, + + onGetValues: function(values) { + let me = this; + + let id; + if (me.isCreate) { + id = values.id; + delete values.id; + } else { + id = me.ifname; + } + let newdata = {}; + if (id) { + if (values.ipv6mode !== 'static') { + values.ip6 = values.ipv6mode; + } + if (values.ipv4mode !== 'static') { + values.ip = values.ipv4mode; + } + newdata[id] = PVE.Parser.printLxcNetwork(values); + } + return newdata; + }, + + initComponent: function() { + let me = this; + + let cdata = {}; + if (me.insideWizard) { + me.ifname = 'net0'; + cdata.name = 'eth0'; + me.dataCache = {}; + } + cdata.firewall = me.insideWizard || me.isCreate; + + if (!me.dataCache) { + throw "no dataCache specified"; + } + + if (!me.isCreate) { + if (!me.ifname) { + throw "no interface name specified"; + } + if (!me.dataCache[me.ifname]) { + throw "no such interface '" + me.ifname + "'"; + } + cdata = PVE.Parser.parseLxcNetwork(me.dataCache[me.ifname]); + } + + for (let i = 0; i < 32; i++) { + let ifname = 'net' + i.toString(); + if (me.isCreate && !me.dataCache[ifname]) { + me.ifname = ifname; + break; + } + } + + me.column1 = [ + { + xtype: 'hidden', + name: 'id', + value: me.ifname, + }, + { + xtype: 'textfield', + name: 'name', + fieldLabel: gettext('Name'), + emptyText: '(e.g., eth0)', + allowBlank: false, + value: cdata.name, + validator: function(value) { + for (const [key, netRaw] of Object.entries(me.dataCache)) { + if (!key.match(/^net\d+/) || key === me.ifname) { + continue; + } + let net = PVE.Parser.parseLxcNetwork(netRaw); + if (net.name === value) { + return "interface name already in use"; + } + } + return true; + }, + }, + { + xtype: 'textfield', + name: 'hwaddr', + fieldLabel: gettext('MAC address'), + vtype: 'MacAddress', + value: cdata.hwaddr, + allowBlank: true, + emptyText: 'auto', + }, + { + xtype: 'PVE.form.BridgeSelector', + name: 'bridge', + nodename: me.nodename, + fieldLabel: gettext('Bridge'), + value: cdata.bridge, + allowBlank: false, + }, + { + xtype: 'pveVlanField', + name: 'tag', + value: cdata.tag, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Firewall'), + name: 'firewall', + value: cdata.firewall, + }, + ]; + + let dhcp4 = cdata.ip === 'dhcp'; + if (dhcp4) { + cdata.ip = ''; + cdata.gw = ''; + } + + let auto6 = cdata.ip6 === 'auto'; + let dhcp6 = cdata.ip6 === 'dhcp'; + if (auto6 || dhcp6) { + cdata.ip6 = ''; + cdata.gw6 = ''; + } + + me.column2 = [ + { + layout: { + type: 'hbox', + align: 'middle', + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: 'IPv4:', // do not localize + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv4mode', + inputValue: 'static', + checked: !dhcp4, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip]').setEmptyText( + value ? Proxmox.Utils.NoneText : "", + ); + me.down('field[name=ip]').setDisabled(!value); + me.down('field[name=gw]').setDisabled(!value); + }, + }, + }, + { + xtype: 'radiofield', + boxLabel: 'DHCP', // do not localize + name: 'ipv4mode', + inputValue: 'dhcp', + checked: dhcp4, + margin: '0 0 0 10', + }, + ], + }, + { + xtype: 'textfield', + name: 'ip', + vtype: 'IPCIDRAddress', + value: cdata.ip, + emptyText: dhcp4 ? '' : Proxmox.Utils.NoneText, + disabled: dhcp4, + fieldLabel: 'IPv4/CIDR', // do not localize + }, + { + xtype: 'textfield', + name: 'gw', + value: cdata.gw, + vtype: 'IPAddress', + disabled: dhcp4, + fieldLabel: gettext('Gateway') + ' (IPv4)', + margin: '0 0 3 0', // override bottom margin to account for the menuseparator + }, + { + xtype: 'menuseparator', + height: '3', + margin: '0', + }, + { + layout: { + type: 'hbox', + align: 'middle', + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: 'IPv6:', // do not localize + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv6mode', + inputValue: 'static', + checked: !(auto6 || dhcp6), + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip6]').setEmptyText( + value ? Proxmox.Utils.NoneText : "", + ); + me.down('field[name=ip6]').setDisabled(!value); + me.down('field[name=gw6]').setDisabled(!value); + }, + }, + }, + { + xtype: 'radiofield', + boxLabel: 'DHCP', // do not localize + name: 'ipv6mode', + inputValue: 'dhcp', + checked: dhcp6, + margin: '0 0 0 10', + }, + { + xtype: 'radiofield', + boxLabel: 'SLAAC', // do not localize + name: 'ipv6mode', + inputValue: 'auto', + checked: auto6, + margin: '0 0 0 10', + }, + ], + }, + { + xtype: 'textfield', + name: 'ip6', + value: cdata.ip6, + emptyText: dhcp6 || auto6 ? '' : Proxmox.Utils.NoneText, + vtype: 'IP6CIDRAddress', + disabled: dhcp6 || auto6, + fieldLabel: 'IPv6/CIDR', // do not localize + }, + { + xtype: 'textfield', + name: 'gw6', + vtype: 'IP6Address', + value: cdata.gw6, + disabled: dhcp6 || auto6, + fieldLabel: gettext('Gateway') + ' (IPv6)', + }, + ]; + + me.advancedColumn1 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Disconnect'), + name: 'link_down', + value: cdata.link_down, + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: 'MTU', + emptyText: gettext('Same as bridge'), + name: 'mtu', + value: cdata.mtu, + minValue: 576, + maxValue: 65535, + }, + ]; + + me.advancedColumn2 = [ + { + xtype: 'numberfield', + name: 'rate', + fieldLabel: gettext('Rate limit') + ' (MB/s)', + minValue: 0, + maxValue: 10*1024, + value: cdata.rate, + emptyText: 'unlimited', + allowBlank: true, + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.lxc.NetworkEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + initComponent: function() { + let me = this; + + if (!me.dataCache) { + throw "no dataCache specified"; + } + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + subject: gettext('Network Device') + ' (veth)', + digest: me.dataCache.digest, + items: [ + { + xtype: 'pveLxcNetworkInputPanel', + ifname: me.ifname, + nodename: me.nodename, + dataCache: me.dataCache, + isCreate: me.isCreate, + }, + ], + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.lxc.NetworkView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveLxcNetworkView', + + onlineHelp: 'pct_container_network', + + dataCache: {}, // used to store result of last load + + stateful: true, + stateId: 'grid-lxc-network', + + load: function() { + let me = this; + + Proxmox.Utils.setErrorMask(me, true); + + Proxmox.Utils.API2Request({ + url: me.url, + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, gettext('Error') + ': ' + response.htmlStatus); + }, + success: function(response, opts) { + Proxmox.Utils.setErrorMask(me, false); + let result = Ext.decode(response.responseText); + me.dataCache = result.data || {}; + let records = []; + for (const [key, value] of Object.entries(me.dataCache)) { + if (key.match(/^net\d+/)) { + let net = PVE.Parser.parseLxcNetwork(value); + net.id = key; + records.push(net); + } + } + me.store.loadData(records); + me.down('button[name=addButton]').setDisabled(records.length >= 32); + }, + }); + }, + + initComponent: function() { + let me = this; + + let nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + let vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + let caps = Ext.state.Manager.get('GuiCap'); + + me.url = `/nodes/${nodename}/lxc/${vmid}/config`; + + let store = new Ext.data.Store({ + model: 'pve-lxc-network', + sorters: [ + { + property: 'id', + direction: 'ASC', + }, + ], + }); + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec || !caps.vms['VM.Config.Network']) { + return false; // disable default-propagation when triggered by grid dblclick + } + Ext.create('PVE.lxc.NetworkEdit', { + url: me.url, + nodename: nodename, + dataCache: me.dataCache, + ifname: rec.data.id, + listeners: { + destroy: () => me.load(), + }, + autoShow: true, + }); + return undefined; // make eslint happier + }; + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + name: 'addButton', + disabled: !caps.vms['VM.Config.Network'], + handler: function() { + Ext.create('PVE.lxc.NetworkEdit', { + url: me.url, + nodename: nodename, + isCreate: true, + dataCache: me.dataCache, + listeners: { + destroy: () => me.load(), + }, + autoShow: true, + }); + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Remove'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return !!caps.vms['VM.Config.Network']; + }, + confirmMsg: ({ data }) => + Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${data.id}'`), + handler: function(btn, e, rec) { + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + method: 'PUT', + params: { + 'delete': rec.data.id, + digest: me.dataCache.digest, + }, + callback: () => me.load(), + failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + selModel: sm, + disabled: true, + enableFn: rec => !!caps.vms['VM.Config.Network'], + handler: run_editor, + }, + ], + columns: [ + { + header: 'ID', + width: 50, + dataIndex: 'id', + }, + { + header: gettext('Name'), + width: 80, + dataIndex: 'name', + }, + { + header: gettext('Bridge'), + width: 80, + dataIndex: 'bridge', + }, + { + header: gettext('Firewall'), + width: 80, + dataIndex: 'firewall', + renderer: Proxmox.Utils.format_boolean, + }, + { + header: gettext('VLAN Tag'), + width: 80, + dataIndex: 'tag', + }, + { + header: gettext('MAC address'), + width: 110, + dataIndex: 'hwaddr', + }, + { + header: gettext('IP address'), + width: 150, + dataIndex: 'ip', + renderer: function(value, metaData, rec) { + if (rec.data.ip && rec.data.ip6) { + return rec.data.ip + "
" + rec.data.ip6; + } else if (rec.data.ip6) { + return rec.data.ip6; + } else { + return rec.data.ip; + } + }, + }, + { + header: gettext('Gateway'), + width: 150, + dataIndex: 'gw', + renderer: function(value, metaData, rec) { + if (rec.data.gw && rec.data.gw6) { + return rec.data.gw + "
" + rec.data.gw6; + } else if (rec.data.gw6) { + return rec.data.gw6; + } else { + return rec.data.gw; + } + }, + }, + { + header: gettext('MTU'), + width: 80, + dataIndex: 'mtu', + }, + { + header: gettext('Disconnected'), + width: 100, + dataIndex: 'link_down', + renderer: Proxmox.Utils.format_boolean, + }, + ], + listeners: { + activate: me.load, + itemdblclick: run_editor, + }, + }); + + me.callParent(); + }, +}, function() { + Ext.define('pve-lxc-network', { + extend: "Ext.data.Model", + proxy: { type: 'memory' }, + fields: [ + 'id', + 'name', + 'hwaddr', + 'bridge', + 'ip', + 'gw', + 'ip6', + 'gw6', + 'tag', + 'firewall', + 'mtu', + 'link_down', + ], + }); +}); + +Ext.define('PVE.lxc.Options', { + extend: 'Proxmox.grid.PendingObjectGrid', + alias: ['widget.pveLxcOptions'], + + onlineHelp: 'pct_options', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + var rows = { + onboot: { + header: gettext('Start at boot'), + defaultValue: '', + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Start at boot'), + items: { + xtype: 'proxmoxcheckbox', + name: 'onboot', + uncheckedValue: 0, + defaultValue: 0, + fieldLabel: gettext('Start at boot'), + }, + } : undefined, + }, + startup: { + header: gettext('Start/Shutdown order'), + defaultValue: '', + renderer: PVE.Utils.render_kvm_startup, + editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] + ? { + xtype: 'pveWindowStartupEdit', + onlineHelp: 'pct_startup_and_shutdown', + } : undefined, + }, + ostype: { + header: gettext('OS Type'), + defaultValue: Proxmox.Utils.unknownText, + }, + arch: { + header: gettext('Architecture'), + defaultValue: Proxmox.Utils.unknownText, + }, + console: { + header: '/dev/console', + defaultValue: 1, + renderer: Proxmox.Utils.format_enabled_toggle, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: '/dev/console', + items: { + xtype: 'proxmoxcheckbox', + name: 'console', + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + checked: true, + fieldLabel: '/dev/console', + }, + } : undefined, + }, + tty: { + header: gettext('TTY count'), + defaultValue: 2, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('TTY count'), + items: { + xtype: 'proxmoxintegerfield', + name: 'tty', + minValue: 0, + maxValue: 6, + value: 2, + fieldLabel: gettext('TTY count'), + emptyText: gettext('Default'), + deleteEmpty: true, + }, + } : undefined, + }, + cmode: { + header: gettext('Console mode'), + defaultValue: 'tty', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Console mode'), + items: { + xtype: 'proxmoxKVComboBox', + name: 'cmode', + deleteEmpty: true, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + " (tty)"], + ['tty', "/dev/tty[X]"], + ['console', "/dev/console"], + ['shell', "shell"], + ], + fieldLabel: gettext('Console mode'), + }, + } : undefined, + }, + protection: { + header: gettext('Protection'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Protection'), + items: { + xtype: 'proxmoxcheckbox', + name: 'protection', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled'), + }, + } : undefined, + }, + unprivileged: { + header: gettext('Unprivileged container'), + renderer: Proxmox.Utils.format_boolean, + defaultValue: 0, + }, + features: { + header: gettext('Features'), + defaultValue: Proxmox.Utils.noneText, + editor: 'PVE.lxc.FeaturesEdit', + }, + hookscript: { + header: gettext('Hookscript'), + }, + }; + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: function() { me.run_editor(); }, + }); + + var revert_btn = new PVE.button.PendingRevert(); + + var set_button_status = function() { + let button_sm = me.getSelectionModel(); + let rec = button_sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + + var key = rec.data.key; + var pending = rec.data.delete || me.hasPendingChanges(key); + var rowdef = rows[key]; + + if (key === 'features') { + let unprivileged = me.getStore().getById('unprivileged').data.value; + let root = Proxmox.UserName === 'root@pam'; + let vmalloc = caps.vms['VM.Allocate']; + edit_btn.setDisabled(!(root || (vmalloc && unprivileged))); + } else { + edit_btn.setDisabled(!rowdef.editor); + } + + revert_btn.setDisabled(!pending); + }; + + + Ext.apply(me, { + url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/pending", + selModel: sm, + interval: 5000, + tbar: [edit_btn, revert_btn], + rows: rows, + editorConfig: { + url: '/api2/extjs/' + baseurl, + }, + listeners: { + itemdblclick: me.run_editor, + selectionchange: set_button_status, + }, + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + + me.mon(me.getStore(), 'datachanged', function() { + set_button_status(); + }); + }, +}); + +var labelWidth = 120; + +Ext.define('PVE.lxc.MemoryEdit', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + Ext.apply(me, { + subject: gettext('Memory'), + items: Ext.create('PVE.lxc.MemoryInputPanel'), + }); + + me.callParent(); + + me.load(); + }, +}); + + +Ext.define('PVE.lxc.CPUEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveLxcCPUEdit', + + viewModel: { + data: { + cgroupMode: 2, + }, + }, + + initComponent: function() { + let me = this; + me.getViewModel().set('cgroupMode', me.cgroupMode); + + Ext.apply(me, { + subject: gettext('CPU'), + items: Ext.create('PVE.lxc.CPUInputPanel'), + }); + + me.callParent(); + + me.load(); + }, +}); + +// The view model of the parent shoul contain a 'cgroupMode' variable (or params for v2 are used). +Ext.define('PVE.lxc.CPUInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcCPUInputPanel', + + onlineHelp: 'pct_cpu', + + insideWizard: false, + + viewModel: { + formulas: { + cpuunitsDefault: (get) => get('cgroupMode') === 1 ? 1024 : 100, + cpuunitsMax: (get) => get('cgroupMode') === 1 ? 500000 : 10000, + }, + }, + + onGetValues: function(values) { + let me = this; + let cpuunitsDefault = me.getViewModel().get('cpuunitsDefault'); + + PVE.Utils.delete_if_default(values, 'cpulimit', '0', me.insideWizard); + PVE.Utils.delete_if_default(values, 'cpuunits', `${cpuunitsDefault}`, me.insideWizard); + + return values; + }, + + advancedColumn1: [ + { + xtype: 'numberfield', + name: 'cpulimit', + minValue: 0, + value: '', + step: 1, + fieldLabel: gettext('CPU limit'), + allowBlank: true, + emptyText: gettext('unlimited'), + }, + ], + + advancedColumn2: [ + { + xtype: 'proxmoxintegerfield', + name: 'cpuunits', + fieldLabel: gettext('CPU units'), + value: '', + minValue: 8, + maxValue: '10000', + emptyText: '100', + bind: { + emptyText: '{cpuunitsDefault}', + maxValue: '{cpuunitsMax}', + }, + labelWidth: labelWidth, + deleteEmpty: true, + allowBlank: true, + }, + ], + + initComponent: function() { + var me = this; + + me.column1 = [ + { + xtype: 'proxmoxintegerfield', + name: 'cores', + minValue: 1, + maxValue: 8192, + value: me.insideWizard ? 1 : '', + fieldLabel: gettext('Cores'), + allowBlank: true, + deleteEmpty: true, + emptyText: gettext('unlimited'), + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.lxc.MemoryInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcMemoryInputPanel', + + onlineHelp: 'pct_memory', + + insideWizard: false, + + initComponent: function() { + var me = this; + + var items = [ + { + xtype: 'proxmoxintegerfield', + name: 'memory', + minValue: 16, + value: '512', + step: 32, + fieldLabel: gettext('Memory') + ' (MiB)', + labelWidth: labelWidth, + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'swap', + minValue: 0, + value: '512', + step: 32, + fieldLabel: gettext('Swap') + ' (MiB)', + labelWidth: labelWidth, + allowBlank: false, + }, + ]; + + if (me.insideWizard) { + me.column1 = items; + } else { + me.items = items; + } + + me.callParent(); + }, +}); +Ext.define('PVE.lxc.RessourceView', { + extend: 'Proxmox.grid.PendingObjectGrid', + alias: ['widget.pveLxcRessourceView'], + + onlineHelp: 'pct_configuration', + + renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { + let me = this; + let rowdef = me.rows[key] || {}; + + let txt = rowdef.header || key; + let icon = ''; + + metaData.tdAttr = "valign=middle"; + if (rowdef.tdCls) { + metaData.tdCls = rowdef.tdCls; + } else if (rowdef.iconCls) { + icon = ``; + metaData.tdCls += " pve-itype-fa"; + } + // only return icons in grid but not remove dialog + if (rowIndex !== undefined) { + return icon + txt; + } else { + return txt; + } + }, + + initComponent: function() { + var me = this; + let confid; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + var diskCap = caps.vms['VM.Config.Disk']; + + var mpeditor = caps.vms['VM.Config.Disk'] ? 'PVE.lxc.MountPointEdit' : undefined; + + const nodeInfo = PVE.data.ResourceStore.getNodes().find(node => node.node === nodename); + let cpuEditor = { + xtype: 'pveLxcCPUEdit', + cgroupMode: nodeInfo['cgroup-mode'], + }; + + var rows = { + memory: { + header: gettext('Memory'), + editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined, + defaultValue: 512, + tdCls: 'pmx-itype-icon-memory', + group: 1, + renderer: function(value) { + return Proxmox.Utils.format_size(value*1024*1024); + }, + }, + swap: { + header: gettext('Swap'), + editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined, + defaultValue: 512, + iconCls: 'refresh', + group: 2, + renderer: function(value) { + return Proxmox.Utils.format_size(value*1024*1024); + }, + }, + cores: { + header: gettext('Cores'), + editor: caps.vms['VM.Config.CPU'] ? cpuEditor : undefined, + defaultValue: '', + tdCls: 'pmx-itype-icon-processor', + group: 3, + renderer: function(value) { + var cpulimit = me.getObjectValue('cpulimit'); + var cpuunits = me.getObjectValue('cpuunits'); + var res; + if (value) { + res = value; + } else { + res = gettext('unlimited'); + } + + if (cpulimit) { + res += ' [cpulimit=' + cpulimit + ']'; + } + + if (cpuunits) { + res += ' [cpuunits=' + cpuunits + ']'; + } + return res; + }, + }, + rootfs: { + header: gettext('Root Disk'), + defaultValue: Proxmox.Utils.noneText, + editor: mpeditor, + iconCls: 'hdd-o', + group: 4, + }, + cpulimit: { + visible: false, + }, + cpuunits: { + visible: false, + }, + unprivileged: { + visible: false, + }, + }; + + PVE.Utils.forEachMP(function(bus, i) { + confid = bus + i; + var group = 5; + var header; + if (bus === 'mp') { + header = gettext('Mount Point') + ' (' + confid + ')'; + } else { + header = gettext('Unused Disk') + ' ' + i; + group += 1; + } + rows[confid] = { + group: group, + order: i, + tdCls: 'pve-itype-icon-storage', + editor: mpeditor, + header: header, + }; + }, true); + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + me.selModel = Ext.create('Ext.selection.RowModel', {}); + + var run_resize = function() { + var rec = me.selModel.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.MPResize', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + }); + + win.show(); + }; + + var run_remove = function(b, e, rec) { + Proxmox.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'delete': rec.data.key, + }, + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + }); + }; + + let run_move = function() { + let rec = me.selModel.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.HDMove', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + type: 'lxc', + }); + + win.show(); + + win.on('destroy', me.reload, me); + }; + + let run_reassign = function() { + let rec = me.selModel.getSelection()[0]; + if (!rec) { + return; + } + + Ext.create('PVE.window.GuestDiskReassign', { + disk: rec.data.key, + nodename: nodename, + autoShow: true, + vmid: vmid, + type: 'lxc', + listeners: { + destroy: () => me.reload(), + }, + }); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + selModel: me.selModel, + disabled: true, + enableFn: function(rec) { + if (!rec) { + return false; + } + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: function() { me.run_editor(); }, + }); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + defaultText: gettext('Remove'), + altText: gettext('Detach'), + selModel: me.selModel, + disabled: true, + dangerous: true, + confirmMsg: function(rec) { + let warn = Ext.String.format(gettext('Are you sure you want to remove entry {0}')); + if (this.text === this.altText) { + warn = gettext('Are you sure you want to detach entry {0}'); + } + let rendered = me.renderKey(rec.data.key, {}, rec); + let msg = Ext.String.format(warn, `'${rendered}'`); + + if (rec.data.key.match(/^unused\d+$/)) { + msg += " " + gettext('This will permanently erase all data.'); + } + return msg; + }, + handler: run_remove, + listeners: { + render: function(btn) { + // hack: calculate the max button width on first display to prevent the whole + // toolbar to move when we switch between the "Remove" and "Detach" labels + let def = btn.getSize().width; + + btn.setText(btn.altText); + let alt = btn.getSize().width; + + btn.setText(btn.defaultText); + + let optimal = alt > def ? alt : def; + btn.setSize({ width: optimal }); + }, + }, + }); + + let move_menuitem = new Ext.menu.Item({ + text: gettext('Move Storage'), + tooltip: gettext('Move volume to another storage'), + iconCls: 'fa fa-database', + selModel: me.selModel, + handler: run_move, + }); + + let reassign_menuitem = new Ext.menu.Item({ + text: gettext('Reassign Owner'), + tooltip: gettext('Reassign volume to another CT'), + iconCls: 'fa fa-cube', + handler: run_reassign, + reference: 'reassing_item', + }); + + let resize_menuitem = new Ext.menu.Item({ + text: gettext('Resize'), + iconCls: 'fa fa-plus', + selModel: me.selModel, + handler: run_resize, + }); + + let volumeaction_btn = new Proxmox.button.Button({ + text: gettext('Volume Action'), + disabled: true, + menu: { + items: [ + move_menuitem, + reassign_menuitem, + resize_menuitem, + ], + }, + }); + + let revert_btn = new PVE.button.PendingRevert(); + + let set_button_status = function() { + let rec = me.selModel.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + remove_btn.disable(); + volumeaction_btn.disable(); + revert_btn.disable(); + return; + } + let { key, value, 'delete': isDelete } = rec.data; + let rowdef = rows[key]; + + let pending = isDelete || me.hasPendingChanges(key); + let isRootFS = key === 'rootfs'; + let isDisk = isRootFS || key.match(/^(mp|unused)\d+/); + let isUnusedDisk = key.match(/^unused\d+/); + let isUsedDisk = isDisk && !isUnusedDisk; + + let noedit = isDelete || !rowdef.editor; + if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) { + let mp = PVE.Parser.parseLxcMountPoint(value); + if (mp.type !== 'volume') { + noedit = true; + } + } + edit_btn.setDisabled(noedit); + + volumeaction_btn.setDisabled(!isDisk || !diskCap); + move_menuitem.setDisabled(isUnusedDisk); + reassign_menuitem.setDisabled(isRootFS); + resize_menuitem.setDisabled(isUnusedDisk); + + remove_btn.setDisabled(!isDisk || isRootFS || !diskCap || pending); + revert_btn.setDisabled(!pending); + + remove_btn.setText(isUsedDisk ? remove_btn.altText : remove_btn.defaultText); + }; + + let sorterFn = function(rec1, rec2) { + let v1 = rec1.data.key, v2 = rec2.data.key; + + let g1 = rows[v1].group || 0, g2 = rows[v2].group || 0; + if (g1 - g2 !== 0) { + return g1 - g2; + } + + let order1 = rows[v1].order || 0, order2 = rows[v2].order || 0; + if (order1 - order2 !== 0) { + return order1 - order2; + } + + if (v1 > v2) { + return 1; + } else if (v1 < v2) { + return -1; + } else { + return 0; + } + }; + + Ext.apply(me, { + url: `/api2/json/nodes/${nodename}/lxc/${vmid}/pending`, + selModel: me.selModel, + interval: 2000, + cwidth1: 170, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Mount Point'), + iconCls: 'fa fa-fw fa-hdd-o black', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + Ext.create('PVE.lxc.MountPointEdit', { + autoShow: true, + url: `/api2/extjs/${baseurl}`, + unprivileged: me.getObjectValue('unprivileged'), + pveSelNode: me.pveSelNode, + listeners: { + destroy: () => me.reload(), + }, + }); + }, + }, + ], + }), + }, + edit_btn, + remove_btn, + volumeaction_btn, + revert_btn, + ], + rows: rows, + sorterFn: sorterFn, + editorConfig: { + pveSelNode: me.pveSelNode, + url: '/api2/extjs/' + baseurl, + }, + listeners: { + itemdblclick: me.run_editor, + selectionchange: set_button_status, + }, + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + + me.mon(me.getStore(), 'datachanged', function() { + set_button_status(); + }); + + Ext.apply(me.editorConfig, { unprivileged: me.getObjectValue('unprivileged') }); + }, +}); +Ext.define('PVE.lxc.MultiMPPanel', { + extend: 'PVE.panel.MultiDiskPanel', + alias: 'widget.pveMultiMPPanel', + + onlineHelp: 'pct_container_storage', + + controller: { + xclass: 'Ext.app.ViewController', + + // count of mps + rootfs + maxCount: PVE.Utils.mp_counts.mp + 1, + + getNextFreeDisk: function(vmconfig) { + let nextFreeDisk; + if (!vmconfig.rootfs) { + return { + confid: 'rootfs', + }; + } else { + for (let i = 0; i < PVE.Utils.mp_counts.mp; i++) { + let confid = `mp${i}`; + if (!vmconfig[confid]) { + nextFreeDisk = { + confid, + }; + break; + } + } + } + return nextFreeDisk; + }, + + addPanel: function(itemId, vmconfig, nextFreeDisk) { + let me = this; + return me.getView().add({ + vmconfig, + border: false, + showAdvanced: Ext.state.Manager.getProvider().get('proxmox-advanced-cb'), + xtype: 'pveLxcMountPointInputPanel', + confid: nextFreeDisk.confid === 'rootfs' ? 'rootfs' : null, + bind: { + nodename: '{nodename}', + unprivileged: '{unprivileged}', + }, + padding: '0 5 0 10', + itemId, + selectFree: true, + isCreate: true, + insideWizard: true, + }); + }, + + getBaseVMConfig: function() { + let me = this; + + return { + unprivileged: me.getViewModel().get('unprivileged'), + }; + }, + + diskSorter: { + sorterFn: function(rec1, rec2) { + if (rec1.data.name === 'rootfs') { + return -1; + } else if (rec2.data.name === 'rootfs') { + return 1; + } + + let mp_match = /^mp(\d+)$/; + let [, id1] = mp_match.exec(rec1.data.name); + let [, id2] = mp_match.exec(rec2.data.name); + + return parseInt(id1, 10) - parseInt(id2, 10); + }, + }, + + deleteDisabled: (view, rI, cI, item, rec) => rec.data.name === 'rootfs', + }, +}); +Ext.define('PVE.menu.Item', { + extend: 'Ext.menu.Item', + alias: 'widget.pveMenuItem', + + // set to wrap the handler callback in a confirm dialog showing this text + confirmMsg: false, + + // set to focus 'No' instead of 'Yes' button and show a warning symbol + dangerous: false, + + initComponent: function() { + let me = this; + if (me.handler) { + me.setHandler(me.handler, me.scope); + } + me.callParent(); + }, + + setHandler: function(fn, scope) { + let me = this; + me.scope = scope; + me.handler = function(button, e) { + if (me.confirmMsg) { + Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1; + Ext.Msg.show({ + title: gettext('Confirm'), + icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION, + msg: me.confirmMsg, + buttons: Ext.Msg.YESNO, + defaultFocus: me.dangerous ? 'no' : 'yes', + callback: function(btn) { + if (btn === 'yes') { + Ext.callback(fn, me.scope, [me, e], 0, me); + } + }, + }); + } else { + Ext.callback(fn, me.scope, [me, e], 0, me); + } + }; + }, +}); +Ext.define('PVE.menu.TemplateMenu', { + extend: 'Ext.menu.Menu', + + initComponent: function() { + let me = this; + + let info = me.pveSelNode.data; + if (!info.node) { + throw "no node name specified"; + } + if (!info.vmid) { + throw "no VM ID specified"; + } + + let guestType = me.pveSelNode.data.type; + if (guestType !== 'qemu' && guestType !== 'lxc') { + throw `invalid guest type ${guestType}`; + } + + let template = me.pveSelNode.data.template; + + me.title = (guestType === 'qemu' ? 'VM ' : 'CT ') + info.vmid; + + let caps = Ext.state.Manager.get('GuiCap'); + let standaloneNode = PVE.data.ResourceStore.getNodes().length < 2; + + me.items = [ + { + text: gettext('Migrate'), + iconCls: 'fa fa-fw fa-send-o', + hidden: standaloneNode || !caps.vms['VM.Migrate'], + handler: function() { + Ext.create('PVE.window.Migrate', { + vmtype: guestType, + nodename: info.node, + vmid: info.vmid, + autoShow: true, + }); + }, + }, + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: !caps.vms['VM.Clone'], + handler: function() { + Ext.create('PVE.window.Clone', { + nodename: info.node, + guestType: guestType, + vmid: info.vmid, + isTemplate: template, + autoShow: true, + }); + }, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.ceph.CephInstallWizardInfo', { + extend: 'Ext.panel.Panel', + xtype: 'pveCephInstallWizardInfo', + + html: `

Ceph?

+

"Ceph is a unified, + distributed storage system, designed for excellent performance, reliability, + and scalability."

+

+ Ceph is currently not installed on this node. This wizard + will guide you through the installation. Click on the next button below + to begin. After the initial installation, the wizard will offer to create + an initial configuration. This configuration step is only + needed once per cluster and will be skipped if a config is already present. +

+

+ Before starting the installation, please take a look at our documentation, + by clicking the help button below. If you want to gain deeper knowledge about + Ceph, visit ceph.com. +

`, +}); + +Ext.define('PVE.ceph.CephVersionSelector', { + extend: 'Ext.form.field.ComboBox', + xtype: 'pveCephVersionSelector', + + fieldLabel: gettext('Ceph version to install'), + + displayField: 'display', + valueField: 'release', + + queryMode: 'local', + editable: false, + forceSelection: true, + + store: { + fields: [ + 'release', + 'version', + { + name: 'display', + calculate: d => `${d.release} (${d.version})`, + }, + ], + proxy: { + type: 'memory', + reader: { + type: 'json', + }, + }, + data: [ + { release: "octopus", version: "15.2" }, + { release: "pacific", version: "16.2" }, + { release: "quincy", version: "17.2" }, + ], + }, +}); + +Ext.define('PVE.ceph.CephHighestVersionDisplay', { + extend: 'Ext.form.field.Display', + xtype: 'pveCephHighestVersionDisplay', + + fieldLabel: gettext('Ceph in the cluster'), + + value: 'unknown', + + // called on success with (release, versionTxt, versionParts) + gotNewestVersion: Ext.emptyFn, + + initComponent: function() { + let me = this; + + me.callParent(arguments); + + Proxmox.Utils.API2Request({ + method: 'GET', + url: '/cluster/ceph/metadata', + params: { + scope: 'versions', + }, + waitMsgTarget: me, + success: (response) => { + let res = response.result; + if (!res || !res.data || !res.data.node) { + me.setValue( + gettext('Could not detect a ceph installation in the cluster'), + ); + return; + } + let nodes = res.data.node; + if (me.nodename) { + // can happen on ceph purge, we do not yet cleanup old version data + delete nodes[me.nodename]; + } + + let maxversion = []; + let maxversiontext = ""; + for (const [_nodename, data] of Object.entries(nodes)) { + let version = data.version.parts; + if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) { + maxversion = version; + maxversiontext = data.version.str; + } + } + // FIXME: get from version selector store + const major2release = { + 13: 'luminous', + 14: 'nautilus', + 15: 'octopus', + 16: 'pacific', + 17: 'quincy', + }; + let release = major2release[maxversion[0]] || 'unknown'; + let newestVersionTxt = `${Ext.String.capitalize(release)} (${maxversiontext})`; + + if (release === 'unknown') { + me.setValue( + gettext('Could not detect a ceph installation in the cluster'), + ); + } else { + me.setValue(Ext.String.format( + gettext('Newest ceph version in cluster is {0}'), + newestVersionTxt, + )); + } + me.gotNewestVersion(release, maxversiontext, maxversion); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, +}); + +Ext.define('PVE.ceph.CephInstallWizard', { + extend: 'PVE.window.Wizard', + alias: 'widget.pveCephInstallWizard', + mixins: ['Proxmox.Mixin.CBind'], + + resizable: false, + nodename: undefined, + + viewModel: { + data: { + nodename: '', + cephRelease: 'quincy', + configuration: true, + isInstalled: false, + }, + }, + cbindData: { + nodename: undefined, + }, + + title: gettext('Setup'), + navigateNext: function() { + var tp = this.down('#wizcontent'); + var atab = tp.getActiveTab(); + + var next = tp.items.indexOf(atab) + 1; + var ntab = tp.items.getAt(next); + if (ntab) { + ntab.enable(); + tp.setActiveTab(ntab); + } + }, + setInitialTab: function(index) { + var tp = this.down('#wizcontent'); + var initialTab = tp.items.getAt(index); + initialTab.enable(); + tp.setActiveTab(initialTab); + }, + onShow: function() { + this.callParent(arguments); + var isInstalled = this.getViewModel().get('isInstalled'); + if (isInstalled) { + this.getViewModel().set('configuration', false); + this.setInitialTab(2); + } + }, + items: [ + { + xtype: 'panel', + title: gettext('Info'), + viewModel: {}, // needed to inherit parent viewModel data + border: false, + bodyBorder: false, + onlineHelp: 'chapter_pveceph', + layout: { + type: 'vbox', + align: 'stretch', + }, + defaults: { + border: false, + bodyBorder: false, + }, + items: [ + { + xtype: 'pveCephInstallWizardInfo', + }, + { + flex: 1, + }, + { + xtype: 'pveCephHighestVersionDisplay', + labelWidth: 180, + cbind: { + nodename: '{nodename}', + }, + gotNewestVersion: function(release, maxversiontext, maxversion) { + if (release === 'unknown') { + return; + } + let wizard = this.up('pveCephInstallWizard'); + wizard.getViewModel().set('cephRelease', release); + }, + }, + { + xtype: 'pveCephVersionSelector', + labelWidth: 180, + submitValue: false, + bind: { + value: '{cephRelease}', + }, + listeners: { + change: function(field, release) { + let wizard = this.up('pveCephInstallWizard'); + wizard.down('#next').setText( + Ext.String.format(gettext('Start {0} installation'), release), + ); + }, + }, + }, + ], + listeners: { + activate: function() { + // notify owning container that it should display a help button + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp); + } + let wizard = this.up('pveCephInstallWizard'); + let release = wizard.getViewModel().get('cephRelease'); + wizard.down('#back').hide(true); + wizard.down('#next').setText( + Ext.String.format(gettext('Start {0} installation'), release), + ); + }, + deactivate: function() { + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp); + } + this.up('pveCephInstallWizard').down('#next').setText(gettext('Next')); + }, + }, + }, + { + title: gettext('Installation'), + xtype: 'panel', + layout: 'fit', + cbind: { + nodename: '{nodename}', + }, + viewModel: {}, // needed to inherit parent viewModel data + listeners: { + afterrender: function() { + var me = this; + if (this.getViewModel().get('isInstalled')) { + this.mask("Ceph is already installed, click next to create your configuration.", ['pve-static-mask']); + } else { + me.down('pveNoVncConsole').fireEvent('activate'); + } + }, + activate: function() { + let me = this; + const nodename = me.nodename; + me.updateStore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'ceph-status-' + nodename, + interval: 1000, + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + nodename + '/ceph/status', + }, + listeners: { + load: function(rec, response, success, operation) { + if (success) { + me.updateStore.stopUpdate(); + me.down('textfield').setValue('success'); + } else if (operation.error.statusText.match("not initialized", "i")) { + me.updateStore.stopUpdate(); + me.up('pveCephInstallWizard').getViewModel().set('configuration', false); + me.down('textfield').setValue('success'); + } else if (operation.error.statusText.match("rados_connect failed", "i")) { + me.updateStore.stopUpdate(); + me.up('pveCephInstallWizard').getViewModel().set('configuration', true); + me.down('textfield').setValue('success'); + } else if (!operation.error.statusText.match("not installed", "i")) { + Proxmox.Utils.setErrorMask(me, operation.error.statusText); + } + }, + }, + }); + me.updateStore.startUpdate(); + }, + destroy: function() { + var me = this; + if (me.updateStore) { + me.updateStore.stopUpdate(); + } + }, + }, + items: [ + { + xtype: 'pveNoVncConsole', + itemId: 'jsconsole', + consoleType: 'cmd', + xtermjs: true, + cbind: { + nodename: '{nodename}', + }, + beforeLoad: function() { + let me = this; + let wizard = me.up('pveCephInstallWizard'); + let release = wizard.getViewModel().get('cephRelease'); + me.cmdOpts = `--version\0${release}`; + }, + cmd: 'ceph_install', + }, + { + xtype: 'textfield', + name: 'installSuccess', + value: '', + allowBlank: false, + submitValue: false, + hidden: true, + }, + ], + }, + { + xtype: 'inputpanel', + title: gettext('Configuration'), + onlineHelp: 'chapter_pveceph', + height: 300, + cbind: { + nodename: '{nodename}', + }, + viewModel: { + data: { + replicas: undefined, + minreplicas: undefined, + }, + }, + listeners: { + activate: function() { + this.up('pveCephInstallWizard').down('#submit').setText(gettext('Next')); + }, + afterrender: function() { + if (this.up('pveCephInstallWizard').getViewModel().get('configuration')) { + this.mask("Configuration already initialized", ['pve-static-mask']); + } else { + this.unmask(); + } + }, + deactivate: function() { + this.up('pveCephInstallWizard').down('#submit').setText(gettext('Finish')); + }, + }, + column1: [ + { + xtype: 'displayfield', + value: gettext('Ceph cluster configuration') + ':', + }, + { + xtype: 'proxmoxNetworkSelector', + name: 'network', + value: '', + fieldLabel: 'Public Network IP/CIDR', + autoSelect: false, + bind: { + allowBlank: '{configuration}', + }, + cbind: { + nodename: '{nodename}', + }, + }, + { + xtype: 'proxmoxNetworkSelector', + name: 'cluster-network', + fieldLabel: 'Cluster Network IP/CIDR', + allowBlank: true, + autoSelect: false, + emptyText: gettext('Same as Public Network'), + cbind: { + nodename: '{nodename}', + }, + }, + // FIXME: add hint about cluster network and/or reference user to docs?? + ], + column2: [ + { + xtype: 'displayfield', + value: gettext('First Ceph monitor') + ':', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Monitor node'), + cbind: { + value: '{nodename}', + }, + }, + { + xtype: 'displayfield', + value: gettext('Additional monitors are recommended. They can be created at any time in the Monitor tab.'), + userCls: 'pmx-hint', + }, + ], + advancedColumn1: [ + { + xtype: 'numberfield', + name: 'size', + fieldLabel: 'Number of replicas', + bind: { + value: '{replicas}', + }, + maxValue: 7, + minValue: 2, + emptyText: '3', + }, + { + xtype: 'numberfield', + name: 'min_size', + fieldLabel: 'Minimum replicas', + bind: { + maxValue: '{replicas}', + value: '{minreplicas}', + }, + minValue: 2, + maxValue: 3, + setMaxValue: function(value) { + this.maxValue = Ext.Number.from(value, 2); + // allow enough to avoid split brains with max 'size', but more makes simply no sense + if (this.maxValue > 4) { + this.maxValue = 4; + } + this.toggleSpinners(); + this.validate(); + }, + emptyText: '2', + }, + ], + onGetValues: function(values) { + ['cluster-network', 'size', 'min_size'].forEach(function(field) { + if (!values[field]) { + delete values[field]; + } + }); + return values; + }, + onSubmit: function() { + var me = this; + if (!this.up('pveCephInstallWizard').getViewModel().get('configuration')) { + var wizard = me.up('window'); + var kv = wizard.getValues(); + delete kv.delete; + var nodename = me.nodename; + delete kv.nodename; + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/ceph/init`, + waitMsgTarget: wizard, + method: 'POST', + params: kv, + success: function() { + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/ceph/mon/${nodename}`, + waitMsgTarget: wizard, + method: 'POST', + success: function() { + me.up('pveCephInstallWizard').navigateNext(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + } else { + me.up('pveCephInstallWizard').navigateNext(); + } + }, + }, + { + title: gettext('Success'), + xtype: 'panel', + border: false, + bodyBorder: false, + onlineHelp: 'pve_ceph_install', + html: '

Installation successful!

'+ + '

The basic installation and configuration is complete. Depending on your setup, some of the following steps are required to start using Ceph:

'+ + '
  1. Install Ceph on other nodes
  2. '+ + '
  3. Create additional Ceph Monitors
  4. '+ + '
  5. Create Ceph OSDs
  6. '+ + '
  7. Create Ceph Pools
'+ + '

To learn more, click on the help button below.

', + listeners: { + activate: function() { + // notify owning container that it should display a help button + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp); + } + + var tp = this.up('#wizcontent'); + var idx = tp.items.indexOf(this)-1; + for (;idx >= 0; idx--) { + var nc = tp.items.getAt(idx); + if (nc) { + nc.disable(); + } + } + }, + deactivate: function() { + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp); + } + }, + }, + onSubmit: function() { + var wizard = this.up('pveCephInstallWizard'); + wizard.close(); + }, + }, + ], +}); +Ext.define('PVE.node.CephConfigDb', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveNodeCephConfigDb', + + border: false, + store: { + proxy: { + type: 'proxmox', + }, + }, + + columns: [ + { + dataIndex: 'section', + text: 'WHO', + width: 100, + }, + { + dataIndex: 'mask', + text: 'MASK', + hidden: true, + width: 80, + }, + { + dataIndex: 'level', + hidden: true, + text: 'LEVEL', + }, + { + dataIndex: 'name', + flex: 1, + text: 'OPTION', + }, + { + dataIndex: 'value', + flex: 1, + text: 'VALUE', + }, + { + dataIndex: 'can_update_at_runtime', + text: 'Runtime Updatable', + hidden: true, + width: 80, + renderer: Proxmox.Utils.format_boolean, + }, + ], + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.store.proxy.url = '/api2/json/nodes/' + nodename + '/ceph/cfg/db'; + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore()); + me.getStore().load(); + }, +}); +Ext.define('PVE.node.CephConfig', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephConfig', + + bodyStyle: 'white-space:pre', + bodyPadding: 5, + border: false, + scrollable: true, + load: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + var msg = response.htmlStatus; + PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node, + function(win) { + me.mon(win, 'cephInstallWindowClosed', function() { + me.load(); + }); + }, + ); + }, + 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/cfg/raw', + listeners: { + activate: function() { + me.load(); + }, + }, + }); + + me.callParent(); + + me.load(); + }, +}); + +Ext.define('PVE.node.CephConfigCrush', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephConfigCrush', + + onlineHelp: 'chapter_pveceph', + + layout: 'border', + items: [{ + title: gettext('Configuration'), + xtype: 'pveNodeCephConfig', + region: 'center', + }, + { + title: 'Crush Map', // do not localize + xtype: 'pveNodeCephCrushMap', + region: 'east', + split: true, + width: '50%', + }, + { + title: gettext('Configuration Database'), + xtype: 'pveNodeCephConfigDb', + region: 'south', + split: true, + weight: -30, + height: '50%', + }], + + initComponent: function() { + var me = this; + me.defaults = { + pveSelNode: me.pveSelNode, + }; + me.callParent(); + }, +}); +Ext.define('PVE.node.CephCrushMap', { + extend: 'Ext.panel.Panel', + alias: ['widget.pveNodeCephCrushMap'], + bodyStyle: 'white-space:pre', + bodyPadding: 5, + border: false, + stateful: true, + stateId: 'layout-ceph-crush', + scrollable: true, + load: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + var msg = response.htmlStatus; + PVE.Utils.showCephInstallOrMask( + me.ownerCt, + msg, + me.pveSelNode.data.node, + win => me.mon(win, 'cephInstallWindowClosed', () => me.load()), + ); + }, + success: function(response, opts) { + var data = response.result.data; + me.update(Ext.htmlEncode(data)); + }, + }); + }, + + initComponent: function() { + let me = this; + + let nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + url: `/nodes/${nodename}/ceph/crush`, + listeners: { + activate: () => me.load(), + }, + }); + + me.callParent(); + + me.load(); + }, +}); +Ext.define('PVE.CephCreateFS', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveCephCreateFS', + + showTaskViewer: true, + onlineHelp: 'pveceph_fs_create', + + subject: 'Ceph FS', + isCreate: true, + method: 'POST', + + setFSName: function(fsName) { + var me = this; + + if (fsName === '' || fsName === undefined) { + fsName = 'cephfs'; + } + + me.url = "/nodes/" + me.nodename + "/ceph/fs/" + fsName; + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Name'), + name: 'name', + value: 'cephfs', + listeners: { + change: function(f, value) { + this.up('pveCephCreateFS').setFSName(value); + }, + }, + submitValue: false, // already encoded in apicall URL + emptyText: 'cephfs', + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: 'Placement Groups', + name: 'pg_num', + value: 128, + emptyText: 128, + minValue: 8, + maxValue: 32768, + allowBlank: false, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Add as Storage'), + value: true, + name: 'add-storage', + autoEl: { + tag: 'div', + 'data-qtip': gettext('Add the new CephFS to the cluster storage configuration.'), + }, + }, + ], + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + me.setFSName(); + + me.callParent(); + }, +}); + +Ext.define('PVE.NodeCephFSPanel', { + extend: 'Ext.panel.Panel', + xtype: 'pveNodeCephFSPanel', + mixins: ['Proxmox.Mixin.CBind'], + + title: gettext('CephFS'), + onlineHelp: 'pveceph_fs', + + border: false, + defaults: { + border: false, + cbind: { + nodename: '{nodename}', + }, + }, + + viewModel: { + parent: null, + data: { + mdsCount: 0, + }, + formulas: { + canCreateFS: function(get) { + return get('mdsCount') > 0; + }, + }, + }, + + items: [ + { + xtype: 'grid', + emptyText: Ext.String.format(gettext('No {0} configured.'), 'CephFS'), + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.rstore = Ext.create('Proxmox.data.UpdateStore', { + autoLoad: true, + xtype: 'update', + interval: 5 * 1000, + autoStart: true, + storeid: 'pve-ceph-fs', + proxy: { + type: 'proxmox', + url: `/api2/json/nodes/${view.nodename}/ceph/fs`, + }, + model: 'pve-ceph-fs', + }); + view.setStore(Ext.create('Proxmox.data.DiffStore', { + rstore: view.rstore, + sorters: { + property: 'name', + direction: 'ASC', + }, + })); + // manages the "install ceph?" overlay + PVE.Utils.monitor_ceph_installed(view, view.rstore, view.nodename, true); + view.on('destroy', () => view.rstore.stopUpdate()); + }, + + onCreate: function() { + let view = this.getView(); + view.rstore.stopUpdate(); + Ext.create('PVE.CephCreateFS', { + autoShow: true, + nodename: view.nodename, + listeners: { + destroy: () => view.rstore.startUpdate(), + }, + }); + }, + }, + tbar: [ + { + text: gettext('Create CephFS'), + reference: 'createButton', + handler: 'onCreate', + bind: { + disabled: '{!canCreateFS}', + }, + }, + ], + columns: [ + { + header: gettext('Name'), + flex: 1, + dataIndex: 'name', + }, + { + header: 'Data Pool', + flex: 1, + dataIndex: 'data_pool', + }, + { + header: 'Metadata Pool', + flex: 1, + dataIndex: 'metadata_pool', + }, + ], + cbind: { + nodename: '{nodename}', + }, + }, + { + xtype: 'pveNodeCephMDSList', + title: gettext('Metadata Servers'), + stateId: 'grid-ceph-mds', + type: 'mds', + storeLoadCallback: function(store, records, success) { + var vm = this.getViewModel(); + if (!success || !records) { + vm.set('mdsCount', 0); + return; + } + let count = 0; + for (const mds of records) { + if (mds.data.state === 'up:standby') { + count++; + } + } + vm.set('mdsCount', count); + }, + cbind: { + nodename: '{nodename}', + }, + }, + ], +}, function() { + Ext.define('pve-ceph-fs', { + extend: 'Ext.data.Model', + fields: ['name', 'data_pool', 'metadata_pool'], + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/localhost/ceph/fs", + }, + idProperty: 'name', + }); +}); +Ext.define('PVE.ceph.Log', { + extend: 'Proxmox.panel.LogView', + xtype: 'cephLogView', + + nodename: undefined, + + failCallback: function(response) { + var me = this; + var msg = response.htmlStatus; + var windowShow = PVE.Utils.showCephInstallOrMask(me, msg, me.nodename, + function(win) { + me.mon(win, 'cephInstallWindowClosed', function() { + me.loadTask.delay(200); + }); + }, + ); + if (!windowShow) { + Proxmox.Utils.setErrorMask(me, msg); + } + }, +}); +Ext.define('PVE.node.CephMonMgrList', { + extend: 'Ext.container.Container', + xtype: 'pveNodeCephMonMgr', + + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'chapter_pveceph', + + defaults: { + border: false, + onlineHelp: 'chapter_pveceph', + flex: 1, + }, + + layout: { + type: 'vbox', + align: 'stretch', + }, + + items: [ + { + xtype: 'pveNodeCephServiceList', + cbind: { pveSelNode: '{pveSelNode}' }, + type: 'mon', + additionalColumns: [ + { + header: gettext('Quorum'), + width: 70, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'quorum', + }, + ], + stateId: 'grid-ceph-monitor', + showCephInstallMask: true, + title: gettext('Monitor'), + }, + { + xtype: 'pveNodeCephServiceList', + type: 'mgr', + stateId: 'grid-ceph-manager', + cbind: { pveSelNode: '{pveSelNode}' }, + title: gettext('Manager'), + }, + ], +}); +Ext.define('PVE.CephCreateOsd', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCephCreateOsd', + + subject: 'Ceph OSD', + + showProgress: true, + + onlineHelp: 'pve_ceph_osds', + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + Proxmox.Utils.API2Request({ + url: `/nodes/${me.nodename}/ceph/crush`, + method: 'GET', + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: function({ result: { data } }) { + let classes = [...new Set( + Array.from( + data.matchAll(/^device\s[0-9]*\sosd\.[0-9]*\sclass\s(.*)$/gim), + m => m[1], + ).filter(v => !['hdd', 'ssd', 'nvme'].includes(v)), + )].map(v => [v, v]); + + if (classes.length) { + let kvField = me.down('field[name=crush-device-class]'); + kvField.setComboItems([...kvField.comboItems, ...classes]); + } + }, + }); + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/ceph/osd", + method: 'POST', + items: [ + { + xtype: 'inputpanel', + onGetValues: function(values) { + Object.keys(values || {}).forEach(function(name) { + if (values[name] === '') { + delete values[name]; + } + }); + + return values; + }, + column1: [ + { + xtype: 'pmxDiskSelector', + name: 'dev', + nodename: me.nodename, + diskType: 'unused', + includePartitions: true, + fieldLabel: gettext('Disk'), + allowBlank: false, + }, + ], + column2: [ + { + xtype: 'pmxDiskSelector', + name: 'db_dev', + nodename: me.nodename, + diskType: 'journal_disks', + includePartitions: true, + fieldLabel: gettext('DB Disk'), + value: '', + autoSelect: false, + allowBlank: true, + emptyText: 'use OSD disk', + listeners: { + change: function(field, val) { + me.down('field[name=db_dev_size]').setDisabled(!val); + }, + }, + }, + { + xtype: 'numberfield', + name: 'db_dev_size', + fieldLabel: gettext('DB size') + ' (GiB)', + minValue: 1, + maxValue: 128*1024, + decimalPrecision: 2, + allowBlank: true, + disabled: true, + emptyText: gettext('Automatic'), + }, + ], + advancedColumn1: [ + { + xtype: 'proxmoxcheckbox', + name: 'encrypted', + fieldLabel: gettext('Encrypt OSD'), + }, + { + xtype: 'proxmoxKVComboBox', + comboItems: [ + ['hdd', 'HDD'], + ['ssd', 'SSD'], + ['nvme', 'NVMe'], + ], + name: 'crush-device-class', + nodename: me.nodename, + fieldLabel: gettext('Device Class'), + value: '', + autoSelect: false, + allowBlank: true, + editable: true, + emptyText: 'auto detect', + deleteEmpty: !me.isCreate, + }, + ], + advancedColumn2: [ + { + xtype: 'pmxDiskSelector', + name: 'wal_dev', + nodename: me.nodename, + diskType: 'journal_disks', + includePartitions: true, + fieldLabel: gettext('WAL Disk'), + value: '', + autoSelect: false, + allowBlank: true, + emptyText: 'use OSD/DB disk', + listeners: { + change: function(field, val) { + me.down('field[name=wal_dev_size]').setDisabled(!val); + }, + }, + }, + { + xtype: 'numberfield', + name: 'wal_dev_size', + fieldLabel: gettext('WAL size') + ' (GiB)', + minValue: 0.5, + maxValue: 128*1024, + decimalPrecision: 2, + allowBlank: true, + disabled: true, + emptyText: gettext('Automatic'), + }, + ], + }, + { + xtype: 'displayfield', + padding: '5 0 0 0', + userCls: 'pmx-hint', + value: 'Note: Ceph is not compatible with disks backed by a hardware ' + + 'RAID controller. For details see ' + + 'the reference documentation.', + }, + ], + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.CephRemoveOsd', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveCephRemoveOsd'], + + isRemove: true, + + showProgress: true, + method: 'DELETE', + items: [ + { + xtype: 'proxmoxcheckbox', + name: 'cleanup', + checked: true, + labelWidth: 130, + fieldLabel: gettext('Cleanup Disks'), + }, + { + xtype: 'displayfield', + name: 'osd-flag-hint', + userCls: 'pmx-hint', + value: gettext('Global flags limiting the self healing of Ceph are enabled.'), + hidden: true, + }, + { + xtype: 'displayfield', + name: 'degraded-objects-hint', + userCls: 'pmx-hint', + value: gettext('Objects are degraded. Consider waiting until the cluster is healthy.'), + hidden: true, + }, + ], + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + if (me.osdid === undefined || me.osdid < 0) { + throw "no osdid specified"; + } + + me.isCreate = true; + + me.title = gettext('Destroy') + ': Ceph OSD osd.' + me.osdid.toString(); + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/ceph/osd/" + me.osdid.toString(), + }); + + me.callParent(); + + if (me.warnings.flags) { + me.down('field[name=osd-flag-hint]').setHidden(false); + } + if (me.warnings.degraded) { + me.down('field[name=degraded-objects-hint]').setHidden(false); + } + }, +}); + +Ext.define('PVE.CephSetFlags', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCephSetFlags', + + showProgress: true, + + width: 720, + layout: 'fit', + + onlineHelp: 'pve_ceph_osds', + isCreate: true, + title: Ext.String.format(gettext('Manage {0}'), 'Global OSD Flags'), + submitText: gettext('Apply'), + + items: [ + { + xtype: 'inputpanel', + onGetValues: function(values) { + let me = this; + let val = {}; + me.down('#flaggrid').getStore().each((rec) => { + val[rec.data.name] = rec.data.value ? 1 : 0; + }); + + return val; + }, + items: [ + { + xtype: 'grid', + itemId: 'flaggrid', + store: { + listeners: { + update: function() { + this.commitChanges(); + }, + }, + }, + + columns: [ + { + text: gettext('Enable'), + xtype: 'checkcolumn', + width: 75, + dataIndex: 'value', + }, + { + text: 'Name', + dataIndex: 'name', + }, + { + text: 'Description', + flex: 1, + dataIndex: 'description', + }, + ], + }, + ], + }, + ], + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.applyIf(me, { + url: "/cluster/ceph/flags", + method: 'PUT', + }); + + me.callParent(); + + let grid = me.down('#flaggrid'); + me.load({ + success: function(response, options) { + let data = response.result.data; + grid.getStore().setData(data); + // re-align after store load, else the window is not centered + me.alignTo(Ext.getBody(), 'c-c'); + }, + }); + }, +}); + +Ext.define('PVE.node.CephOsdTree', { + extend: 'Ext.tree.Panel', + alias: ['widget.pveNodeCephOsdTree'], + onlineHelp: 'chapter_pveceph', + + viewModel: { + data: { + nodename: '', + flags: [], + maxversion: '0', + mixedversions: false, + versions: {}, + isOsd: false, + downOsd: false, + upOsd: false, + inOsd: false, + outOsd: false, + osdid: '', + osdhost: '', + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + reload: function() { + let me = this; + let view = me.getView(); + let vm = me.getViewModel(); + let nodename = vm.get('nodename'); + let sm = view.getSelectionModel(); + Proxmox.Utils.API2Request({ + url: "/nodes/" + nodename + "/ceph/osd", + waitMsgTarget: view, + method: 'GET', + failure: function(response, opts) { + let msg = response.htmlStatus; + PVE.Utils.showCephInstallOrMask(view, msg, nodename, win => + view.mon(win, 'cephInstallWindowClosed', () => { me.reload(); }), + ); + }, + success: function(response, opts) { + let data = response.result.data; + let selected = view.getSelection(); + let name; + if (selected.length) { + name = selected[0].data.name; + } + data.versions = data.versions || {}; + vm.set('versions', data.versions); + // extract max version + let maxversion = "0"; + let mixedversions = false; + let traverse; + traverse = function(node, fn) { + fn(node); + if (Array.isArray(node.children)) { + node.children.forEach(c => { traverse(c, fn); }); + } + }; + traverse(data.root, node => { + // compatibility for old api call + if (node.type === 'host' && !node.version) { + node.version = data.versions[node.name]; + } + + if (node.version === undefined) { + return; + } + + if (PVE.Utils.compare_ceph_versions(node.version, maxversion) !== 0 && maxversion !== "0") { + mixedversions = true; + } + + if (PVE.Utils.compare_ceph_versions(node.version, maxversion) > 0) { + maxversion = node.version; + } + }); + vm.set('maxversion', maxversion); + vm.set('mixedversions', mixedversions); + sm.deselectAll(); + view.setRootNode(data.root); + view.expandAll(); + if (name) { + let node = view.getRootNode().findChild('name', name, true); + if (node) { + view.setSelection([node]); + } + } + + let flags = data.flags.split(','); + vm.set('flags', flags); + }, + }); + }, + + osd_cmd: function(comp) { + let me = this; + let vm = this.getViewModel(); + let cmd = comp.cmd; + let params = comp.params || {}; + let osdid = vm.get('osdid'); + + let doRequest = function() { + let targetnode = vm.get('osdhost'); + // cmds not node specific and need to work if the OSD node is down + if (['in', 'out'].includes(cmd)) { + targetnode = vm.get('nodename'); + } + Proxmox.Utils.API2Request({ + url: `/nodes/${targetnode}/ceph/osd/${osdid}/${cmd}`, + waitMsgTarget: me.getView(), + method: 'POST', + params: params, + success: () => { me.reload(); }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }; + + if (cmd === 'scrub') { + Ext.MessageBox.defaultButton = params.deep === 1 ? 2 : 1; + Ext.Msg.show({ + title: gettext('Confirm'), + icon: params.deep === 1 ? Ext.Msg.WARNING : Ext.Msg.QUESTION, + msg: params.deep !== 1 + ? Ext.String.format(gettext("Scrub OSD.{0}"), osdid) + : Ext.String.format(gettext("Deep Scrub OSD.{0}"), osdid) + + "
Caution: This can reduce performance while it is running.", + buttons: Ext.Msg.YESNO, + callback: function(btn) { + if (btn !== 'yes') { + return; + } + doRequest(); + }, + }); + } else { + doRequest(); + } + }, + + create_osd: function() { + let me = this; + let vm = this.getViewModel(); + Ext.create('PVE.CephCreateOsd', { + nodename: vm.get('nodename'), + taskDone: () => { me.reload(); }, + }).show(); + }, + + destroy_osd: async function() { + let me = this; + let vm = this.getViewModel(); + + let warnings = { + flags: false, + degraded: false, + }; + + let flagsPromise = Proxmox.Async.api2({ + url: `/cluster/ceph/flags`, + method: 'GET', + }); + + let statusPromise = Proxmox.Async.api2({ + url: `/cluster/ceph/status`, + method: 'GET', + }); + + me.getView().mask(gettext('Loading...')); + + try { + let result = await Promise.all([flagsPromise, statusPromise]); + + let flagsData = result[0].result.data; + let statusData = result[1].result.data; + + let flags = Array.from( + flagsData.filter(v => v.value), + v => v.name, + ).filter(v => ['norebalance', 'norecover', 'noout'].includes(v)); + + if (flags.length) { + warnings.flags = true; + } + if (Object.keys(statusData.pgmap).includes('degraded_objects')) { + warnings.degraded = true; + } + } catch (error) { + Ext.Msg.alert(gettext('Error'), error.htmlStatus); + me.getView().unmask(); + return; + } + + me.getView().unmask(); + Ext.create('PVE.CephRemoveOsd', { + nodename: vm.get('osdhost'), + osdid: vm.get('osdid'), + warnings: warnings, + taskDone: () => { me.reload(); }, + autoShow: true, + }); + }, + + set_flags: function() { + let me = this; + let vm = this.getViewModel(); + Ext.create('PVE.CephSetFlags', { + nodename: vm.get('nodename'), + taskDone: () => { me.reload(); }, + }).show(); + }, + + service_cmd: function(comp) { + let me = this; + let vm = this.getViewModel(); + let cmd = comp.cmd || comp; + + let doRequest = function() { + Proxmox.Utils.API2Request({ + url: `/nodes/${vm.get('osdhost')}/ceph/${cmd}`, + params: { service: "osd." + vm.get('osdid') }, + waitMsgTarget: me.getView(), + method: 'POST', + success: function(response, options) { + let upid = response.result.data; + let win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + taskDone: () => { me.reload(); }, + }); + win.show(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }; + + if (cmd === "stop") { + Proxmox.Utils.API2Request({ + url: `/nodes/${vm.get('osdhost')}/ceph/cmd-safety`, + params: { + service: 'osd', + id: vm.get('osdid'), + action: 'stop', + }, + waitMsgTarget: me.getView(), + method: 'GET', + success: function({ result: { data } }) { + if (!data.safe) { + Ext.Msg.show({ + title: gettext('Warning'), + message: data.status, + icon: Ext.Msg.WARNING, + buttons: Ext.Msg.OKCANCEL, + buttonText: { ok: gettext('Stop OSD') }, + fn: function(selection) { + if (selection === 'ok') { + doRequest(); + } + }, + }); + } else { + doRequest(); + } + }, + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + } else { + doRequest(); + } + }, + + run_details: function(view, rec) { + if (rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0) { + this.details(); + } + }, + + details: function() { + let vm = this.getViewModel(); + Ext.create('PVE.CephOsdDetails', { + nodename: vm.get('osdhost'), + osdid: vm.get('osdid'), + }).show(); + }, + + set_selection_status: function(tp, selection) { + if (selection.length < 1) { + return; + } + let rec = selection[0]; + let vm = this.getViewModel(); + + let isOsd = rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0; + + vm.set('isOsd', isOsd); + vm.set('downOsd', isOsd && rec.data.status === 'down'); + vm.set('upOsd', isOsd && rec.data.status !== 'down'); + vm.set('inOsd', isOsd && rec.data.in); + vm.set('outOsd', isOsd && !rec.data.in); + vm.set('osdid', isOsd ? rec.data.id : undefined); + vm.set('osdhost', isOsd ? rec.data.host : undefined); + }, + + render_status: function(value, metaData, rec) { + if (!value) { + return value; + } + let inout = rec.data.in ? 'in' : 'out'; + let updownicon = value === 'up' ? 'good fa-arrow-circle-up' + : 'critical fa-arrow-circle-down'; + + let inouticon = rec.data.in ? 'good fa-circle' + : 'warning fa-circle-o'; + + let text = value + ' / ' + + inout + ' '; + + return text; + }, + + render_wal: function(value, metaData, rec) { + if (!value && + rec.data.osdtype === 'bluestore' && + rec.data.type === 'osd') { + return 'N/A'; + } + return value; + }, + + render_version: function(value, metadata, rec) { + let vm = this.getViewModel(); + let versions = vm.get('versions'); + let icon = ""; + let version = value || ""; + let maxversion = vm.get('maxversion'); + if (value && PVE.Utils.compare_ceph_versions(value, maxversion) !== 0) { + let host_version = rec.parentNode?.data?.version || versions[rec.data.host] || ""; + if (rec.data.type === 'host' || PVE.Utils.compare_ceph_versions(host_version, maxversion) !== 0) { + icon = PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE'); + } else { + icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD'); + } + } else if (value && vm.get('mixedversions')) { + icon = PVE.Utils.get_ceph_icon_html('HEALTH_OK'); + } + + return icon + version; + }, + + render_osd_val: function(value, metaData, rec) { + return rec.data.type === 'osd' ? value : ''; + }, + render_osd_weight: function(value, metaData, rec) { + if (rec.data.type !== 'osd') { + return ''; + } + return Ext.util.Format.number(value, '0.00###'); + }, + + render_osd_latency: function(value, metaData, rec) { + if (rec.data.type !== 'osd') { + return ''; + } + let commit_ms = rec.data.commit_latency_ms, + apply_ms = rec.data.apply_latency_ms; + return apply_ms + ' / ' + commit_ms; + }, + + render_osd_size: function(value, metaData, rec) { + return this.render_osd_val(Proxmox.Utils.render_size(value), metaData, rec); + }, + + control: { + '#': { + selectionchange: 'set_selection_status', + }, + }, + + init: function(view) { + let me = this; + let vm = this.getViewModel(); + + if (!view.pveSelNode.data.node) { + throw "no node name specified"; + } + + vm.set('nodename', view.pveSelNode.data.node); + + me.callParent(); + me.reload(); + }, + }, + + stateful: true, + stateId: 'grid-ceph-osd', + rootVisible: false, + useArrows: true, + listeners: { + itemdblclick: 'run_details', + }, + + columns: [ + { + xtype: 'treecolumn', + text: 'Name', + dataIndex: 'name', + width: 150, + }, + { + text: 'Type', + dataIndex: 'type', + hidden: true, + align: 'right', + width: 75, + }, + { + text: gettext("Class"), + dataIndex: 'device_class', + align: 'right', + width: 75, + }, + { + text: "OSD Type", + dataIndex: 'osdtype', + align: 'right', + width: 100, + }, + { + text: "Bluestore Device", + dataIndex: 'blfsdev', + align: 'right', + width: 75, + hidden: true, + }, + { + text: "DB Device", + dataIndex: 'dbdev', + align: 'right', + width: 75, + hidden: true, + }, + { + text: "WAL Device", + dataIndex: 'waldev', + align: 'right', + renderer: 'render_wal', + width: 75, + hidden: true, + }, + { + text: 'Status', + dataIndex: 'status', + align: 'right', + renderer: 'render_status', + width: 120, + }, + { + text: gettext('Version'), + dataIndex: 'version', + align: 'right', + renderer: 'render_version', + }, + { + text: 'weight', + dataIndex: 'crush_weight', + align: 'right', + renderer: 'render_osd_weight', + width: 90, + }, + { + text: 'reweight', + dataIndex: 'reweight', + align: 'right', + renderer: 'render_osd_weight', + width: 90, + }, + { + text: gettext('Used') + ' (%)', + dataIndex: 'percent_used', + align: 'right', + renderer: function(value, metaData, rec) { + if (rec.data.type !== 'osd') { + return ''; + } + return Ext.util.Format.number(value, '0.00'); + }, + width: 100, + }, + { + text: gettext('Total'), + dataIndex: 'total_space', + align: 'right', + renderer: 'render_osd_size', + width: 100, + }, + { + text: 'Apply/Commit
Latency (ms)', + dataIndex: 'apply_latency_ms', + align: 'right', + renderer: 'render_osd_latency', + width: 120, + }, + { + text: 'PGs', + dataIndex: 'pgs', + align: 'right', + renderer: 'render_osd_val', + width: 90, + }, + ], + + + tbar: { + items: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: 'reload', + }, + '-', + { + text: gettext('Create') + ': OSD', + handler: 'create_osd', + }, + { + text: Ext.String.format(gettext('Manage {0}'), 'Global Flags'), + handler: 'set_flags', + }, + '->', + { + xtype: 'tbtext', + data: { + osd: undefined, + }, + bind: { + data: { + osd: "{osdid}", + }, + }, + tpl: [ + '', + 'osd.{osd}:', + '', + gettext('No OSD selected'), + '', + ], + }, + { + text: gettext('Details'), + iconCls: 'fa fa-info-circle', + disabled: true, + bind: { + disabled: '{!isOsd}', + }, + handler: 'details', + }, + { + text: gettext('Start'), + iconCls: 'fa fa-play', + disabled: true, + bind: { + disabled: '{!downOsd}', + }, + cmd: 'start', + handler: 'service_cmd', + }, + { + text: gettext('Stop'), + iconCls: 'fa fa-stop', + disabled: true, + bind: { + disabled: '{!upOsd}', + }, + cmd: 'stop', + handler: 'service_cmd', + }, + { + text: gettext('Restart'), + iconCls: 'fa fa-refresh', + disabled: true, + bind: { + disabled: '{!upOsd}', + }, + cmd: 'restart', + handler: 'service_cmd', + }, + '-', + { + text: 'Out', + iconCls: 'fa fa-circle-o', + disabled: true, + bind: { + disabled: '{!inOsd}', + }, + cmd: 'out', + handler: 'osd_cmd', + }, + { + text: 'In', + iconCls: 'fa fa-circle', + disabled: true, + bind: { + disabled: '{!outOsd}', + }, + cmd: 'in', + handler: 'osd_cmd', + }, + '-', + { + text: gettext('More'), + iconCls: 'fa fa-bars', + disabled: true, + bind: { + disabled: '{!isOsd}', + }, + menu: [ + { + text: gettext('Scrub'), + iconCls: 'fa fa-shower', + cmd: 'scrub', + handler: 'osd_cmd', + }, + { + text: gettext('Deep Scrub'), + iconCls: 'fa fa-bath', + cmd: 'scrub', + params: { + deep: 1, + }, + handler: 'osd_cmd', + }, + { + text: gettext('Destroy'), + itemId: 'remove', + iconCls: 'fa fa-fw fa-trash-o', + bind: { + disabled: '{!downOsd}', + }, + handler: 'destroy_osd', + }, + ], + }, + ], + }, + + fields: [ + 'name', 'type', 'status', 'host', 'in', 'id', + { type: 'number', name: 'reweight' }, + { type: 'number', name: 'percent_used' }, + { type: 'integer', name: 'bytes_used' }, + { type: 'integer', name: 'total_space' }, + { type: 'integer', name: 'apply_latency_ms' }, + { type: 'integer', name: 'commit_latency_ms' }, + { type: 'string', name: 'device_class' }, + { type: 'string', name: 'osdtype' }, + { type: 'string', name: 'blfsdev' }, + { type: 'string', name: 'dbdev' }, + { type: 'string', name: 'waldev' }, + { + type: 'string', name: 'version', calculate: function(data) { + return PVE.Utils.parse_ceph_version(data); + }, +}, + { + type: 'string', name: 'iconCls', calculate: function(data) { + let iconMap = { + host: 'fa-building', + osd: 'fa-hdd-o', + root: 'fa-server', + }; + return `fa x-fa-tree ${iconMap[data.type] ?? 'fa-folder-o'}`; + }, +}, + { type: 'number', name: 'crush_weight' }, + ], +}); +Ext.define('pve-osd-details-devices', { + extend: 'Ext.data.Model', + fields: ['device', 'type', 'physical_device', 'size', 'support_discard', 'dev_node'], + idProperty: 'device', +}); + +Ext.define('PVE.CephOsdDetails', { + extend: 'Ext.window.Window', + alias: ['widget.pveCephOsdDetails'], + + mixins: ['Proxmox.Mixin.CBind'], + + cbindData: function() { + let me = this; + me.baseUrl = `/nodes/${me.nodename}/ceph/osd/${me.osdid}`; + return { + title: `${gettext('Details')}: OSD ${me.osdid}`, + }; + }, + + viewModel: { + data: { + device: '', + }, + }, + + modal: true, + width: 650, + minHeight: 250, + resizable: true, + cbind: { + title: '{title}', + }, + + layout: { + type: 'vbox', + align: 'stretch', + }, + defaults: { + layout: 'fit', + border: false, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + reload: function() { + let view = this.getView(); + + Proxmox.Utils.API2Request({ + url: `${view.baseUrl}/metadata`, + waitMsgTarget: view.lookup('detailsTabs'), + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(view.lookup('detailsTabs'), response.htmlStatus); + }, + success: function(response, opts) { + let d = response.result.data; + let osdData = Object.keys(d.osd).sort().map(x => ({ key: x, value: d.osd[x] })); + view.osdStore.loadData(osdData); + let devices = view.lookup('devices'); + let deviceStore = devices.getStore(); + deviceStore.loadData(d.devices); + + view.lookup('osdGeneral').rstore.fireEvent('load', view.osdStore, osdData, true); + view.lookup('osdNetwork').rstore.fireEvent('load', view.osdStore, osdData, true); + + // select 'block' device automatically on first load + if (devices.getSelection().length === 0) { + devices.setSelection(deviceStore.findRecord('device', 'block')); + } + }, + }); + }, + + showDevInfo: function(grid, selected) { + let view = this.getView(); + if (selected[0]) { + let device = selected[0].data.device; + this.getViewModel().set('device', device); + + let detailStore = view.lookup('volumeDetails'); + detailStore.rstore.getProxy().setUrl(`api2/json${view.baseUrl}/lv-info`); + detailStore.rstore.getProxy().setExtraParams({ 'type': device }); + detailStore.setLoading(); + detailStore.rstore.load({ callback: () => detailStore.setLoading(false) }); + } + }, + + init: function() { + this.reload(); + }, + + control: { + 'grid[reference=devices]': { + selectionchange: 'showDevInfo', + }, + }, + }, + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: 'reload', + }, + ], + initComponent: function() { + let me = this; + + me.osdStore = Ext.create('Proxmox.data.ObjectStore'); + + Ext.applyIf(me, { + items: [ + { + xtype: 'tabpanel', + reference: 'detailsTabs', + items: [ + { + xtype: 'proxmoxObjectGrid', + reference: 'osdGeneral', + tooltip: gettext('Various information about the OSD'), + rstore: me.osdStore, + title: gettext('General'), + viewConfig: { + enableTextSelection: true, + }, + gridRows: [ + { + xtype: 'text', + name: 'version', + text: gettext('Version'), + }, + { + xtype: 'text', + name: 'hostname', + text: gettext('Hostname'), + }, + { + xtype: 'text', + name: 'osd_data', + text: gettext('OSD data path'), + }, + { + xtype: 'text', + name: 'osd_objectstore', + text: gettext('OSD object store'), + }, + { + xtype: 'text', + name: 'mem_usage', + text: gettext('Memory usage'), + renderer: Proxmox.Utils.render_size, + }, + { + xtype: 'text', + name: 'pid', + text: `${gettext('Process ID')} (PID)`, + }, + ], + }, + { + xtype: 'proxmoxObjectGrid', + reference: 'osdNetwork', + tooltip: gettext('Addresses and ports used by the OSD service'), + rstore: me.osdStore, + title: gettext('Network'), + viewConfig: { + enableTextSelection: true, + }, + gridRows: [ + { + xtype: 'text', + name: 'front_addr', + text: `${gettext('Front Address')}
(Client & Monitor)`, + renderer: PVE.Utils.render_ceph_osd_addr, + }, + { + xtype: 'text', + name: 'hb_front_addr', + text: gettext('Heartbeat Front Address'), + renderer: PVE.Utils.render_ceph_osd_addr, + }, + { + xtype: 'text', + name: 'back_addr', + text: `${gettext('Back Address')}
(OSD)`, + renderer: PVE.Utils.render_ceph_osd_addr, + }, + { + xtype: 'text', + name: 'hb_back_addr', + text: gettext('Heartbeat Back Address'), + renderer: PVE.Utils.render_ceph_osd_addr, + }, + ], + }, + { + xtype: 'panel', + title: 'Devices', + tooltip: gettext('Physical devices used by the OSD'), + items: [ + { + xtype: 'grid', + border: false, + reference: 'devices', + store: { + model: 'pve-osd-details-devices', + }, + columns: { + items: [ + { text: gettext('Device'), dataIndex: 'device' }, + { text: gettext('Type'), dataIndex: 'type' }, + { + text: gettext('Physical Device'), + dataIndex: 'physical_device', + }, + { + text: gettext('Size'), + dataIndex: 'size', + renderer: Proxmox.Utils.render_size, + }, + { + text: 'Discard', + dataIndex: 'support_discard', + hidden: true, + }, + { + text: gettext('Device node'), + dataIndex: 'dev_node', + hidden: true, + }, + ], + defaults: { + tdCls: 'pointer', + flex: 1, + }, + }, + }, + { + xtype: 'proxmoxObjectGrid', + reference: 'volumeDetails', + maskOnLoad: true, + viewConfig: { + enableTextSelection: true, + }, + bind: { + title: Ext.String.format( + gettext('Volume Details for {0}'), + '{device}', + ), + }, + rows: { + creation_time: { + header: gettext('Creation time'), + }, + lv_name: { + header: gettext('LV Name'), + }, + lv_path: { + header: gettext('LV Path'), + }, + lv_uuid: { + header: gettext('LV UUID'), + }, + vg_name: { + header: gettext('VG Name'), + }, + }, + url: 'nodes/', //placeholder will be set when device is selected + }, + ], + }, + ], + }, + ], + }); + + me.callParent(); + }, +}); +Ext.define('PVE.CephPoolInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveCephPoolInputPanel', + mixins: ['Proxmox.Mixin.CBind'], + + showProgress: true, + onlineHelp: 'pve_ceph_pools', + + subject: 'Ceph Pool', + column1: [ + { + xtype: 'pmxDisplayEditField', + fieldLabel: gettext('Name'), + cbind: { + editable: '{isCreate}', + value: '{pool_name}', + }, + name: 'name', + allowBlank: false, + }, + { + xtype: 'pmxDisplayEditField', + cbind: { + editable: '{!isErasure}', + }, + fieldLabel: gettext('Size'), + name: 'size', + editConfig: { + xtype: 'proxmoxintegerfield', + value: 3, + minValue: 2, + maxValue: 7, + allowBlank: false, + listeners: { + change: function(field, val) { + let size = Math.round(val / 2); + if (size > 1) { + field.up('inputpanel').down('field[name=min_size]').setValue(size); + } + }, + }, + }, + + }, + ], + column2: [ + { + xtype: 'proxmoxKVComboBox', + fieldLabel: 'PG Autoscale Mode', + name: 'pg_autoscale_mode', + comboItems: [ + ['warn', 'warn'], + ['on', 'on'], + ['off', 'off'], + ], + value: 'on', // FIXME: check ceph version and only default to on on octopus and newer + allowBlank: false, + autoSelect: false, + labelWidth: 140, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Add as Storage'), + cbind: { + value: '{isCreate}', + hidden: '{!isCreate}', + }, + name: 'add_storages', + labelWidth: 140, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Add the new pool to the cluster storage configuration.'), + }, + }, + ], + advancedColumn1: [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Min. Size'), + name: 'min_size', + value: 2, + cbind: { + minValue: (get) => get('isCreate') ? 2 : 1, + }, + maxValue: 7, + allowBlank: false, + listeners: { + change: function(field, minSize) { + let panel = field.up('inputpanel'); + let size = panel.down('field[name=size]').getValue(); + + let showWarning = minSize < (size / 2) && minSize !== size; + + let fieldLabel = gettext('Min. Size'); + if (showWarning) { + fieldLabel = gettext('Min. Size') + ' '; + } + panel.down('field[name=min_size-warning]').setHidden(!showWarning); + field.setFieldLabel(fieldLabel); + }, + }, + }, + { + xtype: 'displayfield', + name: 'min_size-warning', + userCls: 'pmx-hint', + value: gettext('min_size < size/2 can lead to data loss, incomplete PGs or unfound objects.'), + hidden: true, + }, + { + xtype: 'pmxDisplayEditField', + cbind: { + editable: '{!isErasure}', + nodename: '{nodename}', + isCreate: '{isCreate}', + }, + fieldLabel: 'Crush Rule', // do not localize + name: 'crush_rule', + editConfig: { + xtype: 'pveCephRuleSelector', + allowBlank: false, + }, + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: '# of PGs', + name: 'pg_num', + value: 128, + minValue: 1, + maxValue: 32768, + allowBlank: false, + emptyText: 128, + }, + ], + advancedColumn2: [ + { + xtype: 'numberfield', + fieldLabel: gettext('Target Ratio'), + name: 'target_size_ratio', + minValue: 0, + decimalPrecision: 3, + allowBlank: true, + emptyText: '0.0', + autoEl: { + tag: 'div', + 'data-qtip': gettext('The ratio of storage amount this pool will consume compared to other pools with ratios. Used for auto-scaling.'), + }, + }, + { + xtype: 'pveSizeField', + name: 'target_size', + fieldLabel: gettext('Target Size'), + unit: 'GiB', + minValue: 0, + allowBlank: true, + allowZero: true, + emptyText: '0', + emptyValue: 0, + autoEl: { + tag: 'div', + 'data-qtip': gettext('The amount of data eventually stored in this pool. Used for auto-scaling.'), + }, + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: Ext.String.format(gettext('{0} takes precedence.'), gettext('Target Ratio')), // FIXME: tooltip? + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: 'Min. # of PGs', + name: 'pg_num_min', + minValue: 0, + allowBlank: true, + emptyText: '0', + }, + ], + + onGetValues: function(values) { + Object.keys(values || {}).forEach(function(name) { + if (values[name] === '') { + delete values[name]; + } + }); + + return values; + }, +}); + +Ext.define('PVE.Ceph.PoolEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveCephPoolEdit', + mixins: ['Proxmox.Mixin.CBind'], + + cbindData: { + pool_name: '', + isCreate: (cfg) => !cfg.pool_name, + }, + + cbind: { + autoLoad: get => !get('isCreate'), + url: get => get('isCreate') + ? `/nodes/${get('nodename')}/ceph/pool` + : `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}`, + loadUrl: get => `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}/status`, + method: get => get('isCreate') ? 'POST' : 'PUT', + }, + + showProgress: true, + + subject: gettext('Ceph Pool'), + + items: [{ + xtype: 'pveCephPoolInputPanel', + cbind: { + nodename: '{nodename}', + pool_name: '{pool_name}', + isErasure: '{isErasure}', + isCreate: '{isCreate}', + }, + }], +}); + +Ext.define('PVE.node.Ceph.PoolList', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveNodeCephPoolList', + + onlineHelp: 'chapter_pveceph', + + stateful: true, + stateId: 'grid-ceph-pools', + bufferedRenderer: false, + + features: [{ ftype: 'summary' }], + + columns: [ + { + text: gettext('Name'), + minWidth: 120, + flex: 2, + sortable: true, + dataIndex: 'pool_name', + }, + { + text: gettext('Type'), + minWidth: 100, + flex: 1, + dataIndex: 'type', + hidden: true, + }, + { + text: gettext('Size') + '/min', + minWidth: 100, + flex: 1, + align: 'right', + renderer: (v, meta, rec) => `${v}/${rec.data.min_size}`, + dataIndex: 'size', + }, + { + text: '# of Placement Groups', + flex: 1, + minWidth: 100, + align: 'right', + dataIndex: 'pg_num', + }, + { + text: gettext('Optimal # of PGs'), + flex: 1, + minWidth: 100, + align: 'right', + dataIndex: 'pg_num_final', + renderer: function(value, metaData) { + if (!value) { + value = ' n/a'; + metaData.tdAttr = 'data-qtip="Needs pg_autoscaler module enabled."'; + } + return value; + }, + }, + { + text: gettext('Min. # of PGs'), + flex: 1, + minWidth: 100, + align: 'right', + dataIndex: 'pg_num_min', + hidden: true, + }, + { + text: gettext('Target Ratio'), + flex: 1, + minWidth: 100, + align: 'right', + dataIndex: 'target_size_ratio', + renderer: Ext.util.Format.numberRenderer('0.0000'), + hidden: true, + }, + { + text: gettext('Target Size'), + flex: 1, + minWidth: 100, + align: 'right', + dataIndex: 'target_size', + hidden: true, + renderer: function(v, metaData, rec) { + let value = Proxmox.Utils.render_size(v); + if (rec.data.target_size_ratio > 0) { + value = ' ' + value; + metaData.tdAttr = 'data-qtip="Target Size Ratio takes precedence over Target Size."'; + } + return value; + }, + }, + { + text: gettext('Autoscale Mode'), + flex: 1, + minWidth: 100, + align: 'right', + dataIndex: 'pg_autoscale_mode', + }, + { + text: 'CRUSH Rule (ID)', + flex: 1, + align: 'right', + minWidth: 150, + renderer: (v, meta, rec) => `${v} (${rec.data.crush_rule})`, + dataIndex: 'crush_rule_name', + }, + { + text: gettext('Used') + ' (%)', + flex: 1, + minWidth: 150, + sortable: true, + align: 'right', + dataIndex: 'bytes_used', + summaryType: 'sum', + summaryRenderer: Proxmox.Utils.render_size, + renderer: function(v, meta, rec) { + let percentage = Ext.util.Format.percent(rec.data.percent_used, '0.00'); + let used = Proxmox.Utils.render_size(v); + return `${used} (${percentage})`; + }, + }, + ], + 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('Proxmox.data.UpdateStore', { + interval: 3000, + storeid: 'ceph-pool-list' + nodename, + model: 'ceph-pool-list', + proxy: { + type: 'proxmox', + url: `/api2/json/nodes/${nodename}/ceph/pool`, + }, + }); + let store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore }); + + // manages the "install ceph?" overlay + PVE.Utils.monitor_ceph_installed(me, rstore, nodename); + + var run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec || !rec.data.pool_name) { + return; + } + Ext.create('PVE.Ceph.PoolEdit', { + title: gettext('Edit') + ': Ceph Pool', + nodename: nodename, + pool_name: rec.data.pool_name, + isErasure: rec.data.type === 'erasure', + autoShow: true, + listeners: { + destroy: () => rstore.load(), + }, + }); + }; + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Create'), + handler: function() { + Ext.create('PVE.Ceph.PoolEdit', { + title: gettext('Create') + ': Ceph Pool', + isCreate: true, + isErasure: false, + nodename: nodename, + autoShow: true, + listeners: { + destroy: () => rstore.load(), + }, + }); + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + selModel: sm, + disabled: true, + handler: run_editor, + }, + { + xtype: 'proxmoxButton', + text: gettext('Destroy'), + selModel: sm, + disabled: true, + handler: function() { + let rec = sm.getSelection()[0]; + if (!rec || !rec.data.pool_name) { + return; + } + let poolName = rec.data.pool_name; + Ext.create('Proxmox.window.SafeDestroy', { + showProgress: true, + url: `/nodes/${nodename}/ceph/pool/${poolName}`, + params: { + remove_storages: 1, + }, + item: { + type: 'CephPool', + id: poolName, + }, + taskName: 'cephdestroypool', + autoShow: true, + listeners: { + destroy: () => rstore.load(), + }, + }); + }, + }, + ], + listeners: { + activate: () => rstore.startUpdate(), + destroy: () => rstore.stopUpdate(), + itemdblclick: run_editor, + }, + }); + + 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: 'pg_num_min', type: 'integer' }, + { name: 'bytes_used', type: 'integer' }, + { name: 'percent_used', type: 'number' }, + { name: 'crush_rule', type: 'integer' }, + { name: 'crush_rule_name', type: 'string' }, + { name: 'pg_autoscale_mode', type: 'string' }, + { name: 'pg_num_final', type: 'integer' }, + { name: 'target_size_ratio', type: 'number' }, + { name: 'target_size', type: 'integer' }, + ], + idProperty: 'pool_name', + }); +}); + +Ext.define('PVE.form.CephRuleSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCephRuleSelector', + + allowBlank: false, + valueField: 'name', + displayField: 'name', + editable: false, + queryMode: 'local', + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + me.originalAllowBlank = me.allowBlank; + me.allowBlank = true; + + Ext.apply(me, { + store: { + fields: ['name'], + sorters: 'name', + proxy: { + type: 'proxmox', + url: `/api2/json/nodes/${me.nodename}/ceph/rules`, + }, + autoLoad: { + callback: (records, op, success) => { + if (me.isCreate && success && records.length > 0) { + me.select(records[0]); + } + + me.allowBlank = me.originalAllowBlank; + delete me.originalAllowBlank; + me.validate(); + }, + }, + }, + }); + + me.callParent(); + }, + +}); +Ext.define('PVE.CephCreateService', { + extend: 'Proxmox.window.Edit', + mixins: ['Proxmox.Mixin.CBind'], + xtype: 'pveCephCreateService', + + method: 'POST', + isCreate: true, + showProgress: true, + width: 450, + + setNode: function(node) { + let me = this; + me.nodename = node; + me.updateUrl(); + }, + setExtraID: function(extraID) { + let me = this; + me.extraID = me.type === 'mds' ? `-${extraID}` : ''; + me.updateUrl(); + }, + updateUrl: function() { + let me = this; + + let extraID = me.extraID ?? ''; + let node = me.nodename; + + me.url = `/nodes/${node}/ceph/${me.type}/${node}${extraID}`; + }, + + defaults: { + labelWidth: 75, + }, + items: [ + { + xtype: 'pveNodeSelector', + fieldLabel: gettext('Host'), + selectCurNode: true, + allowBlank: false, + submitValue: false, + listeners: { + change: function(f, value) { + let view = this.up('pveCephCreateService'); + view.setNode(value); + }, + }, + }, + { + xtype: 'textfield', + fieldLabel: gettext('Extra ID'), + regex: /[a-zA-Z0-9]+/, + regexText: gettext('ID may only consist of alphanumeric characters'), + submitValue: false, + emptyText: Proxmox.Utils.NoneText, + cbind: { + disabled: get => get('type') !== 'mds', + hidden: get => get('type') !== 'mds', + }, + listeners: { + change: function(f, value) { + let view = this.up('pveCephCreateService'); + view.setExtraID(value); + }, + }, + }, + { + xtype: 'component', + border: false, + padding: '5 2', + style: { + fontSize: '12px', + }, + userCls: 'pmx-hint', + cbind: { + hidden: get => get('type') !== 'mds', + }, + html: gettext('The Extra ID allows creating multiple MDS per node, which increases redundancy with more than one CephFS.'), + }, + ], + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + if (!me.type) { + throw "no type specified"; + } + me.setNode(me.nodename); + + me.callParent(); + }, +}); + +Ext.define('PVE.node.CephServiceController', { + extend: 'Ext.app.ViewController', + alias: 'controller.CephServiceList', + + render_status: (value, metadata, rec) => value, + + render_version: function(value, metadata, rec) { + if (value === undefined) { + return ''; + } + let view = this.getView(); + let host = rec.data.host, nodev = [0]; + if (view.nodeversions[host] !== undefined) { + nodev = view.nodeversions[host].version.parts; + } + + let icon = ''; + if (PVE.Utils.compare_ceph_versions(view.maxversion, nodev) > 0) { + icon = PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE'); + } else if (PVE.Utils.compare_ceph_versions(nodev, value) > 0) { + icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD'); + } else if (view.mixedversions) { + icon = PVE.Utils.get_ceph_icon_html('HEALTH_OK'); + } + return icon + value; + }, + + getMaxVersions: function(store, records, success) { + if (!success || records.length < 1) { + return; + } + let me = this; + let view = me.getView(); + + view.nodeversions = records[0].data.node; + view.maxversion = []; + view.mixedversions = false; + for (const [_nodename, data] of Object.entries(view.nodeversions)) { + let res = PVE.Utils.compare_ceph_versions(data.version.parts, view.maxversion); + if (res !== 0 && view.maxversion.length > 0) { + view.mixedversions = true; + } + if (res > 0) { + view.maxversion = data.version.parts; + } + } + }, + + init: function(view) { + if (view.pveSelNode) { + view.nodename = view.pveSelNode.data.node; + } + if (!view.nodename) { + throw "no node name specified"; + } + + if (!view.type) { + throw "no type specified"; + } + + view.versionsstore = Ext.create('Proxmox.data.UpdateStore', { + autoStart: true, + interval: 10000, + storeid: `ceph-versions-${view.type}-list${view.nodename}`, + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/ceph/metadata?scope=versions", + }, + }); + view.versionsstore.on('load', this.getMaxVersions, this); + view.on('destroy', view.versionsstore.stopUpdate); + + view.rstore = Ext.create('Proxmox.data.UpdateStore', { + autoStart: true, + interval: 3000, + storeid: `ceph-${view.type}-list${view.nodename}`, + model: 'ceph-service-list', + proxy: { + type: 'proxmox', + url: `/api2/json/nodes/${view.nodename}/ceph/${view.type}`, + }, + }); + + view.setStore(Ext.create('Proxmox.data.DiffStore', { + rstore: view.rstore, + sorters: [{ property: 'name' }], + })); + + if (view.storeLoadCallback) { + view.rstore.on('load', view.storeLoadCallback, this); + } + view.on('destroy', view.rstore.stopUpdate); + + if (view.showCephInstallMask) { + PVE.Utils.monitor_ceph_installed(view, view.rstore, view.nodename, true); + } + }, + + service_cmd: function(rec, cmd) { + let view = this.getView(); + if (!rec.data.host) { + Ext.Msg.alert(gettext('Error'), "entry has no host"); + return; + } + let doRequest = function() { + Proxmox.Utils.API2Request({ + url: `/nodes/${rec.data.host}/ceph/${cmd}`, + method: 'POST', + params: { service: view.type + '.' + rec.data.name }, + success: function(response, options) { + Ext.create('Proxmox.window.TaskProgress', { + autoShow: true, + upid: response.result.data, + taskDone: () => view.rstore.load(), + }); + }, + failure: (response, _opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + }; + if (cmd === "stop" && ['mon', 'mds'].includes(view.type)) { + Proxmox.Utils.API2Request({ + url: `/nodes/${rec.data.host}/ceph/cmd-safety`, + params: { + service: view.type, + id: rec.data.name, + action: 'stop', + }, + method: 'GET', + success: function({ result: { data } }) { + let stopText = { + mon: gettext('Stop MON'), + mds: gettext('Stop MDS'), + }; + if (!data.safe) { + Ext.Msg.show({ + title: gettext('Warning'), + message: data.status, + icon: Ext.Msg.WARNING, + buttons: Ext.Msg.OKCANCEL, + buttonText: { ok: stopText[view.type] }, + fn: function(selection) { + if (selection === 'ok') { + doRequest(); + } + }, + }); + } else { + doRequest(); + } + }, + failure: (response, _opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + } else { + doRequest(); + } + }, + onChangeService: function(button) { + let me = this; + let record = me.getView().getSelection()[0]; + me.service_cmd(record, button.action); + }, + + showSyslog: function() { + let view = this.getView(); + let rec = view.getSelection()[0]; + let service = `ceph-${view.type}@${rec.data.name}`; + Ext.create('Ext.window.Window', { + title: `${gettext('Syslog')}: ${service}`, + autoShow: true, + modal: true, + width: 800, + height: 400, + layout: 'fit', + items: [{ + xtype: 'proxmoxLogView', + url: `/api2/extjs/nodes/${rec.data.host}/syslog?service=${encodeURIComponent(service)}`, + log_select_timespan: 1, + }], + }); + }, + + onCreate: function() { + let view = this.getView(); + Ext.create('PVE.CephCreateService', { + autoShow: true, + nodename: view.nodename, + subject: view.getTitle(), + type: view.type, + taskDone: () => view.rstore.load(), + }); + }, +}); + +Ext.define('PVE.node.CephServiceList', { + extend: 'Ext.grid.GridPanel', + xtype: 'pveNodeCephServiceList', + + onlineHelp: 'chapter_pveceph', + emptyText: gettext('No such service configured.'), + + stateful: true, + + // will be called when the store loads + storeLoadCallback: Ext.emptyFn, + + // if set to true, does shows the ceph install mask if needed + showCephInstallMask: false, + + controller: 'CephServiceList', + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Start'), + iconCls: 'fa fa-play', + action: 'start', + disabled: true, + enableFn: rec => rec.data.state === 'stopped' || rec.data.state === 'unknown', + handler: 'onChangeService', + }, + { + xtype: 'proxmoxButton', + text: gettext('Stop'), + iconCls: 'fa fa-stop', + action: 'stop', + enableFn: rec => rec.data.state !== 'stopped', + disabled: true, + handler: 'onChangeService', + }, + { + xtype: 'proxmoxButton', + text: gettext('Restart'), + iconCls: 'fa fa-refresh', + action: 'restart', + disabled: true, + enableFn: rec => rec.data.state !== 'stopped', + handler: 'onChangeService', + }, + '-', + { + text: gettext('Create'), + reference: 'createButton', + handler: 'onCreate', + }, + { + text: gettext('Destroy'), + xtype: 'proxmoxStdRemoveButton', + getUrl: function(rec) { + let view = this.up('grid'); + if (!rec.data.host) { + Ext.Msg.alert(gettext('Error'), "entry has no host, cannot build API url"); + return ''; + } + return `/nodes/${rec.data.host}/ceph/${view.type}/${rec.data.name}`; + }, + callback: function(options, success, response) { + let view = this.up('grid'); + if (!success) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + return; + } + Ext.create('Proxmox.window.TaskProgress', { + autoShow: true, + upid: response.result.data, + taskDone: () => view.rstore.load(), + }); + }, + handler: function(btn, event, rec) { + let me = this; + let view = me.up('grid'); + let doRequest = function() { + Proxmox.button.StdRemoveButton.prototype.handler.call(me, btn, event, rec); + }; + if (view.type === 'mon') { + Proxmox.Utils.API2Request({ + url: `/nodes/${rec.data.host}/ceph/cmd-safety`, + params: { + service: view.type, + id: rec.data.name, + action: 'destroy', + }, + method: 'GET', + success: function({ result: { data } }) { + if (!data.safe) { + Ext.Msg.show({ + title: gettext('Warning'), + message: data.status, + icon: Ext.Msg.WARNING, + buttons: Ext.Msg.OKCANCEL, + buttonText: { ok: gettext('Destroy MON') }, + fn: function(selection) { + if (selection === 'ok') { + doRequest(); + } + }, + }); + } else { + doRequest(); + } + }, + failure: (response, _opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + } else { + doRequest(); + } + }, + + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Syslog'), + disabled: true, + handler: 'showSyslog', + }, + ], + + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + renderer: function(v) { + return this.type + '.' + v; + }, + dataIndex: 'name', + }, + { + header: gettext('Host'), + flex: 1, + sortable: true, + renderer: function(v) { + return v || Proxmox.Utils.unknownText; + }, + dataIndex: 'host', + }, + { + header: gettext('Status'), + flex: 1, + sortable: false, + renderer: 'render_status', + dataIndex: 'state', + }, + { + header: gettext('Address'), + flex: 3, + sortable: true, + renderer: function(v) { + return v || Proxmox.Utils.unknownText; + }, + dataIndex: 'addr', + }, + { + header: gettext('Version'), + flex: 3, + sortable: true, + dataIndex: 'version', + renderer: 'render_version', + }, + ], + + initComponent: function() { + let me = this; + + if (me.additionalColumns) { + me.columns = me.columns.concat(me.additionalColumns); + } + + me.callParent(); + }, + +}, function() { + Ext.define('ceph-service-list', { + extend: 'Ext.data.Model', + fields: [ + 'addr', + 'name', + 'fs_name', + 'rank', + 'host', + 'quorum', + 'state', + 'ceph_version', + 'ceph_version_short', + { + type: 'string', + name: 'version', + calculate: data => PVE.Utils.parse_ceph_version(data), + }, + ], + idProperty: 'name', + }); +}); + +Ext.define('PVE.node.CephMDSServiceController', { + extend: 'PVE.node.CephServiceController', + alias: 'controller.CephServiceMDSList', + + render_status: (value, mD, rec) => rec.data.fs_name ? `${value} (${rec.data.fs_name})` : value, +}); + +Ext.define('PVE.node.CephMDSList', { + extend: 'PVE.node.CephServiceList', + xtype: 'pveNodeCephMDSList', + + controller: { + type: 'CephServiceMDSList', + }, +}); + +Ext.define('PVE.ceph.Services', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveCephServices', + + layout: { + type: 'hbox', + align: 'stretch', + }, + + bodyPadding: '0 5 20', + defaults: { + xtype: 'box', + style: { + 'text-align': 'center', + }, + }, + + items: [ + { + flex: 1, + xtype: 'pveCephServiceList', + itemId: 'mons', + title: gettext('Monitors'), + }, + { + flex: 1, + xtype: 'pveCephServiceList', + itemId: 'mgrs', + title: gettext('Managers'), + }, + { + flex: 1, + xtype: 'pveCephServiceList', + itemId: 'mdss', + title: gettext('Meta Data Servers'), + }, + ], + + updateAll: function(metadata, status) { + var me = this; + + const healthstates = { + 'HEALTH_UNKNOWN': 0, + 'HEALTH_ERR': 1, + 'HEALTH_WARN': 2, + 'HEALTH_UPGRADE': 3, + 'HEALTH_OLD': 4, + 'HEALTH_OK': 5, + }; + // order guarantee since es2020, but browsers did so before. Note, integers would break it. + const healthmap = Object.keys(healthstates); + let maxversion = "00.0.00"; + Object.values(metadata.node || {}).forEach(function(node) { + if (PVE.Utils.compare_ceph_versions(node?.version?.parts, maxversion) > 0) { + maxversion = node?.version?.parts; + } + }); + var quorummap = status && status.quorum_names ? status.quorum_names : []; + let monmessages = {}, mgrmessages = {}, mdsmessages = {}; + if (status) { + if (status.health) { + Ext.Object.each(status.health.checks, function(key, value, _obj) { + if (!Ext.String.startsWith(key, "MON_")) { + return; + } + for (let i = 0; i < value.detail.length; i++) { + let match = value.detail[i].message.match(/mon.([a-zA-Z0-9\-.]+)/); + if (!match) { + continue; + } + let monid = match[1]; + if (!monmessages[monid]) { + monmessages[monid] = { + worstSeverity: healthstates.HEALTH_OK, + messages: [], + }; + } + + let severityIcon = PVE.Utils.get_ceph_icon_html(value.severity, true); + let details = value.detail.reduce((acc, v) => `${acc}\n${v.message}`, ''); + monmessages[monid].messages.push(severityIcon + details); + + if (healthstates[value.severity] < monmessages[monid].worstSeverity) { + monmessages[monid].worstSeverity = healthstates[value.severity]; + } + } + }); + } + + if (status.mgrmap) { + mgrmessages[status.mgrmap.active_name] = "active"; + status.mgrmap.standbys.forEach(function(mgr) { + mgrmessages[mgr.name] = "standby"; + }); + } + + if (status.fsmap) { + status.fsmap.by_rank.forEach(function(mds) { + mdsmessages[mds.name] = 'rank: ' + mds.rank + "; " + mds.status; + }); + } + } + + let checks = { + mon: function(mon) { + if (quorummap.indexOf(mon.name) !== -1) { + mon.health = healthstates.HEALTH_OK; + } else { + mon.health = healthstates.HEALTH_ERR; + } + if (monmessages[mon.name]) { + if (monmessages[mon.name].worstSeverity < mon.health) { + mon.health = monmessages[mon.name].worstSeverity; + } + Array.prototype.push.apply(mon.messages, monmessages[mon.name].messages); + } + return mon; + }, + mgr: function(mgr) { + if (mgrmessages[mgr.name] === 'active') { + mgr.title = '' + mgr.title + ''; + mgr.statuses.push(gettext('Status') + ': active'); + } else if (mgrmessages[mgr.name] === 'standby') { + mgr.statuses.push(gettext('Status') + ': standby'); + } else if (mgr.health > healthstates.HEALTH_WARN) { + mgr.health = healthstates.HEALTH_WARN; + } + + return mgr; + }, + mds: function(mds) { + if (mdsmessages[mds.name]) { + mds.title = '' + mds.title + ''; + mds.statuses.push(gettext('Status') + ': ' + mdsmessages[mds.name]+""); + } else if (mds.addr !== Proxmox.Utils.unknownText) { + mds.statuses.push(gettext('Status') + ': standby'); + } + + return mds; + }, + }; + + for (let type of ['mon', 'mgr', 'mds']) { + var ids = Object.keys(metadata[type] || {}); + me[type] = {}; + + for (let id of ids) { + const [name, host] = id.split('@'); + let result = { + id: id, + health: healthstates.HEALTH_OK, + statuses: [], + messages: [], + name: name, + title: metadata[type][id].name || name, + host: host, + version: PVE.Utils.parse_ceph_version(metadata[type][id]), + service: metadata[type][id].service, + addr: metadata[type][id].addr || metadata[type][id].addrs || Proxmox.Utils.unknownText, + }; + + result.statuses = [ + gettext('Host') + ": " + host, + gettext('Address') + ": " + result.addr, + ]; + + if (checks[type]) { + result = checks[type](result); + } + + if (result.service && !result.version) { + result.messages.push( + PVE.Utils.get_ceph_icon_html('HEALTH_UNKNOWN', true) + + gettext('Stopped'), + ); + result.health = healthstates.HEALTH_UNKNOWN; + } + + if (!result.version && result.addr === Proxmox.Utils.unknownText) { + result.health = healthstates.HEALTH_UNKNOWN; + } + + if (result.version) { + result.statuses.push(gettext('Version') + ": " + result.version); + + if (PVE.Utils.compare_ceph_versions(result.version, maxversion) !== 0) { + let host_version = metadata.node[host]?.version?.parts || metadata.version?.[host] || ""; + if (PVE.Utils.compare_ceph_versions(host_version, maxversion) === 0) { + if (result.health > healthstates.HEALTH_OLD) { + result.health = healthstates.HEALTH_OLD; + } + result.messages.push( + PVE.Utils.get_ceph_icon_html('HEALTH_OLD', true) + + gettext('A newer version was installed but old version still running, please restart'), + ); + } else { + if (result.health > healthstates.HEALTH_UPGRADE) { + result.health = healthstates.HEALTH_UPGRADE; + } + result.messages.push( + PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE', true) + + gettext('Other cluster members use a newer version of this service, please upgrade and restart'), + ); + } + } + } + + result.statuses.push(''); // empty line + result.text = result.statuses.concat(result.messages).join('
'); + + result.health = healthmap[result.health]; + + me[type][id] = result; + } + } + + me.getComponent('mons').updateAll(Object.values(me.mon)); + me.getComponent('mgrs').updateAll(Object.values(me.mgr)); + me.getComponent('mdss').updateAll(Object.values(me.mds)); + }, +}); + +Ext.define('PVE.ceph.ServiceList', { + extend: 'Ext.container.Container', + xtype: 'pveCephServiceList', + + style: { + 'text-align': 'center', + }, + defaults: { + xtype: 'box', + style: { + 'text-align': 'center', + }, + }, + + items: [ + { + itemId: 'title', + data: { + title: '', + }, + tpl: '

{title}

', + }, + ], + + updateAll: function(list) { + var me = this; + me.suspendLayout = true; + + list.sort((a, b) => a.id > b.id ? 1 : a.id < b.id ? -1 : 0); + if (!me.ids) { + me.ids = []; + } + let pendingRemoval = {}; + me.ids.forEach(id => { pendingRemoval[id] = true; }); // mark all as to-remove first here + + for (let i = 0; i < list.length; i++) { + let service = me.getComponent(list[i].id); + if (!service) { + // services and list are sorted, so just insert at i + 1 (first el. is the title) + service = me.insert(i + 1, { + xtype: 'pveCephServiceWidget', + itemId: list[i].id, + }); + me.ids.push(list[i].id); + } else { + delete pendingRemoval[list[i].id]; // drop exisiting from for-removal + } + service.updateService(list[i].title, list[i].text, list[i].health); + } + Object.keys(pendingRemoval).forEach(id => me.remove(id)); // GC + + me.suspendLayout = false; + me.updateLayout(); + }, + + initComponent: function() { + var me = this; + me.callParent(); + me.getComponent('title').update({ + title: me.title, + }); + }, +}); + +Ext.define('PVE.ceph.ServiceWidget', { + extend: 'Ext.Component', + alias: 'widget.pveCephServiceWidget', + + userCls: 'monitor inline-block', + data: { + title: '0', + health: 'HEALTH_ERR', + text: '', + iconCls: PVE.Utils.get_health_icon(), + }, + + tpl: [ + '{title}: ', + '', + ], + + updateService: function(title, text, health) { + var me = this; + + me.update(Ext.apply(me.data, { + health: health, + text: text, + title: title, + iconCls: PVE.Utils.get_health_icon(PVE.Utils.map_ceph_health[health]), + })); + + if (me.tooltip) { + me.tooltip.setHtml(text); + } + }, + + listeners: { + destroy: function() { + let me = this; + if (me.tooltip) { + me.tooltip.destroy(); + delete me.tooltip; + } + }, + mouseenter: { + element: 'el', + fn: function(events, element) { + let view = this.component; + if (!view) { + return; + } + if (!view.tooltip || view.data.text !== view.tooltip.html) { + view.tooltip = Ext.create('Ext.tip.ToolTip', { + target: view.el, + trackMouse: true, + dismissDelay: 0, + renderTo: Ext.getBody(), + html: view.data.text, + }); + } + view.tooltip.show(); + }, + }, + mouseleave: { + element: 'el', + fn: function(events, element) { + let view = this.component; + if (view.tooltip) { + view.tooltip.destroy(); + delete view.tooltip; + } + }, + }, + }, +}); +Ext.define('PVE.node.CephStatus', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephStatus', + + onlineHelp: 'chapter_pveceph', + + scrollable: true, + bodyPadding: 5, + layout: { + type: 'column', + }, + + defaults: { + padding: 5, + }, + + items: [ + { + xtype: 'panel', + title: gettext('Health'), + bodyPadding: 10, + plugins: 'responsive', + responsiveConfig: { + 'width < 1600': { + minHeight: 230, + columnWidth: 1, + }, + 'width >= 1600': { + minHeight: 500, + columnWidth: 0.5, + }, + }, + layout: { + type: 'hbox', + align: 'stretch', + }, + items: [ + { + xtype: 'container', + layout: { + type: 'vbox', + align: 'stretch', + }, + flex: 1, + items: [ + { + + xtype: 'pveHealthWidget', + itemId: 'overallhealth', + flex: 1, + title: gettext('Status'), + }, + { + xtype: 'displayfield', + itemId: 'versioninfo', + fieldLabel: gettext('Ceph Version'), + value: "", + autoEl: { + tag: 'div', + 'data-qtip': gettext('The newest version installed in the Cluster.'), + }, + padding: '10 0 0 0', + style: { + 'text-align': 'center', + }, + }, + ], + }, + { + xtype: 'grid', + itemId: 'warnings', + flex: 2, + stateful: true, + stateId: 'ceph-status-warnings', + // we load the store manually, to show an emptyText specify an empty intermediate store + store: { + trackRemoved: false, + data: [], + }, + updateHealth: function(health) { + let checks = health.checks || {}; + + let checkRecords = Object.keys(checks).sort().map(key => { + let check = checks[key]; + return { + id: key, + summary: check.summary.message, + detail: check.detail.reduce((acc, v) => `${acc}\n${v.message}`, ''), + severity: check.severity, + }; + }); + + this.getStore().loadRawData(checkRecords, false); + }, + emptyText: gettext('No Warnings/Errors'), + columns: [ + { + dataIndex: 'severity', + header: gettext('Severity'), + align: 'center', + width: 70, + renderer: function(value) { + let health = PVE.Utils.map_ceph_health[value]; + let icon = PVE.Utils.get_health_icon(health); + return ``; + }, + sorter: { + sorterFn: function(a, b) { + let health = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK']; + return health.indexOf(b.data.severity) - health.indexOf(a.data.severity); + }, + }, + }, + { + dataIndex: 'summary', + header: gettext('Summary'), + flex: 1, + }, + { + xtype: 'actioncolumn', + width: 40, + align: 'center', + tooltip: gettext('Detail'), + items: [ + { + iconCls: 'x-fa fa-info-circle', + handler: function(grid, rowindex, colindex, item, e, record) { + var win = Ext.create('Ext.window.Window', { + title: gettext('Detail'), + resizable: true, + modal: true, + width: 650, + height: 400, + layout: { + type: 'fit', + }, + items: [{ + scrollable: true, + padding: 10, + xtype: 'box', + html: [ + '' + Ext.htmlEncode(record.data.summary) + '', + '
' + Ext.htmlEncode(record.data.detail) + '
', + ], + }], + }); + win.show(); + }, + }, + ], + }, + ], + }, + ], + }, + { + xtype: 'pveCephStatusDetail', + itemId: 'statusdetail', + plugins: 'responsive', + responsiveConfig: { + 'width < 1600': { + columnWidth: 1, + minHeight: 250, + }, + 'width >= 1600': { + columnWidth: 0.5, + minHeight: 300, + }, + }, + title: gettext('Status'), + }, + { + xtype: 'pveCephServices', + title: gettext('Services'), + itemId: 'services', + plugins: 'responsive', + layout: { + type: 'hbox', + align: 'stretch', + }, + responsiveConfig: { + 'width < 1600': { + columnWidth: 1, + minHeight: 200, + }, + 'width >= 1600': { + columnWidth: 0.5, + minHeight: 200, + }, + }, + }, + { + xtype: 'panel', + title: gettext('Performance'), + columnWidth: 1, + bodyPadding: 5, + layout: { + type: 'hbox', + align: 'center', + }, + items: [ + { + xtype: 'container', + flex: 1, + items: [ + { + xtype: 'proxmoxGauge', + itemId: 'space', + title: gettext('Usage'), + }, + { + flex: 1, + border: false, + }, + { + xtype: 'container', + itemId: 'recovery', + hidden: true, + padding: 25, + items: [ + { + xtype: 'pveRunningChart', + itemId: 'recoverychart', + title: gettext('Recovery') +'/ '+ gettext('Rebalance'), + renderer: PVE.Utils.render_bandwidth, + height: 100, + }, + { + xtype: 'progressbar', + itemId: 'recoveryprogress', + }, + ], + }, + ], + }, + { + xtype: 'container', + flex: 2, + defaults: { + padding: 0, + height: 100, + }, + items: [ + { + xtype: 'pveRunningChart', + itemId: 'reads', + title: gettext('Reads'), + renderer: PVE.Utils.render_bandwidth, + }, + { + xtype: 'pveRunningChart', + itemId: 'writes', + title: gettext('Writes'), + renderer: PVE.Utils.render_bandwidth, + }, + { + xtype: 'pveRunningChart', + itemId: 'readiops', + title: 'IOPS: ' + gettext('Reads'), + renderer: Ext.util.Format.numberRenderer('0,000'), + }, + { + xtype: 'pveRunningChart', + itemId: 'writeiops', + title: 'IOPS: ' + gettext('Writes'), + renderer: Ext.util.Format.numberRenderer('0,000'), + }, + ], + }, + ], + }, + ], + + updateAll: function(store, records, success) { + if (!success || records.length === 0) { + return; + } + + var me = this; + var rec = records[0]; + me.status = rec.data; + + // add health panel + me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {})); + me.down('#warnings').updateHealth(rec.data.health || {}); // add errors to gridstore + + me.getComponent('services').updateAll(me.metadata || {}, rec.data); + + me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data); + + // add performance data + let pgmap = rec.data.pgmap; + let used = pgmap.bytes_used; + let total = pgmap.bytes_total; + + var text = Ext.String.format(gettext('{0} of {1}'), + Proxmox.Utils.render_size(used), + Proxmox.Utils.render_size(total), + ); + + // update the usage widget + me.down('#space').updateValue(used/total, text); + + let readiops = pgmap.read_op_per_sec; + let writeiops = pgmap.write_op_per_sec; + let reads = pgmap.read_bytes_sec || 0; + let writes = pgmap.write_bytes_sec || 0; + + // update the graphs + me.reads.addDataPoint(reads); + me.writes.addDataPoint(writes); + me.readiops.addDataPoint(readiops); + me.writeiops.addDataPoint(writeiops); + + let degraded = pgmap.degraded_objects || 0; + let misplaced = pgmap.misplaced_objects || 0; + let unfound = pgmap.unfound_objects || 0; + let unhealthy = degraded + unfound + misplaced; + // update recovery + if (pgmap.recovering_objects_per_sec !== undefined || unhealthy > 0) { + let toRecoverObjects = pgmap.misplaced_total || pgmap.unfound_total || pgmap.degraded_total || 0; + if (toRecoverObjects === 0) { + return; // FIXME: unexpected return and leaves things possible visible when it shouldn't? + } + let recovered = toRecoverObjects - unhealthy || 0; + let speed = pgmap.recovering_bytes_per_sec || 0; + + let recoveryRatio = recovered / toRecoverObjects; + let txt = `${(recoveryRatio * 100).toFixed(2)}%`; + if (speed > 0) { + let obj_per_sec = speed / (4 * 1024 * 1024); // 4 MiB per Object + let duration = Proxmox.Utils.format_duration_human(unhealthy/obj_per_sec); + let speedTxt = PVE.Utils.render_bandwidth(speed); + txt += ` (${speedTxt} - ${duration} left)`; + } + + me.down('#recovery').setVisible(true); + me.down('#recoveryprogress').updateValue(recoveryRatio); + me.down('#recoveryprogress').updateText(txt); + me.down('#recoverychart').addDataPoint(speed); + } else { + me.down('#recovery').setVisible(false); + me.down('#recoverychart').addDataPoint(0); + } + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + + me.callParent(); + var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph'; + me.store = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'ceph-status-' + (nodename || 'cluster'), + interval: 5000, + proxy: { + type: 'proxmox', + url: baseurl + '/status', + }, + }); + + me.metadatastore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'ceph-metadata-' + (nodename || 'cluster'), + interval: 15*1000, + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/ceph/metadata', + }, + }); + + // save references for the updatefunction + me.iops = me.down('#iops'); + me.readiops = me.down('#readiops'); + me.writeiops = me.down('#writeiops'); + me.reads = me.down('#reads'); + me.writes = me.down('#writes'); + + // manages the "install ceph?" overlay + PVE.Utils.monitor_ceph_installed(me, me.store, nodename); + + me.mon(me.store, 'load', me.updateAll, me); + me.mon(me.metadatastore, 'load', function(store, records, success) { + if (!success || records.length < 1) { + return; + } + me.metadata = records[0].data; + + // update services + me.getComponent('services').updateAll(me.metadata, me.status || {}); + + // update detailstatus panel + me.getComponent('statusdetail').updateAll(me.metadata, me.status || {}); + + let maxversion = []; + let maxversiontext = ""; + for (const [_nodename, data] of Object.entries(me.metadata.node)) { + let version = data.version.parts; + if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) { + maxversion = version; + maxversiontext = data.version.str; + } + } + me.down('#versioninfo').setValue(maxversiontext); + }, me); + + me.on('destroy', me.store.stopUpdate); + me.on('destroy', me.metadatastore.stopUpdate); + me.store.startUpdate(); + me.metadatastore.startUpdate(); + }, + +}); +Ext.define('PVE.ceph.StatusDetail', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveCephStatusDetail', + + layout: { + type: 'hbox', + align: 'stretch', + }, + + bodyPadding: '0 5', + defaults: { + xtype: 'box', + style: { + 'text-align': 'center', + }, + }, + + items: [{ + flex: 1, + itemId: 'osds', + maxHeight: 250, + scrollable: true, + padding: '0 10 5 10', + data: { + total: 0, + upin: 0, + upout: 0, + downin: 0, + downout: 0, + oldOSD: [], + ghostOSD: [], + }, + tpl: [ + '

OSDs

', + '
HTTP:   `; + usage += `${method} /api2/json${endpoint}
', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
', + gettext('In'), + '', + gettext('Out'), + '
', + gettext('Up'), + '{upin}{upout}
', + gettext('Down'), + '{downin}{downout}
', + '
', + gettext('Total'), + ': {total}', + '

', + '', + ' ' + gettext('Outdated OSDs') + "
", + '
', + '', + '
osd.{id}:
', + '
{version}

', + '
', + '
', + '
', + '
', + '
', + '', + '
', + ` ${gettext('Ghost OSDs')}
`, + `
`, + '', + '
osd.{id}
', + '
', + '
', + '
', + '
', + ], + }, + { + flex: 1, + border: false, + itemId: 'pgchart', + xtype: 'polar', + height: 184, + innerPadding: 5, + insetPadding: 5, + colors: [ + '#CFCFCF', + '#21BF4B', + '#FFCC00', + '#FF6C59', + ], + store: { }, + series: [ + { + type: 'pie', + donut: 60, + angleField: 'count', + tooltip: { + trackMouse: true, + renderer: function(tooltip, record, ctx) { + var html = record.get('text'); + html += '
'; + record.get('states').forEach(function(state) { + html += '
' + + state.state_name + ': ' + state.count.toString(); + }); + tooltip.setHtml(html); + }, + }, + subStyle: { + strokeStyle: false, + }, + }, + ], + }, + { + flex: 1.6, + itemId: 'pgs', + padding: '0 10', + maxHeight: 250, + scrollable: true, + data: { + states: [], + }, + tpl: [ + '

PGs

', + '', + '
{state_name}:
', + '
{count}

', + '
', + '
', + ], + }], + + // similar to mgr dashboard + pgstates: { + // clean + clean: 1, + active: 1, + + // working + activating: 2, + backfill_wait: 2, + backfilling: 2, + creating: 2, + deep: 2, + degraded: 2, + forced_backfill: 2, + forced_recovery: 2, + peered: 2, + peering: 2, + recovering: 2, + recovery_wait: 2, + remapped: 2, + repair: 2, + scrubbing: 2, + snaptrim: 2, + snaptrim_wait: 2, + + // error + backfill_toofull: 3, + backfill_unfound: 3, + down: 3, + incomplete: 3, + inconsistent: 3, + recovery_toofull: 3, + recovery_unfound: 3, + snaptrim_error: 3, + stale: 3, + undersized: 3, + }, + + statecategories: [ + { + text: gettext('Unknown'), + count: 0, + states: [], + cls: 'faded', + }, + { + text: gettext('Clean'), + cls: 'good', + }, + { + text: gettext('Working'), + cls: 'warning', + }, + { + text: gettext('Error'), + cls: 'critical', + }, + ], + + checkThemeColors: function() { + let me = this; + let rootStyle = getComputedStyle(document.documentElement); + + // get color + let background = rootStyle.getPropertyValue("--pwt-panel-background").trim() || "#ffffff"; + + // set the colors + me.chart.setBackground(background); + me.chart.redraw(); + }, + + updateAll: function(metadata, status) { + let me = this; + me.suspendLayout = true; + + let maxversion = "0"; + Object.values(metadata.node || {}).forEach(function(node) { + if (PVE.Utils.compare_ceph_versions(node?.version?.parts, maxversion) > 0) { + maxversion = node.version.parts; + } + }); + + let oldOSD = [], ghostOSD = []; + metadata.osd?.forEach(osd => { + let version = PVE.Utils.parse_ceph_version(osd); + if (version !== undefined) { + if (PVE.Utils.compare_ceph_versions(version, maxversion) !== 0) { + oldOSD.push({ + id: osd.id, + version: version, + }); + } + } else { + if (Object.keys(osd).length > 1) { + console.warn('got OSD entry with no valid version but other keys', osd); + } + ghostOSD.push({ + id: osd.id, + }); + } + }); + + // update PGs sorted + let pgmap = status.pgmap || {}; + let pgs_by_state = pgmap.pgs_by_state || []; + pgs_by_state.sort(function(a, b) { + return a.state_name < b.state_name?-1:a.state_name === b.state_name?0:1; + }); + + me.statecategories.forEach(function(cat) { + cat.count = 0; + cat.states = []; + }); + + pgs_by_state.forEach(function(state) { + let states = state.state_name.split(/[^a-z]+/); + let result = 0; + for (let i = 0; i < states.length; i++) { + if (me.pgstates[states[i]] > result) { + result = me.pgstates[states[i]]; + } + } + // for the list + state.cls = me.statecategories[result].cls; + + me.statecategories[result].count += state.count; + me.statecategories[result].states.push(state); + }); + + me.chart.getStore().setData(me.statecategories); + me.getComponent('pgs').update({ states: pgs_by_state }); + + let health = status.health || {}; + // we collect monitor/osd information from the checks + const downinregex = /(\d+) osds down/; + let downin_osds = 0; + Ext.Object.each(health.checks, function(key, value, obj) { + var found = null; + if (key === 'OSD_DOWN') { + found = value.summary.message.match(downinregex); + if (found !== null) { + downin_osds = parseInt(found[1], 10); + } + } + }); + + let osdmap = status.osdmap || {}; + if (typeof osdmap.osdmap !== "undefined") { + osdmap = osdmap.osdmap; + } + // update OSDs counts + let total_osds = osdmap.num_osds || 0; + let in_osds = osdmap.num_in_osds || 0; + let up_osds = osdmap.num_up_osds || 0; + let down_osds = total_osds - up_osds; + + let downout_osds = down_osds - downin_osds; + let upin_osds = in_osds - downin_osds; + let upout_osds = up_osds - upin_osds; + + let osds = { + total: total_osds, + upin: upin_osds, + upout: upout_osds, + downin: downin_osds, + downout: downout_osds, + oldOSD: oldOSD, + ghostOSD, + }; + let osdcomponent = me.getComponent('osds'); + osdcomponent.update(Ext.apply(osdcomponent.data, osds)); + + me.suspendLayout = false; + me.updateLayout(); + }, + + initComponent: function() { + var me = this; + me.callParent(); + + me.chart = me.getComponent('pgchart'); + me.checkThemeColors(); + + // switch colors on media query changes + me.mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)"); + me.themeListener = (e) => { me.checkThemeColors(); }; + me.mediaQueryList.addEventListener("change", me.themeListener); + }, + + doDestroy: function() { + let me = this; + + me.mediaQueryList.removeEventListener("change", me.themeListener); + + me.callParent(); + }, +}); + +Ext.define('PVE.node.ACMEAccountCreate', { + extend: 'Proxmox.window.Edit', + mixins: ['Proxmox.Mixin.CBind'], + + width: 450, + title: gettext('Register Account'), + isCreate: true, + method: 'POST', + submitText: gettext('Register'), + url: '/cluster/acme/account', + showTaskViewer: true, + defaultExists: false, + + items: [ + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Account Name'), + name: 'name', + cbind: { + emptyText: (get) => get('defaultExists') ? '' : 'default', + allowBlank: (get) => !get('defaultExists'), + }, + }, + { + xtype: 'textfield', + name: 'contact', + vtype: 'email', + allowBlank: false, + fieldLabel: gettext('E-Mail'), + }, + { + xtype: 'proxmoxComboGrid', + name: 'directory', + allowBlank: false, + valueField: 'url', + displayField: 'name', + fieldLabel: gettext('ACME Directory'), + store: { + autoLoad: true, + fields: ['name', 'url'], + idProperty: ['name'], + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/acme/directories', + }, + sorters: { + property: 'name', + direction: 'ASC', + }, + }, + listConfig: { + columns: [ + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1, + }, + { + header: gettext('URL'), + dataIndex: 'url', + flex: 1, + }, + ], + }, + listeners: { + change: function(combogrid, value) { + var me = this; + if (!value) { + return; + } + + var disp = me.up('window').down('#tos_url_display'); + var field = me.up('window').down('#tos_url'); + var checkbox = me.up('window').down('#tos_checkbox'); + + disp.setValue(gettext('Loading')); + field.setValue(undefined); + checkbox.setValue(undefined); + checkbox.setHidden(true); + + Proxmox.Utils.API2Request({ + url: '/cluster/acme/tos', + method: 'GET', + params: { + directory: value, + }, + success: function(response, opt) { + field.setValue(response.result.data); + disp.setValue(response.result.data); + checkbox.setHidden(false); + }, + failure: function(response, opt) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, + }, + }, + { + xtype: 'displayfield', + itemId: 'tos_url_display', + renderer: PVE.Utils.render_optional_url, + name: 'tos_url_display', + }, + { + xtype: 'hidden', + itemId: 'tos_url', + name: 'tos_url', + }, + { + xtype: 'proxmoxcheckbox', + itemId: 'tos_checkbox', + boxLabel: gettext('Accept TOS'), + submitValue: false, + validateValue: function(value) { + if (value && this.checked) { + return true; + } + return false; + }, + }, + ], + +}); + +Ext.define('PVE.node.ACMEAccountView', { + extend: 'Proxmox.window.Edit', + + width: 600, + fieldDefaults: { + labelWidth: 140, + }, + + title: gettext('Account'), + + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('E-Mail'), + name: 'email', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Created'), + name: 'createdAt', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Status'), + name: 'status', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Directory'), + renderer: PVE.Utils.render_optional_url, + name: 'directory', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Terms of Services'), + renderer: PVE.Utils.render_optional_url, + name: 'tos', + }, + ], + + initComponent: function() { + var me = this; + + if (!me.accountname) { + throw "no account name defined"; + } + + me.url = '/cluster/acme/account/' + me.accountname; + + me.callParent(); + + // hide OK/Reset button, because we just want to show data + me.down('toolbar[dock=bottom]').setVisible(false); + + me.load({ + success: function(response) { + var data = response.result.data; + data.email = data.account.contact[0]; + data.createdAt = data.account.createdAt; + data.status = data.account.status; + me.setValues(data); + }, + }); + }, +}); + +Ext.define('PVE.node.ACMEDomainEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveACMEDomainEdit', + + subject: gettext('Domain'), + isCreate: false, + width: 450, + onlineHelp: 'sysadmin_certificate_management', + + items: [ + { + xtype: 'inputpanel', + onGetValues: function(values) { + let me = this; + let win = me.up('pveACMEDomainEdit'); + let nodeconfig = win.nodeconfig; + let olddomain = win.domain || {}; + + let params = { + digest: nodeconfig.digest, + }; + + let configkey = olddomain.configkey; + let acmeObj = PVE.Parser.parseACME(nodeconfig.acme); + + if (values.type === 'dns') { + if (!olddomain.configkey || olddomain.configkey === 'acme') { + // look for first free slot + for (let i = 0; i < PVE.Utils.acmedomain_count; i++) { + if (nodeconfig[`acmedomain${i}`] === undefined) { + configkey = `acmedomain${i}`; + break; + } + } + if (olddomain.domain) { + // we have to remove the domain from the acme domainlist + PVE.Utils.remove_domain_from_acme(acmeObj, olddomain.domain); + params.acme = PVE.Parser.printACME(acmeObj); + } + } + + delete values.type; + params[configkey] = PVE.Parser.printPropertyString(values, 'domain'); + } else { + if (olddomain.configkey && olddomain.configkey !== 'acme') { + // delete the old dns entry + params.delete = [olddomain.configkey]; + } + + // add new, remove old and make entries unique + PVE.Utils.add_domain_to_acme(acmeObj, values.domain); + PVE.Utils.remove_domain_from_acme(acmeObj, olddomain.domain); + params.acme = PVE.Parser.printACME(acmeObj); + } + + return params; + }, + items: [ + { + xtype: 'proxmoxKVComboBox', + name: 'type', + fieldLabel: gettext('Challenge Type'), + allowBlank: false, + value: 'standalone', + comboItems: [ + ['standalone', 'HTTP'], + ['dns', 'DNS'], + ], + validator: function(value) { + let me = this; + let win = me.up('pveACMEDomainEdit'); + let oldconfigkey = win.domain ? win.domain.configkey : undefined; + let val = me.getValue(); + if (val === 'dns' && (!oldconfigkey || oldconfigkey === 'acme')) { + // we have to check if there is a 'acmedomain' slot left + let found = false; + for (let i = 0; i < PVE.Utils.acmedomain_count; i++) { + if (!win.nodeconfig[`acmedomain${i}`]) { + found = true; + } + } + if (!found) { + return gettext('Only 5 Domains with type DNS can be configured'); + } + } + + return true; + }, + listeners: { + change: function(cb, value) { + let me = this; + let view = me.up('pveACMEDomainEdit'); + let pluginField = view.down('field[name=plugin]'); + pluginField.setDisabled(value !== 'dns'); + pluginField.setHidden(value !== 'dns'); + }, + }, + }, + { + xtype: 'hidden', + name: 'alias', + }, + { + xtype: 'pveACMEPluginSelector', + name: 'plugin', + disabled: true, + hidden: true, + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + name: 'domain', + allowBlank: false, + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Domain'), + }, + ], + }, + ], + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw 'no nodename given'; + } + + if (!me.nodeconfig) { + throw 'no nodeconfig given'; + } + + me.isCreate = !me.domain; + if (me.isCreate) { + me.domain = `${me.nodename}.`; // TODO: FQDN of node + } + + me.url = `/api2/extjs/nodes/${me.nodename}/config`; + + me.callParent(); + + if (!me.isCreate) { + me.setValues(me.domain); + } else { + me.setValues({ domain: me.domain }); + } + }, +}); + +Ext.define('pve-acme-domains', { + extend: 'Ext.data.Model', + fields: ['domain', 'type', 'alias', 'plugin', 'configkey'], + idProperty: 'domain', +}); + +Ext.define('PVE.node.ACME', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveACMEView', + + margin: '10 0 0 0', + title: 'ACME', + + emptyText: gettext('No Domains configured'), + + viewModel: { + data: { + domaincount: 0, + account: undefined, // the account we display + configaccount: undefined, // the account set in the config + accountEditable: false, + accountsAvailable: false, + }, + + formulas: { + canOrder: (get) => !!get('account') && get('domaincount') > 0, + editBtnIcon: (get) => 'fa black fa-' + (get('accountEditable') ? 'check' : 'pencil'), + editBtnText: (get) => get('accountEditable') ? gettext('Apply') : gettext('Edit'), + accountTextHidden: (get) => get('accountEditable') || !get('accountsAvailable'), + accountValueHidden: (get) => !get('accountEditable') || !get('accountsAvailable'), + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + let accountSelector = this.lookup('accountselector'); + accountSelector.store.on('load', this.onAccountsLoad, this); + }, + + onAccountsLoad: function(store, records, success) { + let me = this; + let vm = me.getViewModel(); + let configaccount = vm.get('configaccount'); + vm.set('accountsAvailable', records.length > 0); + if (me.autoChangeAccount && records.length > 0) { + me.changeAccount(records[0].data.name, () => { + vm.set('accountEditable', false); + me.reload(); + }); + me.autoChangeAccount = false; + } else if (configaccount) { + if (store.findExact('name', configaccount) !== -1) { + vm.set('account', configaccount); + } else { + vm.set('account', null); + } + } + }, + + addDomain: function() { + let me = this; + let view = me.getView(); + + Ext.create('PVE.node.ACMEDomainEdit', { + nodename: view.nodename, + nodeconfig: view.nodeconfig, + apiCallDone: function() { + me.reload(); + }, + }).show(); + }, + + editDomain: function() { + let me = this; + let view = me.getView(); + + let selection = view.getSelection(); + if (selection.length < 1) return; + + Ext.create('PVE.node.ACMEDomainEdit', { + nodename: view.nodename, + nodeconfig: view.nodeconfig, + domain: selection[0].data, + apiCallDone: function() { + me.reload(); + }, + }).show(); + }, + + removeDomain: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (selection.length < 1) return; + + let rec = selection[0].data; + let params = {}; + if (rec.configkey !== 'acme') { + params.delete = rec.configkey; + } else { + let acme = PVE.Parser.parseACME(view.nodeconfig.acme); + PVE.Utils.remove_domain_from_acme(acme, rec.domain); + params.acme = PVE.Parser.printACME(acme); + } + + Proxmox.Utils.API2Request({ + method: 'PUT', + url: `/nodes/${view.nodename}/config`, + params, + success: function(response, opt) { + me.reload(); + }, + failure: function(response, opt) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, + + toggleEditAccount: function() { + let me = this; + let vm = me.getViewModel(); + let editable = vm.get('accountEditable'); + if (editable) { + me.changeAccount(vm.get('account'), function() { + vm.set('accountEditable', false); + me.reload(); + }); + } else { + vm.set('accountEditable', true); + } + }, + + changeAccount: function(account, callback) { + let me = this; + let view = me.getView(); + let params = {}; + + let acme = PVE.Parser.parseACME(view.nodeconfig.acme); + acme.account = account; + params.acme = PVE.Parser.printACME(acme); + + Proxmox.Utils.API2Request({ + method: 'PUT', + waitMsgTarget: view, + url: `/nodes/${view.nodename}/config`, + params, + success: function(response, opt) { + if (Ext.isFunction(callback)) { + callback(); + } + }, + failure: function(response, opt) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, + + order: function() { + let me = this; + let view = me.getView(); + + Proxmox.Utils.API2Request({ + method: 'POST', + params: { + force: 1, + }, + url: `/nodes/${view.nodename}/certificates/acme/certificate`, + success: function(response, opt) { + Ext.create('Proxmox.window.TaskViewer', { + upid: response.result.data, + taskDone: function(success) { + me.orderFinished(success); + }, + }).show(); + }, + failure: function(response, opt) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, + + orderFinished: function(success) { + if (!success) return; + var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!'); + Ext.getBody().mask(txt, ['pve-static-mask']); + // reload after 10 seconds automatically + Ext.defer(function() { + window.location.reload(true); + }, 10000); + }, + + reload: function() { + let me = this; + let view = me.getView(); + view.rstore.load(); + }, + + addAccount: function() { + let me = this; + Ext.create('PVE.node.ACMEAccountCreate', { + autoShow: true, + taskDone: function() { + me.reload(); + let accountSelector = me.lookup('accountselector'); + me.autoChangeAccount = true; + accountSelector.store.load(); + }, + }); + }, + }, + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Add'), + handler: 'addDomain', + selModel: false, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + disabled: true, + handler: 'editDomain', + }, + { + xtype: 'proxmoxStdRemoveButton', + handler: 'removeDomain', + }, + '-', + { + xtype: 'button', + reference: 'order', + text: gettext('Order Certificates Now'), + bind: { + disabled: '{!canOrder}', + }, + handler: 'order', + }, + '-', + { + xtype: 'displayfield', + value: gettext('Using Account') + ':', + bind: { + hidden: '{!accountsAvailable}', + }, + }, + { + xtype: 'displayfield', + reference: 'accounttext', + renderer: (val) => val || Proxmox.Utils.NoneText, + bind: { + value: '{account}', + hidden: '{accountTextHidden}', + }, + }, + { + xtype: 'pveACMEAccountSelector', + hidden: true, + reference: 'accountselector', + bind: { + value: '{account}', + hidden: '{accountValueHidden}', + }, + }, + { + xtype: 'button', + iconCls: 'fa black fa-pencil', + bind: { + iconCls: '{editBtnIcon}', + text: '{editBtnText}', + hidden: '{!accountsAvailable}', + }, + handler: 'toggleEditAccount', + }, + { + xtype: 'displayfield', + value: gettext('No Account available.'), + bind: { + hidden: '{accountsAvailable}', + }, + }, + { + xtype: 'button', + hidden: true, + reference: 'accountlink', + text: gettext('Add ACME Account'), + bind: { + hidden: '{accountsAvailable}', + }, + handler: 'addAccount', + }, + ], + + updateStore: function(store, records, success) { + let me = this; + let data = []; + let rec; + if (success && records.length > 0) { + rec = records[0]; + } else { + rec = { + data: {}, + }; + } + + me.nodeconfig = rec.data; // save nodeconfig for updates + + let account = 'default'; + + if (rec.data.acme) { + let obj = PVE.Parser.parseACME(rec.data.acme); + (obj.domains || []).forEach(domain => { + if (domain === '') return; + let record = { + domain, + type: 'standalone', + configkey: 'acme', + }; + data.push(record); + }); + + if (obj.account) { + account = obj.account; + } + } + + let vm = me.getViewModel(); + let oldaccount = vm.get('account'); + + // account changed, and we do not edit currently, load again to verify + if (oldaccount !== account && !vm.get('accountEditable')) { + vm.set('configaccount', account); + me.lookup('accountselector').store.load(); + } + + for (let i = 0; i < PVE.Utils.acmedomain_count; i++) { + let acmedomain = rec.data[`acmedomain${i}`]; + if (!acmedomain) continue; + + let record = PVE.Parser.parsePropertyString(acmedomain, 'domain'); + record.type = 'dns'; + record.configkey = `acmedomain${i}`; + data.push(record); + } + + vm.set('domaincount', data.length); + me.store.loadData(data, false); + }, + + listeners: { + itemdblclick: 'editDomain', + }, + + columns: [ + { + dataIndex: 'domain', + flex: 5, + text: gettext('Domain'), + }, + { + dataIndex: 'type', + flex: 1, + text: gettext('Type'), + }, + { + dataIndex: 'plugin', + flex: 1, + text: gettext('Plugin'), + }, + ], + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + me.rstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 10 * 1000, + autoStart: true, + storeid: `pve-node-domains-${me.nodename}`, + proxy: { + type: 'proxmox', + url: `/api2/json/nodes/${me.nodename}/config`, + }, + }); + + me.store = Ext.create('Ext.data.Store', { + model: 'pve-acme-domains', + sorters: 'domain', + }); + + me.callParent(); + me.mon(me.rstore, 'load', 'updateStore', me); + Proxmox.Utils.monStoreErrors(me, me.rstore); + me.on('destroy', me.rstore.stopUpdate, me.rstore); + }, +}); +Ext.define('PVE.node.CertificateView', { + extend: 'Ext.container.Container', + xtype: 'pveCertificatesView', + + onlineHelp: 'sysadmin_certificate_management', + + mixins: ['Proxmox.Mixin.CBind'], + scrollable: 'y', + + items: [ + { + xtype: 'pveCertView', + border: 0, + cbind: { + nodename: '{nodename}', + }, + }, + { + xtype: 'pveACMEView', + border: 0, + cbind: { + nodename: '{nodename}', + }, + }, + ], + +}); + +Ext.define('PVE.node.CertificateViewer', { + extend: 'Proxmox.window.Edit', + + title: gettext('Certificate'), + + fieldDefaults: { + labelWidth: 120, + }, + width: 800, + + items: { + xtype: 'inputpanel', + maxHeight: 900, + scrollable: 'y', + columnT: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Name'), + name: 'filename', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Fingerprint'), + name: 'fingerprint', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Issuer'), + name: 'issuer', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Subject'), + name: 'subject', + }, + ], + column1: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Public Key Type'), + name: 'public-key-type', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Public Key Size'), + name: 'public-key-bits', + }, + ], + column2: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Valid Since'), + renderer: Proxmox.Utils.render_timestamp, + name: 'notbefore', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Expires'), + renderer: Proxmox.Utils.render_timestamp, + name: 'notafter', + }, + ], + columnB: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Subject Alternative Names'), + name: 'san', + renderer: PVE.Utils.render_san, + }, + { + xtype: 'fieldset', + title: gettext('Raw Certificate'), + collapsible: true, + collapsed: true, + items: [{ + xtype: 'textarea', + name: 'pem', + editable: false, + grow: true, + growMax: 350, + fieldStyle: { + 'white-space': 'pre-wrap', + 'font-family': 'monospace', + }, + }], + }, + ], + }, + + initComponent: function() { + let me = this; + + if (!me.cert) { + throw "no cert given"; + } + if (!me.nodename) { + throw "no nodename given"; + } + + me.url = `/nodes/${me.nodename}/certificates/info`; + me.callParent(); + + // hide OK/Reset button, because we just want to show data + me.down('toolbar[dock=bottom]').setVisible(false); + + me.load({ + success: function(response) { + if (Ext.isArray(response.result.data)) { + for (const item of response.result.data) { + if (item.filename === me.cert) { + me.setValues(item); + return; + } + } + } + }, + }); + }, +}); + +Ext.define('PVE.node.CertUpload', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCertUpload', + + title: gettext('Upload Custom Certificate'), + resizable: false, + isCreate: true, + submitText: gettext('Upload'), + method: 'POST', + width: 600, + + apiCallDone: function(success, response, options) { + if (!success) { + return; + } + let txt = gettext('API server will be restarted to use new certificates, please reload web-interface!'); + Ext.getBody().mask(txt, ['pve-static-mask']); + Ext.defer(() => window.location.reload(true), 10000); // reload after 10 seconds automatically + }, + + items: { + xtype: 'inputpanel', + onGetValues: function(values) { + values.restart = 1; + values.force = 1; + if (!values.key) { + delete values.key; + } + return values; + }, + items: [ + { + fieldLabel: gettext('Private Key (Optional)'), + labelAlign: 'top', + emptyText: gettext('No change'), + name: 'key', + xtype: 'textarea', + }, + { + xtype: 'filebutton', + text: gettext('From File'), + listeners: { + change: function(btn, e, value) { + let form = this.up('form'); + for (const file of e.event.target.files) { + PVE.Utils.loadFile(file, res => form.down('field[name=key]').setValue(res)); + } + btn.reset(); + }, + }, + }, + { + fieldLabel: gettext('Certificate Chain'), + labelAlign: 'top', + allowBlank: false, + name: 'certificates', + xtype: 'textarea', + }, + { + xtype: 'filebutton', + text: gettext('From File'), + listeners: { + change: function(btn, e, value) { + let form = this.up('form'); + for (const file of e.event.target.files) { + PVE.Utils.loadFile(file, res => form.down('field[name=certificates]').setValue(res)); + } + btn.reset(); + }, + }, + }, + ], + }, + + initComponent: function() { + let me = this; + if (!me.nodename) { + throw "no nodename given"; + } + me.url = `/nodes/${me.nodename}/certificates/custom`; + + me.callParent(); + }, +}); + +Ext.define('pve-certificate', { + extend: 'Ext.data.Model', + fields: ['filename', 'fingerprint', 'issuer', 'notafter', 'notbefore', 'subject', 'san', 'public-key-bits', 'public-key-type'], + idProperty: 'filename', +}); + +Ext.define('PVE.node.Certificates', { + extend: 'Ext.grid.Panel', + xtype: 'pveCertView', + + tbar: [ + { + xtype: 'button', + text: gettext('Upload Custom Certificate'), + handler: function() { + let view = this.up('grid'); + Ext.create('PVE.node.CertUpload', { + nodename: view.nodename, + listeners: { + destroy: () => view.reload(), + }, + autoShow: true, + }); + }, + }, + { + xtype: 'proxmoxStdRemoveButton', + itemId: 'deletebtn', + text: gettext('Delete Custom Certificate'), + dangerous: true, + selModel: false, + getUrl: function(rec) { + let view = this.up('grid'); + return `/nodes/${view.nodename}/certificates/custom?restart=1`; + }, + confirmMsg: gettext('Delete custom certificate and switch to generated one?'), + callback: function(options, success, response) { + if (success) { + let txt = gettext('API server will be restarted to use new certificates, please reload web-interface!'); + Ext.getBody().mask(txt, ['pve-static-mask']); + // reload after 10 seconds automatically + Ext.defer(() => window.location.reload(true), 10000); + } + }, + }, + '-', + { + xtype: 'proxmoxButton', + itemId: 'viewbtn', + disabled: true, + text: gettext('View Certificate'), + handler: function() { + this.up('grid').viewCertificate(); + }, + }, + ], + + columns: [ + { + header: gettext('File'), + width: 150, + dataIndex: 'filename', + }, + { + header: gettext('Issuer'), + flex: 1, + dataIndex: 'issuer', + }, + { + header: gettext('Subject'), + flex: 1, + dataIndex: 'subject', + }, + { + header: gettext('Public Key Alogrithm'), + flex: 1, + dataIndex: 'public-key-type', + hidden: true, + }, + { + header: gettext('Public Key Size'), + flex: 1, + dataIndex: 'public-key-bits', + hidden: true, + }, + { + header: gettext('Valid Since'), + width: 150, + dataIndex: 'notbefore', + renderer: Proxmox.Utils.render_timestamp, + }, + { + header: gettext('Expires'), + width: 150, + dataIndex: 'notafter', + renderer: Proxmox.Utils.render_timestamp, + }, + { + header: gettext('Subject Alternative Names'), + flex: 1, + dataIndex: 'san', + renderer: PVE.Utils.render_san, + }, + { + header: gettext('Fingerprint'), + dataIndex: 'fingerprint', + hidden: true, + }, + { + header: gettext('PEM'), + dataIndex: 'pem', + hidden: true, + }, + ], + + reload: function() { + this.rstore.load(); + }, + + viewCertificate: function() { + let me = this; + let selection = me.getSelection(); + if (!selection || selection.length < 1) { + return; + } + var win = Ext.create('PVE.node.CertificateViewer', { + cert: selection[0].data.filename, + nodename: me.nodename, + }); + win.show(); + }, + + listeners: { + itemdblclick: 'viewCertificate', + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + me.rstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'certs-' + me.nodename, + model: 'pve-certificate', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/certificates/info', + }, + }); + + me.store = { + type: 'diff', + rstore: me.rstore, + }; + + me.callParent(); + + me.mon(me.rstore, 'load', store => me.down('#deletebtn').setDisabled(!store.getById('pveproxy-ssl.pem'))); + me.rstore.startUpdate(); + me.on('destroy', me.rstore.stopUpdate, me.rstore); + }, +}); +Ext.define('PVE.node.CmdMenu', { + extend: 'Ext.menu.Menu', + xtype: 'nodeCmdMenu', + + showSeparator: false, + + items: [ + { + text: gettext('Create VM'), + itemId: 'createvm', + iconCls: 'fa fa-desktop', + handler: function() { + Ext.create('PVE.qemu.CreateWizard', { + nodename: this.up('menu').nodename, + autoShow: true, + }); + }, + }, + { + text: gettext('Create CT'), + itemId: 'createct', + iconCls: 'fa fa-cube', + handler: function() { + Ext.create('PVE.lxc.CreateWizard', { + nodename: this.up('menu').nodename, + autoShow: true, + }); + }, + }, + { xtype: 'menuseparator' }, + { + text: gettext('Bulk Start'), + itemId: 'bulkstart', + iconCls: 'fa fa-fw fa-play', + handler: function() { + Ext.create('PVE.window.BulkAction', { + nodename: this.up('menu').nodename, + title: gettext('Bulk Start'), + btnText: gettext('Start'), + action: 'startall', + autoShow: true, + }); + }, + }, + { + text: gettext('Bulk Shutdown'), + itemId: 'bulkstop', + iconCls: 'fa fa-fw fa-stop', + handler: function() { + Ext.create('PVE.window.BulkAction', { + nodename: this.up('menu').nodename, + title: gettext('Bulk Shutdown'), + btnText: gettext('Shutdown'), + action: 'stopall', + autoShow: true, + }); + }, + }, + { + text: gettext('Bulk Migrate'), + itemId: 'bulkmigrate', + iconCls: 'fa fa-fw fa-send-o', + handler: function() { + Ext.create('PVE.window.BulkAction', { + nodename: this.up('menu').nodename, + title: gettext('Bulk Migrate'), + btnText: gettext('Migrate'), + action: 'migrateall', + autoShow: true, + }); + }, + }, + { xtype: 'menuseparator' }, + { + text: gettext('Shell'), + itemId: 'shell', + iconCls: 'fa fa-fw fa-terminal', + handler: function() { + let nodename = this.up('menu').nodename; + PVE.Utils.openDefaultConsoleWindow(true, 'shell', undefined, nodename, undefined); + }, + }, + { xtype: 'menuseparator' }, + { + text: gettext('Wake-on-LAN'), + itemId: 'wakeonlan', + iconCls: 'fa fa-fw fa-power-off', + handler: function() { + let nodename = this.up('menu').nodename; + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/wakeonlan`, + method: 'POST', + failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: function(response, opts) { + Ext.Msg.show({ + title: 'Success', + icon: Ext.Msg.INFO, + msg: Ext.String.format( + gettext("Wake on LAN packet send for '{0}': '{1}'"), + nodename, + response.result.data, + ), + }); + }, + }); + }, + }, + ], + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw 'no nodename specified'; + } + + me.title = gettext('Node') + " '" + me.nodename + "'"; + me.callParent(); + + let caps = Ext.state.Manager.get('GuiCap'); + + if (!caps.vms['VM.Allocate']) { + me.getComponent('createct').setDisabled(true); + me.getComponent('createvm').setDisabled(true); + } + if (!caps.vms['VM.Migrate']) { + me.getComponent('bulkmigrate').setDisabled(true); + } + if (!caps.vms['VM.PowerMgmt']) { + me.getComponent('bulkstart').setDisabled(true); + me.getComponent('bulkstop').setDisabled(true); + } + if (!caps.nodes['Sys.PowerMgmt']) { + me.getComponent('wakeonlan').setDisabled(true); + } + if (!caps.nodes['Sys.Console']) { + me.getComponent('shell').setDisabled(true); + } + if (me.pveSelNode.data.running) { + me.getComponent('wakeonlan').setDisabled(true); + } + }, +}); +Ext.define('PVE.node.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.node.Config', + + onlineHelp: 'chapter_system_administration', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + nodename + "/status", + interval: 5000, + }); + + var node_command = function(cmd) { + Proxmox.Utils.API2Request({ + params: { command: cmd }, + url: '/nodes/' + nodename + '/status', + method: 'POST', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }; + + var actionBtn = Ext.create('Ext.Button', { + text: gettext('Bulk Actions'), + iconCls: 'fa fa-fw fa-ellipsis-v', + disabled: !caps.vms['VM.PowerMgmt'] && !caps.vms['VM.Migrate'], + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Bulk Start'), + iconCls: 'fa fa-fw fa-play', + disabled: !caps.vms['VM.PowerMgmt'], + handler: function() { + Ext.create('PVE.window.BulkAction', { + autoShow: true, + nodename: nodename, + title: gettext('Bulk Start'), + btnText: gettext('Start'), + action: 'startall', + }); + }, + }, + { + text: gettext('Bulk Shutdown'), + iconCls: 'fa fa-fw fa-stop', + disabled: !caps.vms['VM.PowerMgmt'], + handler: function() { + Ext.create('PVE.window.BulkAction', { + autoShow: true, + nodename: nodename, + title: gettext('Bulk Shutdown'), + btnText: gettext('Shutdown'), + action: 'stopall', + }); + }, + }, + { + text: gettext('Bulk Migrate'), + iconCls: 'fa fa-fw fa-send-o', + disabled: !caps.vms['VM.Migrate'], + handler: function() { + Ext.create('PVE.window.BulkAction', { + autoShow: true, + nodename: nodename, + title: gettext('Bulk Migrate'), + btnText: gettext('Migrate'), + action: 'migrateall', + }); + }, + }, + ], + }), + }); + + let restartBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Reboot'), + disabled: !caps.nodes['Sys.PowerMgmt'], + dangerous: true, + confirmMsg: Ext.String.format(gettext("Reboot node '{0}'?"), nodename), + handler: function() { + node_command('reboot'); + }, + iconCls: 'fa fa-undo', + }); + + var shutdownBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Shutdown'), + disabled: !caps.nodes['Sys.PowerMgmt'], + dangerous: true, + confirmMsg: Ext.String.format(gettext("Shutdown node '{0}'?"), nodename), + handler: function() { + node_command('shutdown'); + }, + iconCls: 'fa fa-power-off', + }); + + var shellBtn = Ext.create('PVE.button.ConsoleButton', { + disabled: !caps.nodes['Sys.Console'], + text: gettext('Shell'), + consoleType: 'shell', + nodename: nodename, + }); + + me.items = []; + + Ext.apply(me, { + title: gettext('Node') + " '" + nodename + "'", + hstateid: 'nodetab', + defaults: { + statusStore: me.statusStore, + }, + tbar: [restartBtn, shutdownBtn, shellBtn, actionBtn], + }); + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + xtype: 'pveNodeSummary', + title: gettext('Summary'), + iconCls: 'fa fa-book', + itemId: 'summary', + }, + { + xtype: 'pmxNotesView', + title: gettext('Notes'), + iconCls: 'fa fa-sticky-note-o', + itemId: 'notes', + }, + ); + } + + if (caps.nodes['Sys.Console']) { + me.items.push( + { + xtype: 'pveNoVncConsole', + title: gettext('Shell'), + iconCls: 'fa fa-terminal', + itemId: 'jsconsole', + consoleType: 'shell', + xtermjs: true, + nodename: nodename, + }, + ); + } + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + xtype: 'proxmoxNodeServiceView', + title: gettext('System'), + iconCls: 'fa fa-cogs', + itemId: 'services', + expandedOnInit: true, + restartCommand: 'reload', // avoid disruptions + startOnlyServices: { + 'pveproxy': true, + 'pvedaemon': true, + 'pve-cluster': true, + }, + nodename: nodename, + onlineHelp: 'pve_service_daemons', + }, + { + xtype: 'proxmoxNodeNetworkView', + title: gettext('Network'), + iconCls: 'fa fa-exchange', + itemId: 'network', + showApplyBtn: true, + groups: ['services'], + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + }, + { + xtype: 'pveCertificatesView', + title: gettext('Certificates'), + iconCls: 'fa fa-certificate', + itemId: 'certificates', + groups: ['services'], + nodename: nodename, + }, + { + xtype: 'proxmoxNodeDNSView', + title: gettext('DNS'), + iconCls: 'fa fa-globe', + groups: ['services'], + itemId: 'dns', + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + }, + { + xtype: 'proxmoxNodeHostsView', + title: gettext('Hosts'), + iconCls: 'fa fa-globe', + groups: ['services'], + itemId: 'hosts', + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + }, + { + xtype: 'proxmoxNodeOptionsView', + title: gettext('Options'), + iconCls: 'fa fa-gear', + groups: ['services'], + itemId: 'options', + nodename: nodename, + onlineHelp: 'proxmox_node_management', + }, + { + xtype: 'proxmoxNodeTimeView', + title: gettext('Time'), + itemId: 'time', + groups: ['services'], + nodename: nodename, + iconCls: 'fa fa-clock-o', + }); + } + + if (caps.nodes['Sys.Syslog']) { + me.items.push({ + xtype: 'proxmoxJournalView', + title: 'Syslog', + iconCls: 'fa fa-list', + groups: ['services'], + disabled: !caps.nodes['Sys.Syslog'], + itemId: 'syslog', + url: "/api2/extjs/nodes/" + nodename + "/journal", + }); + + if (caps.nodes['Sys.Modify']) { + me.items.push({ + xtype: 'proxmoxNodeAPT', + title: gettext('Updates'), + iconCls: 'fa fa-refresh', + expandedOnInit: true, + disabled: !caps.nodes['Sys.Console'], + // do we want to link to system updates instead? + itemId: 'apt', + upgradeBtn: { + xtype: 'pveConsoleButton', + disabled: Proxmox.UserName !== 'root@pam', + text: gettext('Upgrade'), + consoleType: 'upgrade', + nodename: nodename, + }, + nodename: nodename, + }); + + me.items.push({ + xtype: 'proxmoxNodeAPTRepositories', + title: gettext('Repositories'), + iconCls: 'fa fa-files-o', + itemId: 'aptrepositories', + nodename: nodename, + onlineHelp: 'sysadmin_package_repositories', + groups: ['apt'], + }); + } + } + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + xtype: 'pveFirewallRules', + iconCls: 'fa fa-shield', + title: gettext('Firewall'), + allow_iface: true, + base_url: '/nodes/' + nodename + '/firewall/rules', + list_refs_url: '/cluster/firewall/refs', + itemId: 'firewall', + }, + { + xtype: 'pveFirewallOptions', + title: gettext('Options'), + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_host_specific_configuration', + groups: ['firewall'], + base_url: '/nodes/' + nodename + '/firewall/options', + fwtype: 'node', + itemId: 'firewall-options', + }); + } + + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + xtype: 'pmxDiskList', + title: gettext('Disks'), + itemId: 'storage', + expandedOnInit: true, + iconCls: 'fa fa-hdd-o', + nodename: nodename, + includePartitions: true, + supportsWipeDisk: true, + }, + { + xtype: 'pveLVMList', + title: 'LVM', + itemId: 'lvm', + onlineHelp: 'chapter_lvm', + iconCls: 'fa fa-square', + groups: ['storage'], + }, + { + xtype: 'pveLVMThinList', + title: 'LVM-Thin', + itemId: 'lvmthin', + onlineHelp: 'chapter_lvm', + iconCls: 'fa fa-square-o', + groups: ['storage'], + }, + { + xtype: 'pveDirectoryList', + title: Proxmox.Utils.directoryText, + itemId: 'directory', + onlineHelp: 'chapter_storage', + iconCls: 'fa fa-folder', + groups: ['storage'], + }, + { + title: 'ZFS', + itemId: 'zfs', + onlineHelp: 'chapter_zfs', + iconCls: 'fa fa-th-large', + groups: ['storage'], + xtype: 'pveZFSList', + }, + { + xtype: 'pveNodeCephStatus', + title: 'Ceph', + itemId: 'ceph', + iconCls: 'fa fa-ceph', + }, + { + xtype: 'pveNodeCephConfigCrush', + title: gettext('Configuration'), + iconCls: 'fa fa-gear', + groups: ['ceph'], + itemId: 'ceph-config', + }, + { + xtype: 'pveNodeCephMonMgr', + title: gettext('Monitor'), + iconCls: 'fa fa-tv', + groups: ['ceph'], + itemId: 'ceph-monlist', + }, + { + xtype: 'pveNodeCephOsdTree', + title: 'OSD', + iconCls: 'fa fa-hdd-o', + groups: ['ceph'], + itemId: 'ceph-osdtree', + }, + { + xtype: 'pveNodeCephFSPanel', + title: 'CephFS', + iconCls: 'fa fa-folder', + groups: ['ceph'], + nodename: nodename, + itemId: 'ceph-cephfspanel', + }, + { + xtype: 'pveNodeCephPoolList', + title: 'Pools', + iconCls: 'fa fa-sitemap', + groups: ['ceph'], + itemId: 'ceph-pools', + }, + { + xtype: 'pveReplicaView', + iconCls: 'fa fa-retweet', + title: gettext('Replication'), + itemId: 'replication', + }, + ); + } + + if (caps.nodes['Sys.Syslog']) { + me.items.push( + { + xtype: 'proxmoxLogView', + title: gettext('Log'), + iconCls: 'fa fa-list', + groups: ['firewall'], + onlineHelp: 'chapter_pve_firewall', + url: '/api2/extjs/nodes/' + nodename + '/firewall/log', + itemId: 'firewall-fwlog', + }, + { + xtype: 'cephLogView', + title: gettext('Log'), + itemId: 'ceph-log', + iconCls: 'fa fa-list', + groups: ['ceph'], + onlineHelp: 'chapter_pveceph', + url: "/api2/extjs/nodes/" + nodename + "/ceph/log", + nodename: nodename, + }); + } + + me.items.push( + { + title: gettext('Task History'), + iconCls: 'fa fa-list-alt', + itemId: 'tasks', + nodename: nodename, + xtype: 'proxmoxNodeTasks', + extraFilter: [ + { + xtype: 'pveGuestIDSelector', + fieldLabel: gettext('VMID'), + allowBlank: true, + name: 'vmid', + }, + ], + }, + { + title: gettext('Subscription'), + iconCls: 'fa fa-support', + itemId: 'support', + xtype: 'pveNodeSubscription', + nodename: nodename, + }, + ); + + me.callParent(); + + me.mon(me.statusStore, 'load', function(store, records, success) { + let uptimerec = store.data.get('uptime'); + let powermgmt = caps.nodes['Sys.PowerMgmt'] && uptimerec && uptimerec.data.value; + + restartBtn.setDisabled(!powermgmt); + shutdownBtn.setDisabled(!powermgmt); + shellBtn.setDisabled(!powermgmt); + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + }, +}); +Ext.define('PVE.node.CreateDirectory', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCreateDirectory', + + subject: Proxmox.Utils.directoryText, + + showProgress: true, + + onlineHelp: 'chapter_storage', + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/disks/directory", + method: 'POST', + items: [ + { + xtype: 'pmxDiskSelector', + name: 'device', + nodename: me.nodename, + diskType: 'unused', + includePartitions: true, + fieldLabel: gettext('Disk'), + allowBlank: false, + }, + { + xtype: 'proxmoxKVComboBox', + comboItems: [ + ['ext4', 'ext4'], + ['xfs', 'xfs'], + ], + fieldLabel: gettext('Filesystem'), + name: 'filesystem', + value: '', + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('Name'), + allowBlank: false, + }, + { + xtype: 'proxmoxcheckbox', + name: 'add_storage', + fieldLabel: gettext('Add Storage'), + value: '1', + }, + ], + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.node.Directorylist', { + extend: 'Ext.grid.Panel', + xtype: 'pveDirectoryList', + + viewModel: { + data: { + path: '', + }, + formulas: { + dirName: (get) => get('path')?.replace('/mnt/pve/', '') || undefined, + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + destroyDirectory: function() { + let me = this; + let vm = me.getViewModel(); + let view = me.getView(); + + const dirName = vm.get('dirName'); + + if (!view.nodename) { + throw "no node name specified"; + } + + if (!dirName) { + throw "no directory name specified"; + } + + Ext.create('PVE.window.SafeDestroyStorage', { + url: `/nodes/${view.nodename}/disks/directory/${dirName}`, + item: { id: dirName }, + taskName: 'dirremove', + taskDone: () => { view.reload(); }, + }).show(); + }, + }, + + stateful: true, + stateId: 'grid-node-directory', + columns: [ + { + text: gettext('Path'), + dataIndex: 'path', + flex: 1, + }, + { + header: gettext('Device'), + flex: 1, + dataIndex: 'device', + }, + { + header: gettext('Type'), + width: 100, + dataIndex: 'type', + }, + { + header: gettext('Options'), + width: 100, + dataIndex: 'options', + }, + { + header: gettext('Unit File'), + hidden: true, + dataIndex: 'unitfile', + }, + ], + + rootVisible: false, + useArrows: true, + + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + this.up('panel').reload(); + }, + }, + { + text: `${gettext('Create')}: ${gettext('Directory')}`, + handler: function() { + let view = this.up('panel'); + Ext.create('PVE.node.CreateDirectory', { + nodename: view.nodename, + listeners: { + destroy: () => view.reload(), + }, + autoShow: true, + }); + }, + }, + '->', + { + xtype: 'tbtext', + data: { + dirName: undefined, + }, + bind: { + data: { + dirName: "{dirName}", + }, + }, + tpl: [ + '', + gettext('Directory') + ' {dirName}:', + '', + Ext.String.format(gettext('No {0} selected'), gettext('directory')), + '', + ], + }, + { + text: gettext('More'), + iconCls: 'fa fa-bars', + disabled: true, + bind: { + disabled: '{!dirName}', + }, + menu: [ + { + text: gettext('Destroy'), + itemId: 'remove', + iconCls: 'fa fa-fw fa-trash-o', + handler: 'destroyDirectory', + disabled: true, + bind: { + disabled: '{!dirName}', + }, + }, + ], + }, + ], + + reload: function() { + let me = this; + me.store.load(); + me.store.sort(); + }, + + listeners: { + activate: function() { + this.reload(); + }, + selectionchange: function(model, selected) { + let me = this; + let vm = me.getViewModel(); + + vm.set('path', selected[0]?.data.path || ''); + }, + }, + + initComponent: function() { + let me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + store: { + fields: ['path', 'device', 'type', 'options', 'unitfile'], + proxy: { + type: 'proxmox', + url: `/api2/json/nodes/${me.nodename}/disks/directory`, + }, + sorters: 'path', + }, + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore(), true); + me.reload(); + }, +}); + +Ext.define('PVE.node.CreateLVM', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCreateLVM', + + onlineHelp: 'chapter_lvm', + subject: 'LVM Volume Group', + + showProgress: true, + isCreate: true, + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + Ext.applyIf(me, { + url: `/nodes/${me.nodename}/disks/lvm`, + method: 'POST', + items: [ + { + xtype: 'pmxDiskSelector', + name: 'device', + nodename: me.nodename, + diskType: 'unused', + includePartitions: true, + fieldLabel: gettext('Disk'), + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('Name'), + allowBlank: false, + }, + { + xtype: 'proxmoxcheckbox', + name: 'add_storage', + fieldLabel: gettext('Add Storage'), + value: '1', + }, + ], + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.node.LVMList', { + extend: 'Ext.tree.Panel', + xtype: 'pveLVMList', + + viewModel: { + data: { + volumeGroup: '', + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + destroyVolumeGroup: function() { + let me = this; + let vm = me.getViewModel(); + let view = me.getView(); + + const volumeGroup = vm.get('volumeGroup'); + + if (!view.nodename) { + throw "no node name specified"; + } + + if (!volumeGroup) { + throw "no volume group specified"; + } + + Ext.create('PVE.window.SafeDestroyStorage', { + url: `/nodes/${view.nodename}/disks/lvm/${volumeGroup}`, + item: { id: volumeGroup }, + taskName: 'lvmremove', + taskDone: () => { view.reload(); }, + }).show(); + }, + }, + + emptyText: PVE.Utils.renderNotFound('VGs'), + + stateful: true, + stateId: 'grid-node-lvm', + + rootVisible: false, + useArrows: true, + + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name'), + dataIndex: 'name', + flex: 1, + }, + { + text: gettext('Number of LVs'), + dataIndex: 'lvcount', + width: 150, + align: 'right', + }, + { + header: gettext('Assigned to LVs'), + width: 130, + dataIndex: 'usage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar', + }, + }, + { + header: gettext('Size'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size', + }, + { + header: gettext('Free'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'free', + }, + ], + + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + this.up('panel').reload(); + }, + }, + { + text: gettext('Create') + ': Volume Group', + handler: function() { + let view = this.up('panel'); + Ext.create('PVE.node.CreateLVM', { + nodename: view.nodename, + taskDone: () => view.reload(), + autoShow: true, + }); + }, + }, + '->', + { + xtype: 'tbtext', + data: { + volumeGroup: undefined, + }, + bind: { + data: { + volumeGroup: "{volumeGroup}", + }, + }, + tpl: [ + '', + 'Volume group {volumeGroup}:', + '', + Ext.String.format(gettext('No {0} selected'), 'volume group'), + '', + ], + }, + { + text: gettext('More'), + iconCls: 'fa fa-bars', + disabled: true, + bind: { + disabled: '{!volumeGroup}', + }, + menu: [ + { + text: gettext('Destroy'), + itemId: 'remove', + iconCls: 'fa fa-fw fa-trash-o', + handler: 'destroyVolumeGroup', + disabled: true, + bind: { + disabled: '{!volumeGroup}', + }, + }, + ], + }, + ], + + reload: function() { + let me = this; + let sm = me.getSelectionModel(); + Proxmox.Utils.API2Request({ + url: `/nodes/${me.nodename}/disks/lvm`, + waitMsgTarget: me, + method: 'GET', + failure: (response, opts) => Proxmox.Utils.setErrorMask(me, response.htmlStatus), + success: function(response, opts) { + sm.deselectAll(); + me.setRootNode(response.result.data); + me.expandAll(); + }, + }); + }, + + listeners: { + activate: function() { + this.reload(); + }, + selectionchange: function(model, selected) { + let me = this; + let vm = me.getViewModel(); + + if (selected.length < 1 || selected[0].data.parentId !== 'root') { + vm.set('volumeGroup', ''); + } else { + vm.set('volumeGroup', selected[0].data.name); + } + }, + }, + + selModel: 'treemodel', + fields: [ + 'name', + 'size', + 'free', + { + type: 'string', + name: 'iconCls', + calculate: data => `fa x-fa-tree fa-${data.leaf ? 'hdd-o' : 'object-group'}`, + }, + { + type: 'number', + name: 'usage', + calculate: data => (data.size - data.free) / data.size, + }, + ], + sorters: 'name', + + initComponent: function() { + let me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + me.callParent(); + + me.reload(); + }, +}); + +Ext.define('PVE.node.CreateLVMThin', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCreateLVMThin', + + onlineHelp: 'chapter_lvm', + subject: 'LVM Thinpool', + + showProgress: true, + isCreate: true, + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.applyIf(me, { + url: `/nodes/${me.nodename}/disks/lvmthin`, + method: 'POST', + items: [ + { + xtype: 'pmxDiskSelector', + name: 'device', + nodename: me.nodename, + diskType: 'unused', + includePartitions: true, + fieldLabel: gettext('Disk'), + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('Name'), + allowBlank: false, + }, + { + xtype: 'proxmoxcheckbox', + name: 'add_storage', + fieldLabel: gettext('Add Storage'), + value: '1', + }, + ], + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.node.LVMThinList', { + extend: 'Ext.grid.Panel', + xtype: 'pveLVMThinList', + + viewModel: { + data: { + thinPool: '', + volumeGroup: '', + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + destroyThinPool: function() { + let me = this; + let vm = me.getViewModel(); + let view = me.getView(); + + const thinPool = vm.get('thinPool'); + const volumeGroup = vm.get('volumeGroup'); + + if (!view.nodename) { + throw "no node name specified"; + } + + if (!thinPool) { + throw "no thin pool specified"; + } + + if (!volumeGroup) { + throw "no volume group specified"; + } + + Ext.create('PVE.window.SafeDestroyStorage', { + url: `/nodes/${view.nodename}/disks/lvmthin/${thinPool}`, + params: { 'volume-group': volumeGroup }, + item: { id: `${volumeGroup}/${thinPool}` }, + taskName: 'lvmthinremove', + taskDone: () => { view.reload(); }, + }).show(); + }, + }, + + emptyText: PVE.Utils.renderNotFound('Thin-Pool'), + + stateful: true, + stateId: 'grid-node-lvmthin', + + rootVisible: false, + useArrows: true, + + columns: [ + { + text: gettext('Name'), + dataIndex: 'lv', + flex: 1, + }, + { + header: 'Volume Group', + width: 110, + dataIndex: 'vg', + }, + { + header: gettext('Usage'), + width: 110, + dataIndex: 'usage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar', + }, + }, + { + header: gettext('Size'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'lv_size', + }, + { + header: gettext('Used'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'used', + }, + { + header: gettext('Metadata Usage'), + width: 120, + dataIndex: 'metadata_usage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar', + }, + }, + { + header: gettext('Metadata Size'), + width: 120, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'metadata_size', + }, + { + header: gettext('Metadata Used'), + width: 125, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'metadata_used', + }, + ], + + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + this.up('panel').reload(); + }, + }, + { + text: gettext('Create') + ': Thinpool', + handler: function() { + var view = this.up('panel'); + Ext.create('PVE.node.CreateLVMThin', { + nodename: view.nodename, + taskDone: () => view.reload(), + autoShow: true, + }); + }, + }, + '->', + { + xtype: 'tbtext', + data: { + thinPool: undefined, + volumeGroup: undefined, + }, + bind: { + data: { + thinPool: "{thinPool}", + volumeGroup: "{volumeGroup}", + }, + }, + tpl: [ + '', + '', + 'Thinpool {volumeGroup}/{thinPool}:', + '', // volumeGroup + 'Missing volume group (node running old version?)', + '', + '', // thinPool + Ext.String.format(gettext('No {0} selected'), 'thinpool'), + '', + ], + }, + { + text: gettext('More'), + iconCls: 'fa fa-bars', + disabled: true, + bind: { + disabled: '{!volumeGroup || !thinPool}', + }, + menu: [ + { + text: gettext('Destroy'), + itemId: 'remove', + iconCls: 'fa fa-fw fa-trash-o', + handler: 'destroyThinPool', + disabled: true, + bind: { + disabled: '{!volumeGroup || !thinPool}', + }, + }, + ], + }, + ], + + reload: function() { + let me = this; + me.store.load(); + me.store.sort(); + }, + + listeners: { + activate: function() { + this.reload(); + }, + selectionchange: function(model, selected) { + let me = this; + let vm = me.getViewModel(); + + vm.set('volumeGroup', selected[0]?.data.vg || ''); + vm.set('thinPool', selected[0]?.data.lv || ''); + }, + }, + + initComponent: function() { + let me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + store: { + fields: [ + 'lv', + 'lv_size', + 'used', + 'metadata_size', + 'metadata_used', + { + type: 'number', + name: 'usage', + calculate: data => data.used / data.lv_size, + }, + { + type: 'number', + name: 'metadata_usage', + calculate: data => data.metadata_used / data.metadata_size, + }, + ], + proxy: { + type: 'proxmox', + url: `/api2/json/nodes/${me.nodename}/disks/lvmthin`, + }, + sorters: 'lv', + }, + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore(), true); + me.reload(); + }, +}); + +Ext.define('PVE.node.StatusView', { + extend: 'Proxmox.panel.StatusView', + alias: 'widget.pveNodeStatus', + + height: 300, + bodyPadding: '15 5 15 5', + + layout: { + type: 'table', + columns: 2, + tableAttrs: { + style: { + width: '100%', + }, + }, + }, + + defaults: { + xtype: 'pmxInfoWidget', + padding: '0 10 5 10', + }, + + items: [ + { + itemId: 'cpu', + iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon', + title: gettext('CPU usage'), + valueField: 'cpu', + maxField: 'cpuinfo', + renderer: Proxmox.Utils.render_node_cpu_usage, + }, + { + itemId: 'wait', + iconCls: 'fa fa-fw fa-clock-o', + title: gettext('IO delay'), + valueField: 'wait', + rowspan: 2, + }, + { + itemId: 'load', + iconCls: 'fa fa-fw fa-tasks', + title: gettext('Load average'), + printBar: false, + textField: 'loadavg', + }, + { + xtype: 'box', + colspan: 2, + padding: '0 0 20 0', + }, + { + iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon', + itemId: 'memory', + title: gettext('RAM usage'), + valueField: 'memory', + maxField: 'memory', + renderer: Proxmox.Utils.render_node_size_usage, + }, + { + itemId: 'ksm', + printBar: false, + title: gettext('KSM sharing'), + textField: 'ksm', + renderer: function(record) { + return Proxmox.Utils.render_size(record.shared); + }, + padding: '0 10 10 10', + }, + { + iconCls: 'fa fa-fw fa-hdd-o', + itemId: 'rootfs', + title: '/ ' + gettext('HD space'), + valueField: 'rootfs', + maxField: 'rootfs', + renderer: Proxmox.Utils.render_node_size_usage, + }, + { + iconCls: 'fa fa-fw fa-refresh', + itemId: 'swap', + printSize: true, + title: gettext('SWAP usage'), + valueField: 'swap', + maxField: 'swap', + renderer: Proxmox.Utils.render_node_size_usage, + }, + { + xtype: 'box', + colspan: 2, + padding: '0 0 20 0', + }, + { + itemId: 'cpus', + colspan: 2, + printBar: false, + title: gettext('CPU(s)'), + textField: 'cpuinfo', + renderer: Proxmox.Utils.render_cpu_model, + value: '', + }, + { + itemId: 'kversion', + colspan: 2, + title: gettext('Kernel Version'), + printBar: false, + textField: 'kversion', + value: '', + }, + { + itemId: 'version', + colspan: 2, + printBar: false, + title: gettext('PVE Manager Version'), + textField: 'pveversion', + value: '', + }, + ], + + updateTitle: function() { + var me = this; + var uptime = Proxmox.Utils.render_uptime(me.getRecordValue('uptime')); + me.setTitle(me.pveSelNode.data.node + ' (' + gettext('Uptime') + ': ' + uptime + ')'); + }, + + initComponent: function() { + let me = this; + + let stateProvider = Ext.state.Manager.getProvider(); + let repoLink = stateProvider.encodeHToken({ + view: "server", + rid: `node/${me.pveSelNode.data.node}`, + ltab: "tasks", + nodetab: "aptrepositories", + }); + + me.items.push({ + xtype: 'pmxNodeInfoRepoStatus', + itemId: 'repositoryStatus', + product: 'Proxmox VE', + repoLink: `#${repoLink}`, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.node.SubscriptionKeyEdit', { + extend: 'Proxmox.window.Edit', + title: gettext('Upload Subscription Key'), + width: 300, + items: { + xtype: 'textfield', + name: 'key', + value: '', + fieldLabel: gettext('Subscription Key'), + }, + initComponent: function() { + var me = this; + + me.callParent(); + + me.load(); + }, +}); + +Ext.define('PVE.node.Subscription', { + extend: 'Proxmox.grid.ObjectGrid', + + alias: ['widget.pveNodeSubscription'], + + onlineHelp: 'getting_help', + + viewConfig: { + enableTextSelection: true, + }, + + showReport: function() { + var me = this; + + var getReportFileName = function() { + var now = Ext.Date.format(new Date(), 'D-d-F-Y-G-i'); + return `${me.nodename}-pve-report-${now}.txt`; + }; + + var view = Ext.createWidget('component', { + itemId: 'system-report-view', + scrollable: true, + style: { + 'white-space': 'pre', + 'font-family': 'monospace', + padding: '5px', + }, + }); + + var reportWindow = Ext.create('Ext.window.Window', { + title: gettext('System Report'), + width: 1024, + height: 600, + layout: 'fit', + modal: true, + buttons: [ + '->', + { + text: gettext('Download'), + handler: function() { + var fileContent = Ext.String.htmlDecode(reportWindow.getComponent('system-report-view').html); + var fileName = getReportFileName(); + + // Internet Explorer + if (window.navigator.msSaveOrOpenBlob) { + navigator.msSaveOrOpenBlob(new Blob([fileContent]), fileName); + } else { + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + + encodeURIComponent(fileContent)); + element.setAttribute('download', fileName); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } + }, + }, + ], + items: view, + }); + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/nodes/' + me.nodename + '/report', + method: 'GET', + waitMsgTarget: me, + failure: function(response) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response) { + var report = Ext.htmlEncode(response.result.data); + reportWindow.show(); + view.update(report); + }, + }); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var reload = function() { + me.rstore.load(); + }; + + var baseurl = '/nodes/' + me.nodename + '/subscription'; + + var render_status = function(value) { + var message = me.getObjectValue('message'); + if (message) { + return value + ": " + message; + } + return value; + }; + + var rows = { + productname: { + header: gettext('Type'), + }, + key: { + header: gettext('Subscription Key'), + }, + status: { + header: gettext('Status'), + renderer: render_status, + }, + message: { + visible: false, + }, + serverid: { + header: gettext('Server ID'), + }, + sockets: { + header: gettext('Sockets'), + }, + checktime: { + header: gettext('Last checked'), + renderer: Proxmox.Utils.render_timestamp, + }, + nextduedate: { + header: gettext('Next due date'), + }, + signature: { + header: gettext('Signed/Offline'), + renderer: (value) => { + if (value) { + return gettext('Yes'); + } else { + return gettext('No'); + } + }, + }, + }; + + Ext.apply(me, { + url: '/api2/json' + baseurl, + cwidth1: 170, + tbar: [ + { + text: gettext('Upload Subscription Key'), + handler: function() { + var win = Ext.create('PVE.node.SubscriptionKeyEdit', { + url: '/api2/extjs/' + baseurl, + }); + win.show(); + win.on('destroy', reload); + }, + }, + { + text: gettext('Check'), + handler: function() { + Proxmox.Utils.API2Request({ + params: { force: 1 }, + url: baseurl, + method: 'POST', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: reload, + }); + }, + }, + { + text: gettext('Remove Subscription'), + xtype: 'proxmoxStdRemoveButton', + confirmMsg: gettext('Are you sure you want to remove the subscription key?'), + baseurl: baseurl, + dangerous: true, + selModel: false, + callback: reload, + }, + '-', + { + text: gettext('System Report'), + handler: function() { + Proxmox.Utils.checked_command(function() { me.showReport(); }); + }, + }, + ], + rows: rows, + listeners: { + activate: reload, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.node.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeSummary', + + scrollable: true, + bodyPadding: 5, + + showVersions: function() { + var me = this; + + // Note: we use simply text/html here, because ExtJS grid has problems + // with cut&paste + + var nodename = me.pveSelNode.data.node; + + var view = Ext.createWidget('component', { + autoScroll: true, + id: 'pkgversions', + padding: 5, + style: { + 'white-space': 'pre', + 'font-family': 'monospace', + }, + }); + + var win = Ext.create('Ext.window.Window', { + title: gettext('Package versions'), + width: 600, + height: 600, + layout: 'fit', + modal: true, + items: [view], + buttons: [ + { + xtype: 'button', + iconCls: 'fa fa-clipboard', + handler: function(button) { + window.getSelection().selectAllChildren( + document.getElementById('pkgversions'), + ); + document.execCommand("copy"); + }, + text: gettext('Copy'), + }, + { + text: gettext('Ok'), + handler: function() { + this.up('window').close(); + }, + }, + ], + }); + + Proxmox.Utils.API2Request({ + waitMsgTarget: me, + url: `/nodes/${nodename}/apt/versions`, + method: 'GET', + failure: function(response, opts) { + win.close(); + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + win.show(); + let text = ''; + Ext.Array.each(response.result.data, function(rec) { + let version = "not correctly installed"; + let pkg = rec.Package; + if (rec.OldVersion && rec.CurrentState === 'Installed') { + version = rec.OldVersion; + } + if (rec.RunningKernel) { + text += `${pkg}: ${version} (running kernel: ${rec.RunningKernel})\n`; + } else if (rec.ManagerVersion) { + text += `${pkg}: ${version} (running version: ${rec.ManagerVersion})\n`; + } else { + text += `${pkg}: ${version}\n`; + } + }); + + view.update(Ext.htmlEncode(text)); + }, + }); + }, + + updateRepositoryStatus: function() { + let me = this; + let repoStatus = me.nodeStatus.down('#repositoryStatus'); + + let nodename = me.pveSelNode.data.node; + + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/apt/repositories`, + method: 'GET', + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: response => repoStatus.setRepositoryInfo(response.result.data['standard-repos']), + }); + + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/subscription`, + method: 'GET', + failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: function(response, opts) { + const res = response.result; + const subscription = res?.data?.status.toLowerCase() === 'active'; + repoStatus.setSubscriptionStatus(subscription); + }, + }); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + if (!me.statusStore) { + throw "no status storage specified"; + } + + var rstore = me.statusStore; + + var version_btn = new Ext.Button({ + text: gettext('Package versions'), + handler: function() { + Proxmox.Utils.checked_command(function() { me.showVersions(); }); + }, + }); + + var rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: "/api2/json/nodes/" + nodename + "/rrddata", + model: 'pve-rrd-node', + }); + + let nodeStatus = Ext.create('PVE.node.StatusView', { + xtype: 'pveNodeStatus', + rstore: rstore, + width: 770, + pveSelNode: me.pveSelNode, + }); + + Ext.apply(me, { + tbar: [version_btn, '->', { xtype: 'proxmoxRRDTypeSelector' }], + nodeStatus: nodeStatus, + items: [ + { + xtype: 'container', + itemId: 'itemcontainer', + layout: 'column', + minWidth: 700, + defaults: { + minHeight: 325, + padding: 5, + columnWidth: 1, + }, + items: [ + nodeStatus, + { + xtype: 'proxmoxRRDChart', + title: gettext('CPU usage'), + fields: ['cpu', 'iowait'], + fieldTitles: [gettext('CPU usage'), gettext('IO delay')], + unit: 'percent', + store: rrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Server load'), + fields: ['loadavg'], + fieldTitles: [gettext('Load average')], + store: rrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Memory usage'), + fields: ['memtotal', 'memused'], + fieldTitles: [gettext('Total'), gettext('RAM usage')], + unit: 'bytes', + powerOfTwo: true, + store: rrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Network traffic'), + fields: ['netin', 'netout'], + store: rrdstore, + }, + ], + listeners: { + resize: function(panel) { + Proxmox.Utils.updateColumns(panel); + }, + }, + }, + ], + listeners: { + activate: function() { + rstore.setInterval(1000); + rstore.startUpdate(); // just to be sure + rrdstore.startUpdate(); + }, + destroy: function() { + rstore.setInterval(5000); // don't stop it, it's not ours! + rrdstore.stopUpdate(); + }, + }, + }); + + me.updateRepositoryStatus(); + + me.callParent(); + + let sp = Ext.state.Manager.getProvider(); + me.mon(sp, 'statechange', function(provider, key, value) { + if (key !== 'summarycolumns') { + return; + } + Proxmox.Utils.updateColumns(me.getComponent('itemcontainer')); + }); + }, +}); +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 the reference documentation.', + }, + ], + }); + + 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: [ + '', + 'Pool {pool}:', + '', + Ext.String.format(gettext('No {0} selected'), 'pool'), + '', + ], + }, + { + 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(); + }, +}); + +Ext.define('Proxmox.node.NodeOptionsView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.proxmoxNodeOptionsView'], + mixins: ['Proxmox.Mixin.CBind'], + + cbindData: function(_initialconfig) { + let me = this; + + let baseUrl = `/nodes/${me.nodename}/config`; + me.url = `/api2/json${baseUrl}`; + me.editorConfig = { + url: `/api2/extjs/${baseUrl}`, + }; + + return {}; + }, + + listeners: { + itemdblclick: function() { this.run_editor(); }, + activate: function() { this.rstore.startUpdate(); }, + destroy: function() { this.rstore.stopUpdate(); }, + deactivate: function() { this.rstore.stopUpdate(); }, + }, + + tbar: [ + { + text: gettext('Edit'), + xtype: 'proxmoxButton', + disabled: true, + handler: btn => btn.up('grid').run_editor(), + }, + ], + + gridRows: [ + { + xtype: 'integer', + name: 'startall-onboot-delay', + text: gettext('Start on boot delay'), + minValue: 0, + maxValue: 300, + labelWidth: 130, + deleteEmpty: true, + renderer: function(value) { + if (value === undefined) { + return Proxmox.Utils.defaultText; + } + + let secString = value === '1' ? gettext('Second') : gettext('Seconds'); + return `${value} ${secString}`; + }, + }, + { + xtype: 'text', + name: 'wakeonlan', + text: gettext('MAC address for Wake on LAN'), + vtype: 'MacAddress', + labelWidth: 150, + deleteEmpty: true, + renderer: function(value) { + if (value === undefined) { + return Proxmox.Utils.NoneText; + } + + return value; + }, + }, + ], +}); +Ext.define('PVE.pool.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.pvePoolConfig', + + onlineHelp: 'pveum_pools', + + initComponent: function() { + var me = this; + + var pool = me.pveSelNode.data.pool; + if (!pool) { + throw "no pool specified"; + } + + Ext.apply(me, { + title: Ext.String.format(gettext("Resource Pool") + ': ' + pool), + hstateid: 'pooltab', + items: [ + { + title: gettext('Summary'), + iconCls: 'fa fa-book', + xtype: 'pvePoolSummary', + itemId: 'summary', + }, + { + title: gettext('Members'), + xtype: 'pvePoolMembers', + iconCls: 'fa fa-th', + pool: pool, + itemId: 'members', + }, + { + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: '/pool/' + pool, + }, + ], + }); + + me.callParent(); + }, +}); +Ext.define('PVE.pool.StatusView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pvePoolStatusView'], + disabled: true, + + title: gettext('Status'), + cwidth1: 150, + interval: 30000, + //height: 195, + initComponent: function() { + var me = this; + + var pool = me.pveSelNode.data.pool; + if (!pool) { + throw "no pool specified"; + } + + var rows = { + comment: { + header: gettext('Comment'), + renderer: Ext.String.htmlEncode, + required: true, + }, + }; + + Ext.apply(me, { + url: "/api2/json/pools/" + pool, + rows: rows, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.pool.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pvePoolSummary', + + initComponent: function() { + var me = this; + + var pool = me.pveSelNode.data.pool; + if (!pool) { + throw "no pool specified"; + } + + var statusview = Ext.create('PVE.pool.StatusView', { + pveSelNode: me.pveSelNode, + style: 'padding-top:0px', + }); + + var rstore = statusview.rstore; + + Ext.apply(me, { + autoScroll: true, + bodyStyle: 'padding:10px', + defaults: { + style: 'padding-top:10px', + width: 800, + }, + items: [statusview], + }); + + me.on('activate', rstore.startUpdate); + me.on('destroy', rstore.stopUpdate); + + me.callParent(); + }, +}); +Ext.define('PVE.window.IPInfo', { + extend: 'Ext.window.Window', + width: 600, + title: gettext('Guest Agent Network Information'), + height: 300, + layout: { + type: 'fit', + }, + modal: true, + items: [ + { + xtype: 'grid', + store: {}, + emptyText: gettext('No network information'), + columns: [ + { + dataIndex: 'name', + text: gettext('Name'), + flex: 3, + }, + { + dataIndex: 'hardware-address', + text: gettext('MAC address'), + width: 140, + }, + { + dataIndex: 'ip-addresses', + text: gettext('IP address'), + align: 'right', + flex: 4, + renderer: function(val) { + if (!Ext.isArray(val)) { + return ''; + } + var ips = []; + val.forEach(function(ip) { + var addr = ip['ip-address']; + var pref = ip.prefix; + if (addr && pref) { + ips.push(addr + '/' + pref); + } + }); + return ips.join('
'); + }, + }, + ], + }, + ], +}); + +Ext.define('PVE.qemu.AgentIPView', { + extend: 'Ext.container.Container', + xtype: 'pveAgentIPView', + + layout: { + type: 'hbox', + align: 'top', + }, + + nics: [], + + items: [ + { + xtype: 'box', + html: ' IPs', + }, + { + xtype: 'container', + flex: 1, + layout: { + type: 'vbox', + align: 'right', + pack: 'end', + }, + items: [ + { + xtype: 'label', + flex: 1, + itemId: 'ipBox', + style: { + 'text-align': 'right', + }, + }, + { + xtype: 'button', + itemId: 'moreBtn', + hidden: true, + ui: 'default-toolbar', + handler: function(btn) { + let view = this.up('pveAgentIPView'); + + var win = Ext.create('PVE.window.IPInfo'); + win.down('grid').getStore().setData(view.nics); + win.show(); + }, + text: gettext('More'), + }, + ], + }, + ], + + getDefaultIps: function(nics) { + var me = this; + var ips = []; + nics.forEach(function(nic) { + if (nic['hardware-address'] && + nic['hardware-address'] !== '00:00:00:00:00:00' && + nic['hardware-address'] !== '0:0:0:0:0:0') { + var nic_ips = nic['ip-addresses'] || []; + nic_ips.forEach(function(ip) { + var p = ip['ip-address']; + // show 2 ips at maximum + if (ips.length < 2) { + ips.push(p); + } + }); + } + }); + + return ips; + }, + + startIPStore: function(store, records, success) { + var me = this; + let agentRec = store.getById('agent'); + let state = store.getById('status'); + + me.agent = agentRec && agentRec.data.value === 1; + me.running = state && state.data.value === 'running'; + + var caps = Ext.state.Manager.get('GuiCap'); + + if (!caps.vms['VM.Monitor']) { + var errorText = gettext("Requires '{0}' Privileges"); + me.updateStatus(false, Ext.String.format(errorText, 'VM.Monitor')); + return; + } + + if (me.agent && me.running && me.ipStore.isStopped) { + me.ipStore.startUpdate(); + } else if (me.ipStore.isStopped) { + me.updateStatus(); + } + }, + + updateStatus: function(unsuccessful, defaulttext) { + var me = this; + var text = defaulttext || gettext('No network information'); + var more = false; + if (unsuccessful) { + text = gettext('Guest Agent not running'); + } else if (me.agent && me.running) { + if (Ext.isArray(me.nics) && me.nics.length) { + more = true; + var ips = me.getDefaultIps(me.nics); + if (ips.length !== 0) { + text = ips.join('
'); + } + } else if (me.nics && me.nics.error) { + text = Ext.String.format(text, me.nics.error.desc); + } + } else if (me.agent) { + text = gettext('Guest Agent not running'); + } else { + text = gettext('No Guest Agent configured'); + } + + var ipBox = me.down('#ipBox'); + ipBox.update(text); + + var moreBtn = me.down('#moreBtn'); + moreBtn.setVisible(more); + }, + + initComponent: function() { + var me = this; + + if (!me.rstore) { + throw 'rstore not given'; + } + + if (!me.pveSelNode) { + throw 'pveSelNode not given'; + } + + var nodename = me.pveSelNode.data.node; + var vmid = me.pveSelNode.data.vmid; + + me.ipStore = Ext.create('Proxmox.data.UpdateStore', { + interval: 10000, + storeid: 'pve-qemu-agent-' + vmid, + method: 'POST', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + nodename + '/qemu/' + vmid + '/agent/network-get-interfaces', + }, + }); + + me.callParent(); + + me.mon(me.ipStore, 'load', function(store, records, success) { + if (records && records.length) { + me.nics = records[0].data.result; + } else { + me.nics = undefined; + } + me.updateStatus(!success); + }); + + me.on('destroy', me.ipStore.stopUpdate, me.ipStore); + + // if we already have info about the vm, use it immediately + if (me.rstore.getCount()) { + me.startIPStore(me.rstore, me.rstore.getData(), false); + } + + // check if the guest agent is there on every statusstore load + me.mon(me.rstore, 'load', me.startIPStore, me); + }, +}); +Ext.define('PVE.qemu.AudioInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveAudioInputPanel', + + // FIXME: enable once we bumped doc-gen so this ref is included + //onlineHelp: 'qm_audio_device', + + onGetValues: function(values) { + var ret = PVE.Parser.printPropertyString(values); + if (ret === '') { + return { + 'delete': 'audio0', + }; + } + return { + audio0: ret, + }; + }, + + items: [{ + name: 'device', + xtype: 'proxmoxKVComboBox', + value: 'ich9-intel-hda', + fieldLabel: gettext('Audio Device'), + comboItems: [ + ['ich9-intel-hda', 'ich9-intel-hda'], + ['intel-hda', 'intel-hda'], + ['AC97', 'AC97'], + ], + }, { + name: 'driver', + xtype: 'proxmoxKVComboBox', + value: 'spice', + fieldLabel: gettext('Backend Driver'), + comboItems: [ + ['spice', 'SPICE'], + ['none', `${Proxmox.Utils.NoneText} (${gettext('Dummy Device')})`], + ], + }], +}); + +Ext.define('PVE.qemu.AudioEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + subject: gettext('Audio Device'), + + items: [{ + xtype: 'pveAudioInputPanel', + }], + + initComponent: function() { + var me = this; + + me.callParent(); + + me.load({ + success: function(response) { + me.vmconfig = response.result.data; + + var audio0 = me.vmconfig.audio0; + if (audio0) { + me.setValues(PVE.Parser.parsePropertyString(audio0)); + } + }, + }); + }, +}); +Ext.define('pve-boot-order-entry', { + extend: 'Ext.data.Model', + fields: [ + { name: 'name', type: 'string' }, + { name: 'enabled', type: 'bool' }, + { name: 'desc', type: 'string' }, + ], +}); + +Ext.define('PVE.qemu.BootOrderPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuBootOrderPanel', + + onlineHelp: 'qm_bootorder', + + vmconfig: {}, // store loaded vm config + store: undefined, + + inUpdate: false, + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + let me = this; + + let grid = me.lookup('grid'); + let marker = me.lookup('marker'); + let emptyWarning = me.lookup('emptyWarning'); + + marker.originalValue = undefined; + + view.store = Ext.create('Ext.data.Store', { + model: 'pve-boot-order-entry', + listeners: { + update: function() { + this.commitChanges(); + let val = view.calculateValue(); + if (marker.originalValue === undefined) { + marker.originalValue = val; + } + view.inUpdate = true; + marker.setValue(val); + view.inUpdate = false; + marker.checkDirty(); + emptyWarning.setHidden(val !== ''); + grid.getView().refresh(); + }, + }, + }); + grid.setStore(view.store); + }, + }, + + isCloudinit: (v) => v.match(/media=cdrom/) && v.match(/[:/]vm-\d+-cloudinit/), + + isDisk: function(value) { + return PVE.Utils.bus_match.test(value); + }, + + isBootdev: function(dev, value) { + return (this.isDisk(dev) && !this.isCloudinit(value)) || + (/^net\d+/).test(dev) || + (/^hostpci\d+/).test(dev) || + ((/^usb\d+/).test(dev) && !(/spice/).test(value)); + }, + + setVMConfig: function(vmconfig) { + let me = this; + me.vmconfig = vmconfig; + + me.store.removeAll(); + + let boot = PVE.Parser.parsePropertyString(me.vmconfig.boot, "legacy"); + + let bootorder = []; + if (boot.order) { + bootorder = boot.order.split(';').map(dev => ({ name: dev, enabled: true })); + } else if (!(/^\s*$/).test(me.vmconfig.boot)) { + // legacy style, transform to new bootorder + let order = boot.legacy || 'cdn'; + let bootdisk = me.vmconfig.bootdisk || undefined; + + // get the first 4 characters (acdn) + // ignore the rest (there should never be more than 4) + let orderList = order.split('').slice(0, 4); + + // build bootdev list + for (let i = 0; i < orderList.length; i++) { + let list = []; + if (orderList[i] === 'c') { + if (bootdisk !== undefined && me.vmconfig[bootdisk]) { + list.push(bootdisk); + } + } else if (orderList[i] === 'd') { + Ext.Object.each(me.vmconfig, function(key, value) { + if (me.isDisk(key) && value.match(/media=cdrom/) && !me.isCloudinit(value)) { + list.push(key); + } + }); + } else if (orderList[i] === 'n') { + Ext.Object.each(me.vmconfig, function(key, value) { + if ((/^net\d+/).test(key)) { + list.push(key); + } + }); + } + + // Object.each iterates in random order, sort alphabetically + list.sort(); + list.forEach(dev => bootorder.push({ name: dev, enabled: true })); + } + } + + // add disabled devices as well + let disabled = []; + Ext.Object.each(me.vmconfig, function(key, value) { + if (me.isBootdev(key, value) && + !Ext.Array.some(bootorder, x => x.name === key)) { + disabled.push(key); + } + }); + disabled.sort(); + disabled.forEach(dev => bootorder.push({ name: dev, enabled: false })); + + // add descriptions + bootorder.forEach(entry => { + entry.desc = me.vmconfig[entry.name]; + }); + + me.store.insert(0, bootorder); + me.store.fireEvent("update"); + }, + + calculateValue: function() { + let me = this; + return me.store.getData().items + .filter(x => x.data.enabled) + .map(x => x.data.name) + .join(';'); + }, + + onGetValues: function() { + let me = this; + // Note: we allow an empty value, so no 'delete' option + let val = { order: me.calculateValue() }; + let res = { boot: PVE.Parser.printPropertyString(val) }; + return res; + }, + + items: [ + { + xtype: 'grid', + reference: 'grid', + margin: '0 0 5 0', + minHeight: 150, + defaults: { + sortable: false, + hideable: false, + draggable: false, + }, + columns: [ + { + header: '#', + flex: 4, + renderer: (value, metaData, record, rowIndex) => { + let dragHandle = ""; + let idx = (rowIndex + 1).toString(); + if (record.get('enabled')) { + return dragHandle + idx; + } else { + return dragHandle + "" + idx + ""; + } + }, + }, + { + xtype: 'checkcolumn', + header: gettext('Enabled'), + dataIndex: 'enabled', + flex: 4, + }, + { + header: gettext('Device'), + dataIndex: 'name', + flex: 6, + renderer: (value, metaData, record, rowIndex) => { + let desc = record.get('desc'); + + let icon = '', iconCls; + if (value.match(/^net\d+$/)) { + iconCls = 'exchange'; + } else if (desc.match(/media=cdrom/)) { + metaData.tdCls = 'pve-itype-icon-cdrom'; + } else { + iconCls = 'hdd-o'; + } + if (iconCls !== undefined) { + metaData.tdCls += 'pve-itype-fa'; + icon = ``; + } + + return icon + value; + }, + }, + { + header: gettext('Description'), + dataIndex: 'desc', + flex: 20, + }, + ], + viewConfig: { + plugins: { + ptype: 'gridviewdragdrop', + dragText: gettext('Drag and drop to reorder'), + }, + }, + listeners: { + drop: function() { + // doesn't fire automatically on reorder + this.getStore().fireEvent("update"); + }, + }, + }, + { + xtype: 'component', + html: gettext('Drag and drop to reorder'), + }, + { + xtype: 'displayfield', + reference: 'emptyWarning', + userCls: 'pmx-hint', + value: gettext('Warning: No devices selected, the VM will probably not boot!'), + }, + { + // for dirty marking and 'reset' function + xtype: 'field', + reference: 'marker', + hidden: true, + setValue: function(val) { + let me = this; + let panel = me.up('pveQemuBootOrderPanel'); + + // on form reset, go back to original state + if (!panel.inUpdate) { + panel.setVMConfig(panel.vmconfig); + } + + // not a subclass, so no callParent; just do it manually + me.setRawValue(me.valueToRaw(val)); + return me.mixins.field.setValue.call(me, val); + }, + }, + ], +}); + +Ext.define('PVE.qemu.BootOrderEdit', { + extend: 'Proxmox.window.Edit', + + items: [{ + xtype: 'pveQemuBootOrderPanel', + itemId: 'inputpanel', + }], + + subject: gettext('Boot Order'), + width: 640, + + initComponent: function() { + let me = this; + me.callParent(); + me.load({ + success: ({ result }) => me.down('#inputpanel').setVMConfig(result.data), + }); + }, +}); +Ext.define('PVE.qemu.CDInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuCDInputPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + var confid = me.confid || values.controller + values.deviceid; + + me.drive.media = 'cdrom'; + if (values.mediaType === 'iso') { + me.drive.file = values.cdimage; + } else if (values.mediaType === 'cdrom') { + me.drive.file = 'cdrom'; + } else { + me.drive.file = 'none'; + } + + var params = {}; + + params[confid] = PVE.Parser.printQemuDrive(me.drive); + + return params; + }, + + setVMConfig: function(vmconfig) { + var me = this; + + if (me.bussel) { + me.bussel.setVMConfig(vmconfig, 'cdrom'); + } + }, + + setDrive: function(drive) { + var me = this; + + var values = {}; + if (drive.file === 'cdrom') { + values.mediaType = 'cdrom'; + } else if (drive.file === 'none') { + values.mediaType = 'none'; + } else { + values.mediaType = 'iso'; + var match = drive.file.match(/^([^:]+):/); + if (match) { + values.cdstorage = match[1]; + values.cdimage = drive.file; + } + } + + me.drive = drive; + + me.setValues(values); + }, + + setNodename: function(nodename) { + var me = this; + + me.cdstoragesel.setNodename(nodename); + me.cdfilesel.setStorage(undefined, nodename); + }, + + initComponent: function() { + var me = this; + + me.drive = {}; + + var items = []; + + if (!me.confid) { + me.bussel = Ext.create('PVE.form.ControllerSelector', { + withVirtIO: false, + }); + items.push(me.bussel); + } + + items.push({ + xtype: 'radiofield', + name: 'mediaType', + inputValue: 'iso', + boxLabel: gettext('Use CD/DVD disc image file (iso)'), + checked: true, + listeners: { + change: function(f, value) { + if (!me.rendered) { + return; + } + me.down('field[name=cdstorage]').setDisabled(!value); + var cdImageField = me.down('field[name=cdimage]'); + cdImageField.setDisabled(!value); + if (value) { + cdImageField.validate(); + } else { + cdImageField.reset(); + } + }, + }, + }); + + me.cdfilesel = Ext.create('PVE.form.FileSelector', { + name: 'cdimage', + nodename: me.nodename, + storageContent: 'iso', + fieldLabel: gettext('ISO image'), + labelAlign: 'right', + allowBlank: false, + }); + + me.cdstoragesel = Ext.create('PVE.form.StorageSelector', { + name: 'cdstorage', + nodename: me.nodename, + fieldLabel: gettext('Storage'), + labelAlign: 'right', + storageContent: 'iso', + allowBlank: false, + autoSelect: me.insideWizard, + listeners: { + change: function(f, value) { + me.cdfilesel.setStorage(value); + }, + }, + }); + + items.push(me.cdstoragesel); + items.push(me.cdfilesel); + + items.push({ + xtype: 'radiofield', + name: 'mediaType', + inputValue: 'cdrom', + boxLabel: gettext('Use physical CD/DVD Drive'), + }); + + items.push({ + xtype: 'radiofield', + name: 'mediaType', + inputValue: 'none', + boxLabel: gettext('Do not use any media'), + }); + + me.items = items; + + me.callParent(); + }, +}); + +Ext.define('PVE.qemu.CDEdit', { + extend: 'Proxmox.window.Edit', + + width: 400, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.isCreate = !me.confid; + + var ipanel = Ext.create('PVE.qemu.CDInputPanel', { + confid: me.confid, + nodename: nodename, + }); + + Ext.applyIf(me, { + subject: 'CD/DVD Drive', + items: [ipanel], + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + var value = response.result.data[me.confid]; + var drive = PVE.Parser.parseQemuDrive(me.confid, value); + if (!drive) { + Ext.Msg.alert('Error', 'Unable to parse drive options'); + me.close(); + return; + } + ipanel.setDrive(drive); + } + }, + }); + }, +}); +Ext.define('PVE.qemu.CIDriveInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveCIDriveInputPanel', + + insideWizard: false, + + vmconfig: {}, // used to select usused disks + + onGetValues: function(values) { + var me = this; + + var drive = {}; + var params = {}; + drive.file = values.hdstorage + ":cloudinit"; + drive.format = values.diskformat; + params[values.controller + values.deviceid] = PVE.Parser.printQemuDrive(drive); + return params; + }, + + setNodename: function(nodename) { + var me = this; + me.down('#hdstorage').setNodename(nodename); + me.down('#hdimage').setStorage(undefined, nodename); + }, + + setVMConfig: function(config) { + var me = this; + me.down('#drive').setVMConfig(config, 'cdrom'); + }, + + initComponent: function() { + var me = this; + + me.drive = {}; + + me.items = [ + { + xtype: 'pveControllerSelector', + withVirtIO: false, + itemId: 'drive', + fieldLabel: gettext('CloudInit Drive'), + name: 'drive', + }, + { + xtype: 'pveDiskStorageSelector', + itemId: 'storselector', + storageContent: 'images', + nodename: me.nodename, + hideSize: true, + }, + ]; + me.callParent(); + }, +}); + +Ext.define('PVE.qemu.CIDriveEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCIDriveEdit', + + isCreate: true, + subject: gettext('CloudInit Drive'), + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.items = [{ + xtype: 'pveCIDriveInputPanel', + itemId: 'cipanel', + nodename: nodename, + }]; + + me.callParent(); + + me.load({ + success: function(response, opts) { + me.down('#cipanel').setVMConfig(response.result.data); + }, + }); + }, +}); +Ext.define('PVE.qemu.CloudInit', { + extend: 'Proxmox.grid.PendingObjectGrid', + xtype: 'pveCiPanel', + + onlineHelp: 'qm_cloud_init', + + tbar: [ + { + xtype: 'proxmoxButton', + disabled: true, + dangerous: true, + confirmMsg: function(rec) { + let view = this.up('grid'); + var warn = gettext('Are you sure you want to remove entry {0}'); + + var entry = rec.data.key; + var msg = Ext.String.format(warn, "'" + + view.renderKey(entry, {}, rec) + "'"); + + return msg; + }, + enableFn: function(record) { + let view = this.up('grid'); + var caps = Ext.state.Manager.get('GuiCap'); + if (view.rows[record.data.key].never_delete || + !caps.vms['VM.Config.Network']) { + return false; + } + + if (record.data.key === 'cipassword' && !record.data.value) { + return false; + } + return true; + }, + handler: function() { + let view = this.up('grid'); + let records = view.getSelection(); + if (!records || !records.length) { + return; + } + + var id = records[0].data.key; + var match = id.match(/^net(\d+)$/); + if (match) { + id = 'ipconfig' + match[1]; + } + + var params = {}; + params.delete = id; + Proxmox.Utils.API2Request({ + url: view.baseurl + '/config', + waitMsgTarget: view, + method: 'PUT', + params: params, + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + callback: function() { + view.reload(); + }, + }); + }, + text: gettext('Remove'), + }, + { + xtype: 'proxmoxButton', + disabled: true, + enableFn: function(rec) { + let view = this.up('pveCiPanel'); + return !!view.rows[rec.data.key].editor; + }, + handler: function() { + let view = this.up('grid'); + view.run_editor(); + }, + text: gettext('Edit'), + }, + '-', + { + xtype: 'button', + itemId: 'savebtn', + text: gettext('Regenerate Image'), + handler: function() { + let view = this.up('grid'); + var eject_params = {}; + var insert_params = {}; + let disk = PVE.Parser.parseQemuDrive(view.ciDriveId, view.ciDrive); + var storage = ''; + var stormatch = disk.file.match(/^([^:]+):/); + if (stormatch) { + storage = stormatch[1]; + } + eject_params[view.ciDriveId] = 'none,media=cdrom'; + insert_params[view.ciDriveId] = storage + ':cloudinit'; + + var failure = function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }; + + Proxmox.Utils.API2Request({ + url: view.baseurl + '/config', + waitMsgTarget: view, + method: 'PUT', + params: eject_params, + failure: failure, + callback: function() { + Proxmox.Utils.API2Request({ + url: view.baseurl + '/config', + waitMsgTarget: view, + method: 'PUT', + params: insert_params, + failure: failure, + callback: function() { + view.reload(); + }, + }); + }, + }); + }, + }, + ], + + border: false, + + set_button_status: function(rstore, records, success) { + if (!success || records.length < 1) { + return; + } + var me = this; + var found; + records.forEach(function(record) { + if (found) { + return; + } + var id = record.data.key; + var value = record.data.value; + var ciregex = new RegExp("vm-" + me.pveSelNode.data.vmid + "-cloudinit"); + if (id.match(/^(ide|scsi|sata)\d+$/) && ciregex.test(value)) { + found = id; + me.ciDriveId = found; + me.ciDrive = value; + } + }); + + me.down('#savebtn').setDisabled(!found); + me.setDisabled(!found); + if (!found) { + me.getView().mask(gettext('No CloudInit Drive found'), ['pve-static-mask']); + } else { + me.getView().unmask(); + } + }, + + renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var rowdef = rows[key] || {}; + + var icon = ""; + if (rowdef.iconCls) { + icon = ' '; + } + return icon + (rowdef.header || key); + }, + + listeners: { + activate: function() { + var me = this; + me.rstore.startUpdate(); + }, + itemdblclick: function() { + var me = this; + me.run_editor(); + }, + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + var caps = Ext.state.Manager.get('GuiCap'); + me.baseurl = '/api2/extjs/nodes/' + nodename + '/qemu/' + vmid; + me.url = me.baseurl + '/pending'; + me.editorConfig.url = me.baseurl + '/config'; + me.editorConfig.pveSelNode = me.pveSelNode; + + let caps_ci = caps.vms['VM.Config.Cloudinit'] || caps.vms['VM.Config.Network']; + /* editor is string and object */ + me.rows = { + ciuser: { + header: gettext('User'), + iconCls: 'fa fa-user', + never_delete: true, + defaultValue: '', + editor: caps_ci ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('User'), + items: [ + { + xtype: 'proxmoxtextfield', + deleteEmpty: true, + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('User'), + name: 'ciuser', + }, + ], + } : undefined, + renderer: function(value) { + return value || Proxmox.Utils.defaultText; + }, + }, + cipassword: { + header: gettext('Password'), + iconCls: 'fa fa-unlock', + defaultValue: '', + editor: caps_ci ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Password'), + items: [ + { + xtype: 'proxmoxtextfield', + inputType: 'password', + deleteEmpty: true, + emptyText: Proxmox.Utils.noneText, + fieldLabel: gettext('Password'), + name: 'cipassword', + }, + ], + } : undefined, + renderer: function(value) { + return value || Proxmox.Utils.noneText; + }, + }, + searchdomain: { + header: gettext('DNS domain'), + iconCls: 'fa fa-globe', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + never_delete: true, + defaultValue: gettext('use host settings'), + }, + nameserver: { + header: gettext('DNS servers'), + iconCls: 'fa fa-globe', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + never_delete: true, + defaultValue: gettext('use host settings'), + }, + sshkeys: { + header: gettext('SSH public key'), + iconCls: 'fa fa-key', + editor: caps_ci ? 'PVE.qemu.SSHKeyEdit' : undefined, + never_delete: true, + renderer: function(value) { + value = decodeURIComponent(value); + var keys = value.split('\n'); + var text = []; + keys.forEach(function(key) { + if (key.length) { + let res = PVE.Parser.parseSSHKey(key); + if (res) { + key = Ext.String.htmlEncode(res.comment); + if (res.options) { + key += ' (' + gettext('with options') + ')'; + } + text.push(key); + return; + } + // Most likely invalid at this point, so just stick to + // the old value. + text.push(Ext.String.htmlEncode(key)); + } + }); + if (text.length) { + return text.join('
'); + } else { + return Proxmox.Utils.noneText; + } + }, + defaultValue: '', + }, + }; + var i; + var ipconfig_renderer = function(value, md, record, ri, ci, store, pending) { + var id = record.data.key; + var match = id.match(/^net(\d+)$/); + var val = ''; + if (match) { + val = me.getObjectValue('ipconfig'+match[1], '', pending); + } + return val; + }; + for (i = 0; i < 32; i++) { + // we want to show an entry for every network device + // even if it is empty + me.rows['net' + i.toString()] = { + multiKey: ['ipconfig' + i.toString(), 'net' + i.toString()], + header: gettext('IP Config') + ' (net' + i.toString() +')', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.IPConfigEdit' : undefined, + iconCls: 'fa fa-exchange', + renderer: ipconfig_renderer, + }; + me.rows['ipconfig' + i.toString()] = { + visible: false, + }; + } + + PVE.Utils.forEachBus(['ide', 'scsi', 'sata'], function(type, id) { + me.rows[type+id] = { + visible: false, + }; + }); + me.callParent(); + me.mon(me.rstore, 'load', me.set_button_status, me); + }, +}); +Ext.define('PVE.qemu.CmdMenu', { + extend: 'Ext.menu.Menu', + + showSeparator: false, + initComponent: function() { + let me = this; + + let info = me.pveSelNode.data; + if (!info.node) { + throw "no node name specified"; + } + if (!info.vmid) { + throw "no VM ID specified"; + } + + let vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: `/nodes/${info.node}/${info.type}/${info.vmid}/status/${cmd}`, + method: 'POST', + failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + }); + }; + let confirmedVMCommand = (cmd, params, confirmTask) => { + let task = confirmTask || `qm${cmd}`; + let msg = Proxmox.Utils.format_task_description(task, info.vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, btn => { + if (btn === 'yes') { + vm_command(cmd, params); + } + }); + }; + + let caps = Ext.state.Manager.get('GuiCap'); + let standalone = PVE.data.ResourceStore.getNodes().length < 2; + + let running = false, stopped = true, suspended = false; + switch (info.status) { + case 'running': + running = true; + stopped = false; + break; + case 'suspended': + stopped = false; + suspended = true; + break; + case 'paused': + stopped = false; + suspended = true; + break; + default: break; + } + + me.title = "VM " + info.vmid; + + me.items = [ + { + text: gettext('Start'), + iconCls: 'fa fa-fw fa-play', + hidden: running || suspended, + disabled: running || suspended, + handler: () => vm_command('start'), + }, + { + text: gettext('Pause'), + iconCls: 'fa fa-fw fa-pause', + hidden: stopped || suspended, + disabled: stopped || suspended, + handler: () => confirmedVMCommand('suspend', undefined, 'qmpause'), + }, + { + text: gettext('Hibernate'), + iconCls: 'fa fa-fw fa-download', + hidden: stopped || suspended, + disabled: stopped || suspended, + tooltip: gettext('Suspend to disk'), + handler: () => confirmedVMCommand('suspend', { todisk: 1 }), + }, + { + text: gettext('Resume'), + iconCls: 'fa fa-fw fa-play', + hidden: !suspended, + handler: () => vm_command('resume'), + }, + { + text: gettext('Shutdown'), + iconCls: 'fa fa-fw fa-power-off', + disabled: stopped || suspended, + handler: () => confirmedVMCommand('shutdown'), + }, + { + text: gettext('Stop'), + iconCls: 'fa fa-fw fa-stop', + disabled: stopped, + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'), + handler: () => confirmedVMCommand('stop'), + }, + { + text: gettext('Reboot'), + iconCls: 'fa fa-fw fa-refresh', + disabled: stopped, + tooltip: Ext.String.format(gettext('Reboot {0}'), 'VM'), + handler: () => confirmedVMCommand('reboot'), + }, + { + xtype: 'menuseparator', + hidden: (standalone || !caps.vms['VM.Migrate']) && !caps.vms['VM.Allocate'] && !caps.vms['VM.Clone'], + }, + { + text: gettext('Migrate'), + iconCls: 'fa fa-fw fa-send-o', + hidden: standalone || !caps.vms['VM.Migrate'], + handler: function() { + Ext.create('PVE.window.Migrate', { + vmtype: 'qemu', + nodename: info.node, + vmid: info.vmid, + autoShow: true, + }); + }, + }, + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: !caps.vms['VM.Clone'], + handler: () => PVE.window.Clone.wrap(info.node, info.vmid, me.isTemplate, 'qemu'), + }, + { + text: gettext('Convert to template'), + iconCls: 'fa fa-fw fa-file-o', + hidden: !caps.vms['VM.Allocate'], + handler: function() { + let msg = Proxmox.Utils.format_task_description('qmtemplate', info.vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, btn => { + if (btn === 'yes') { + Proxmox.Utils.API2Request({ + url: `/nodes/${info.node}/qemu/${info.vmid}/template`, + method: 'POST', + failure: (response, opts) => Ext.Msg.alert('Error', response.htmlStatus), + }); + } + }); + }, + }, + { xtype: 'menuseparator' }, + { + text: gettext('Console'), + iconCls: 'fa fa-fw fa-terminal', + handler: function() { + Proxmox.Utils.API2Request({ + url: `/nodes/${info.node}/qemu/${info.vmid}/status/current`, + failure: (response, opts) => Ext.Msg.alert('Error', response.htmlStatus), + success: function({ result: { data } }, opts) { + PVE.Utils.openDefaultConsoleWindow( + { + spice: data.spice, + xtermjs: data.serial, + }, + 'kvm', + info.vmid, + info.node, + info.name, + ); + }, + }); + }, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.qemu.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.qemu.Config', + + onlineHelp: 'chapter_virtual_machines', + userCls: 'proxmox-tags-full', + + initComponent: function() { + var me = this; + var vm = me.pveSelNode.data; + + var nodename = vm.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = vm.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var template = !!vm.template; + + var running = !!vm.uptime; + + var caps = Ext.state.Manager.get('GuiCap'); + + var base_url = '/nodes/' + nodename + "/qemu/" + vmid; + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json' + base_url + '/status/current', + interval: 1000, + }); + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: base_url + '/status/' + cmd, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + }); + }; + + var resumeBtn = Ext.create('Ext.Button', { + text: gettext('Resume'), + disabled: !caps.vms['VM.PowerMgmt'], + hidden: true, + handler: function() { + vm_command('resume'); + }, + iconCls: 'fa fa-play', + }); + + var startBtn = Ext.create('Ext.Button', { + text: gettext('Start'), + disabled: !caps.vms['VM.PowerMgmt'] || running, + hidden: template, + handler: function() { + vm_command('start'); + }, + iconCls: 'fa fa-play', + }); + + var migrateBtn = Ext.create('Ext.Button', { + text: gettext('Migrate'), + disabled: !caps.vms['VM.Migrate'], + hidden: PVE.data.ResourceStore.getNodes().length < 2, + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: 'qemu', + nodename: nodename, + vmid: vmid, + }); + win.show(); + }, + iconCls: 'fa fa-send-o', + }); + + var moreBtn = Ext.create('Proxmox.button.Button', { + text: gettext('More'), + menu: { + items: [ + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: !caps.vms['VM.Clone'], + handler: function() { + PVE.window.Clone.wrap(nodename, vmid, template, 'qemu'); + }, + }, + { + text: gettext('Convert to template'), + disabled: template, + xtype: 'pveMenuItem', + iconCls: 'fa fa-fw fa-file-o', + hidden: !caps.vms['VM.Allocate'], + confirmMsg: Proxmox.Utils.format_task_description('qmtemplate', vmid), + handler: function() { + Proxmox.Utils.API2Request({ + url: base_url + '/template', + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + }); + }, + }, + { + iconCls: 'fa fa-heartbeat ', + hidden: !caps.nodes['Sys.Console'], + text: gettext('Manage HA'), + handler: function() { + var ha = vm.hastate; + Ext.create('PVE.ha.VMResourceEdit', { + vmid: vmid, + isCreate: !ha || ha === 'unmanaged', + }).show(); + }, + }, + { + text: gettext('Remove'), + itemId: 'removeBtn', + disabled: !caps.vms['VM.Allocate'], + handler: function() { + Ext.create('PVE.window.SafeDestroyGuest', { + url: base_url, + item: { type: 'VM', id: vmid }, + taskName: 'qmdestroy', + }).show(); + }, + iconCls: 'fa fa-trash-o', + }, + ], +}, + }); + + var shutdownBtn = Ext.create('PVE.button.Split', { + text: gettext('Shutdown'), + disabled: !caps.vms['VM.PowerMgmt'] || !running, + hidden: template, + confirmMsg: Proxmox.Utils.format_task_description('qmshutdown', vmid), + handler: function() { + vm_command('shutdown'); + }, + menu: { + items: [{ + text: gettext('Reboot'), + disabled: !caps.vms['VM.PowerMgmt'], + tooltip: Ext.String.format(gettext('Shutdown, apply pending changes and reboot {0}'), 'VM'), + confirmMsg: Proxmox.Utils.format_task_description('qmreboot', vmid), + handler: function() { + vm_command("reboot"); + }, + iconCls: 'fa fa-refresh', + }, { + text: gettext('Pause'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('qmpause', vmid), + handler: function() { + vm_command("suspend"); + }, + iconCls: 'fa fa-pause', + }, { + text: gettext('Hibernate'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('qmsuspend', vmid), + tooltip: gettext('Suspend to disk'), + handler: function() { + vm_command("suspend", { todisk: 1 }); + }, + iconCls: 'fa fa-download', + }, { + text: gettext('Stop'), + disabled: !caps.vms['VM.PowerMgmt'], + dangerous: true, + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'), + confirmMsg: Proxmox.Utils.format_task_description('qmstop', vmid), + handler: function() { + vm_command("stop", { timeout: 30 }); + }, + iconCls: 'fa fa-stop', + }, { + text: gettext('Reset'), + disabled: !caps.vms['VM.PowerMgmt'], + tooltip: Ext.String.format(gettext('Reset {0} immediately'), 'VM'), + confirmMsg: Proxmox.Utils.format_task_description('qmreset', vmid), + handler: function() { + vm_command("reset"); + }, + iconCls: 'fa fa-bolt', + }], + }, + iconCls: 'fa fa-power-off', + }); + + var consoleBtn = Ext.create('PVE.button.ConsoleButton', { + disabled: !caps.vms['VM.Console'], + hidden: template, + consoleType: 'kvm', + // disable spice/xterm for default action until status api call succeeded + enableSpice: false, + enableXtermjs: false, + consoleName: vm.name, + nodename: nodename, + vmid: vmid, + }); + + var statusTxt = Ext.create('Ext.toolbar.TextItem', { + data: { + lock: undefined, + }, + tpl: [ + '', + ' ({lock})', + '', + ], + }); + + let tagsContainer = Ext.create('PVE.panel.TagEditContainer', { + tags: vm.tags, + canEdit: !!caps.vms['VM.Config.Options'], + listeners: { + change: function(tags) { + Proxmox.Utils.API2Request({ + url: base_url + '/config', + method: 'PUT', + params: { + tags, + }, + success: function() { + me.statusStore.load(); + }, + failure: function(response) { + Ext.Msg.alert('Error', response.htmlStatus); + me.statusStore.load(); + }, + }); + }, + }, + }); + + let vm_text = `${vm.vmid} (${vm.name})`; + + Ext.apply(me, { + title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm_text, nodename), + hstateid: 'kvmtab', + tbarSpacing: false, + tbar: [statusTxt, tagsContainer, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn], + defaults: { statusStore: me.statusStore }, + items: [ + { + title: gettext('Summary'), + xtype: 'pveGuestSummary', + iconCls: 'fa fa-book', + itemId: 'summary', + }, + ], + }); + + if (caps.vms['VM.Console'] && !template) { + me.items.push({ + title: gettext('Console'), + itemId: 'console', + iconCls: 'fa fa-terminal', + xtype: 'pveNoVncConsole', + vmid: vmid, + consoleType: 'kvm', + nodename: nodename, + }); + } + + me.items.push( + { + title: gettext('Hardware'), + itemId: 'hardware', + iconCls: 'fa fa-desktop', + xtype: 'PVE.qemu.HardwareView', + }, + { + title: 'Cloud-Init', + itemId: 'cloudinit', + iconCls: 'fa fa-cloud', + xtype: 'pveCiPanel', + }, + { + title: gettext('Options'), + iconCls: 'fa fa-gear', + itemId: 'options', + xtype: 'PVE.qemu.Options', + }, + { + title: gettext('Task History'), + itemId: 'tasks', + xtype: 'proxmoxNodeTasks', + iconCls: 'fa fa-list-alt', + nodename: nodename, + preFilter: { + vmid, + }, + }, + ); + + if (caps.vms['VM.Monitor'] && !template) { + me.items.push({ + title: gettext('Monitor'), + iconCls: 'fa fa-eye', + itemId: 'monitor', + xtype: 'pveQemuMonitor', + }); + } + + if (caps.vms['VM.Backup']) { + me.items.push({ + title: gettext('Backup'), + iconCls: 'fa fa-floppy-o', + xtype: 'pveBackupView', + itemId: 'backup', + }, + { + title: gettext('Replication'), + iconCls: 'fa fa-retweet', + xtype: 'pveReplicaView', + itemId: 'replication', + }); + } + + if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback'] || + caps.vms['VM.Audit']) && !template) { + me.items.push({ + title: gettext('Snapshots'), + iconCls: 'fa fa-history', + type: 'qemu', + xtype: 'pveGuestSnapshotTree', + itemId: 'snapshot', + }); + } + + if (caps.vms['VM.Console']) { + me.items.push( + { + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + iconCls: 'fa fa-shield', + allow_iface: true, + base_url: base_url + '/firewall/rules', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall', + }, + { + xtype: 'pveFirewallOptions', + groups: ['firewall'], + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_vm_container_configuration', + title: gettext('Options'), + base_url: base_url + '/firewall/options', + fwtype: 'vm', + itemId: 'firewall-options', + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: base_url + '/firewall/aliases', + itemId: 'firewall-aliases', + }, + { + xtype: 'pveIPSet', + title: gettext('IPSet'), + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: base_url + '/firewall/ipset', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall-ipset', + }, + { + title: gettext('Log'), + groups: ['firewall'], + iconCls: 'fa fa-list', + onlineHelp: 'chapter_pve_firewall', + itemId: 'firewall-fwlog', + xtype: 'proxmoxLogView', + url: '/api2/extjs' + base_url + '/firewall/log', + }, + ); + } + + if (caps.vms['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: '/vms/' + vmid, + }); + } + + me.callParent(); + + var prevQMPStatus = 'unknown'; + me.mon(me.statusStore, 'load', function(s, records, success) { + var status; + var qmpstatus; + var spice = false; + var xtermjs = false; + var lock; + var rec; + + if (!success) { + status = qmpstatus = 'unknown'; + } else { + rec = s.data.get('status'); + status = rec ? rec.data.value : 'unknown'; + rec = s.data.get('qmpstatus'); + qmpstatus = rec ? rec.data.value : 'unknown'; + rec = s.data.get('template'); + template = rec ? rec.data.value : false; + rec = s.data.get('lock'); + lock = rec ? rec.data.value : undefined; + + spice = !!s.data.get('spice'); + xtermjs = !!s.data.get('serial'); + } + + rec = s.data.get('tags'); + tagsContainer.loadTags(rec?.data?.value); + + if (template) { + return; + } + + var resume = ['prelaunch', 'paused', 'suspended'].indexOf(qmpstatus) !== -1; + + if (resume || lock === 'suspended') { + startBtn.setVisible(false); + resumeBtn.setVisible(true); + } else { + startBtn.setVisible(true); + resumeBtn.setVisible(false); + } + + consoleBtn.setEnableSpice(spice); + consoleBtn.setEnableXtermJS(xtermjs); + + statusTxt.update({ lock: lock }); + + let guest_running = status === 'running' && + !(qmpstatus === "shutdown" || qmpstatus === "prelaunch"); + startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || template || guest_running); + + shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running'); + me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped'); + consoleBtn.setDisabled(template); + + let wasStopped = ['prelaunch', 'stopped', 'suspended'].indexOf(prevQMPStatus) !== -1; + if (wasStopped && qmpstatus === 'running') { + let con = me.down('#console'); + if (con) { + con.reload(); + } + } + + prevQMPStatus = qmpstatus; + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + }, +}); +Ext.define('PVE.qemu.CreateWizard', { + extend: 'PVE.window.Wizard', + alias: 'widget.pveQemuCreateWizard', + mixins: ['Proxmox.Mixin.CBind'], + + viewModel: { + data: { + nodename: '', + current: { + scsihw: '', + }, + }, + formulas: { + cgroupMode: function(get) { + const nodeInfo = PVE.data.ResourceStore.getNodes().find( + node => node.node === get('nodename'), + ); + return nodeInfo ? nodeInfo['cgroup-mode'] : 2; + }, + }, + }, + + cbindData: { + nodename: undefined, + }, + + subject: gettext('Virtual Machine'), + + items: [ + { + xtype: 'inputpanel', + title: gettext('General'), + onlineHelp: 'qm_general_settings', + column1: [ + { + xtype: 'pveNodeSelector', + name: 'nodename', + cbind: { + selectCurNode: '{!nodename}', + preferredValue: '{nodename}', + }, + bind: { + value: '{nodename}', + }, + fieldLabel: gettext('Node'), + allowBlank: false, + onlineValidator: true, + }, + { + xtype: 'pveGuestIDSelector', + name: 'vmid', + guestType: 'qemu', + value: '', + loadNextFreeID: true, + validateExists: false, + }, + { + xtype: 'textfield', + name: 'name', + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Name'), + allowBlank: true, + }, + ], + column2: [ + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true, + }, + ], + advancedColumn1: [ + { + xtype: 'proxmoxcheckbox', + name: 'onboot', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Start at boot'), + }, + ], + advancedColumn2: [ + { + xtype: 'textfield', + name: 'order', + defaultValue: '', + emptyText: 'any', + labelWidth: 120, + fieldLabel: gettext('Start/Shutdown order'), + }, + { + xtype: 'textfield', + name: 'up', + defaultValue: '', + emptyText: 'default', + labelWidth: 120, + fieldLabel: gettext('Startup delay'), + }, + { + xtype: 'textfield', + name: 'down', + defaultValue: '', + emptyText: 'default', + labelWidth: 120, + fieldLabel: gettext('Shutdown timeout'), + }, + ], + onGetValues: function(values) { + ['name', 'pool', 'onboot', 'agent'].forEach(function(field) { + if (!values[field]) { + delete values[field]; + } + }); + + var res = PVE.Parser.printStartup({ + order: values.order, + up: values.up, + down: values.down, + }); + + if (res) { + values.startup = res; + } + + delete values.order; + delete values.up; + delete values.down; + + return values; + }, + }, + { + xtype: 'container', + layout: 'hbox', + defaults: { + flex: 1, + padding: '0 10', + }, + title: gettext('OS'), + items: [ + { + xtype: 'pveQemuCDInputPanel', + bind: { + nodename: '{nodename}', + }, + confid: 'ide2', + insideWizard: true, + }, + { + xtype: 'pveQemuOSTypePanel', + insideWizard: true, + }, + ], + }, + { + xtype: 'pveQemuSystemPanel', + title: gettext('System'), + isCreate: true, + insideWizard: true, + }, + { + xtype: 'pveMultiHDPanel', + bind: { + nodename: '{nodename}', + }, + title: gettext('Disks'), + }, + { + xtype: 'pveQemuProcessorPanel', + insideWizard: true, + title: gettext('CPU'), + }, + { + xtype: 'pveQemuMemoryPanel', + insideWizard: true, + title: gettext('Memory'), + }, + { + xtype: 'pveQemuNetworkInputPanel', + bind: { + nodename: '{nodename}', + }, + title: gettext('Network'), + insideWizard: true, + }, + { + title: gettext('Confirm'), + layout: 'fit', + items: [ + { + xtype: 'grid', + store: { + model: 'KeyValue', + sorters: [{ + property: 'key', + direction: 'ASC', + }], + }, + columns: [ + { header: 'Key', width: 150, dataIndex: 'key' }, + { header: 'Value', flex: 1, dataIndex: 'value' }, + ], + }, + ], + dockedItems: [ + { + xtype: 'proxmoxcheckbox', + name: 'start', + dock: 'bottom', + margin: '5 0 0 0', + boxLabel: gettext('Start after created'), + }, + ], + listeners: { + show: function(panel) { + var kv = this.up('window').getValues(); + var data = []; + Ext.Object.each(kv, function(key, value) { + if (key === 'delete') { // ignore + return; + } + data.push({ key: key, value: value }); + }); + + var summarystore = panel.down('grid').getStore(); + summarystore.suspendEvents(); + summarystore.removeAll(); + summarystore.add(data); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh'); + }, + }, + onSubmit: function() { + var wizard = this.up('window'); + var kv = wizard.getValues(); + delete kv.delete; + + var nodename = kv.nodename; + delete kv.nodename; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/qemu', + waitMsgTarget: wizard, + method: 'POST', + params: kv, + success: function(response) { + wizard.close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, + }, + ], +}); + + +Ext.define('PVE.qemu.DisplayInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveDisplayInputPanel', + onlineHelp: 'qm_display', + + onGetValues: function(values) { + let ret = PVE.Parser.printPropertyString(values, 'type'); + if (ret === '') { + return { 'delete': 'vga' }; + } + return { vga: ret }; + }, + + items: [{ + name: 'type', + xtype: 'proxmoxKVComboBox', + value: '__default__', + deleteEmpty: false, + fieldLabel: gettext('Graphic card'), + comboItems: Object.entries(PVE.Utils.kvm_vga_drivers), + validator: function(v) { + let cfg = this.up('proxmoxWindowEdit').vmconfig || {}; + + if (v.match(/^serial\d+$/) && (!cfg[v] || cfg[v] !== 'socket')) { + let fmt = gettext("Serial interface '{0}' is not correctly configured."); + return Ext.String.format(fmt, v); + } + return true; + }, + listeners: { + change: function(cb, val) { + if (!val) { + return; + } + let memoryfield = this.up('panel').down('field[name=memory]'); + let disableMemoryField = false; + + if (val === "cirrus") { + memoryfield.setEmptyText("4"); + } else if (val === "std" || val.match(/^qxl\d?$/) || val === "vmware") { + memoryfield.setEmptyText("16"); + } else if (val.match(/^virtio/)) { + memoryfield.setEmptyText("256"); + } else if (val.match(/^(serial\d|none)$/)) { + memoryfield.setEmptyText("N/A"); + disableMemoryField = true; + } else { + console.debug("unexpected display type", val); + memoryfield.setEmptyText(Proxmox.Utils.defaultText); + } + memoryfield.setDisabled(disableMemoryField); + }, + }, + }, + { + xtype: 'proxmoxintegerfield', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Memory') + ' (MiB)', + minValue: 4, + maxValue: 512, + step: 4, + name: 'memory', + }], +}); + +Ext.define('PVE.qemu.DisplayEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + subject: gettext('Display'), + width: 350, + + items: [{ + xtype: 'pveDisplayInputPanel', + }], + + initComponent: function() { + let me = this; + + me.callParent(); + + me.load({ + success: function(response) { + me.vmconfig = response.result.data; + let vga = me.vmconfig.vga || '__default__'; + me.setValues(PVE.Parser.parsePropertyString(vga, 'type')); + }, + }); + }, +}); +/* 'change' property is assigned a string and then a function */ +Ext.define('PVE.qemu.HDInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuHDInputPanel', + onlineHelp: 'qm_hard_disk', + + insideWizard: false, + + unused: false, // ADD usused disk imaged + + vmconfig: {}, // used to select usused disks + + viewModel: { + data: { + isSCSI: false, + isVirtIO: false, + isSCSISingle: false, + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + onControllerChange: function(field) { + let me = this; + let vm = this.getViewModel(); + + let value = field.getValue(); + vm.set('isSCSI', value.match(/^scsi/)); + vm.set('isVirtIO', value.match(/^virtio/)); + + me.fireIdChange(); + }, + + fireIdChange: function() { + let view = this.getView(); + view.fireEvent('diskidchange', view, view.bussel.getConfId()); + }, + + control: { + 'field[name=controller]': { + change: 'onControllerChange', + afterrender: 'onControllerChange', + }, + 'field[name=deviceid]': { + change: 'fireIdChange', + }, + 'field[name=scsiController]': { + change: function(f, value) { + let vm = this.getViewModel(); + vm.set('isSCSISingle', value === 'virtio-scsi-single'); + }, + }, + }, + + init: function(view) { + var vm = this.getViewModel(); + if (view.isCreate) { + vm.set('isIncludedInBackup', true); + } + if (view.confid) { + vm.set('isSCSI', view.confid.match(/^scsi/)); + vm.set('isVirtIO', view.confid.match(/^virtio/)); + } + }, + }, + + onGetValues: function(values) { + var me = this; + + var params = {}; + var confid = me.confid || values.controller + values.deviceid; + + if (me.unused) { + me.drive.file = me.vmconfig[values.unusedId]; + confid = values.controller + values.deviceid; + } else if (me.isCreate) { + if (values.hdimage) { + me.drive.file = values.hdimage; + } else { + me.drive.file = values.hdstorage + ":" + values.disksize; + } + me.drive.format = values.diskformat; + } + + PVE.Utils.propertyStringSet(me.drive, !values.backup, 'backup', '0'); + PVE.Utils.propertyStringSet(me.drive, values.noreplicate, 'replicate', 'no'); + PVE.Utils.propertyStringSet(me.drive, values.discard, 'discard', 'on'); + PVE.Utils.propertyStringSet(me.drive, values.ssd, 'ssd', 'on'); + PVE.Utils.propertyStringSet(me.drive, values.iothread, 'iothread', 'on'); + PVE.Utils.propertyStringSet(me.drive, values.readOnly, 'ro', 'on'); + PVE.Utils.propertyStringSet(me.drive, values.cache, 'cache'); + PVE.Utils.propertyStringSet(me.drive, values.aio, 'aio'); + + ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr'].forEach(name => { + let burst_name = `${name}_max`; + PVE.Utils.propertyStringSet(me.drive, values[name], name); + PVE.Utils.propertyStringSet(me.drive, values[burst_name], burst_name); + }); + + params[confid] = PVE.Parser.printQemuDrive(me.drive); + + return params; + }, + + updateVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + me.bussel?.updateVMConfig(vmconfig); + }, + + setVMConfig: function(vmconfig) { + var me = this; + + me.vmconfig = vmconfig; + + if (me.bussel) { + me.bussel.setVMConfig(vmconfig); + me.scsiController.setValue(vmconfig.scsihw); + } + if (me.unusedDisks) { + var disklist = []; + Ext.Object.each(vmconfig, function(key, value) { + if (key.match(/^unused\d+$/)) { + disklist.push([key, value]); + } + }); + me.unusedDisks.store.loadData(disklist); + me.unusedDisks.setValue(me.confid); + } + }, + + setDrive: function(drive) { + var me = this; + + me.drive = drive; + + var values = {}; + var match = drive.file.match(/^([^:]+):/); + if (match) { + values.hdstorage = match[1]; + } + + values.hdimage = drive.file; + values.backup = PVE.Parser.parseBoolean(drive.backup, 1); + values.noreplicate = !PVE.Parser.parseBoolean(drive.replicate, 1); + values.diskformat = drive.format || 'raw'; + values.cache = drive.cache || '__default__'; + values.discard = drive.discard === 'on'; + values.ssd = PVE.Parser.parseBoolean(drive.ssd); + values.iothread = PVE.Parser.parseBoolean(drive.iothread); + values.readOnly = PVE.Parser.parseBoolean(drive.ro); + values.aio = drive.aio || '__default__'; + + values.mbps_rd = drive.mbps_rd; + values.mbps_wr = drive.mbps_wr; + values.iops_rd = drive.iops_rd; + values.iops_wr = drive.iops_wr; + values.mbps_rd_max = drive.mbps_rd_max; + values.mbps_wr_max = drive.mbps_wr_max; + values.iops_rd_max = drive.iops_rd_max; + values.iops_wr_max = drive.iops_wr_max; + + me.setValues(values); + }, + + setNodename: function(nodename) { + var me = this; + me.down('#hdstorage').setNodename(nodename); + me.down('#hdimage').setStorage(undefined, nodename); + }, + + hasAdvanced: true, + + initComponent: function() { + var me = this; + + me.drive = {}; + + let column1 = []; + let column2 = []; + + let advancedColumn1 = []; + let advancedColumn2 = []; + + if (!me.confid || me.unused) { + me.bussel = Ext.create('PVE.form.ControllerSelector', { + vmconfig: me.vmconfig, + selectFree: true, + }); + column1.push(me.bussel); + + me.scsiController = Ext.create('Ext.form.field.Display', { + fieldLabel: gettext('SCSI Controller'), + reference: 'scsiController', + name: 'scsiController', + bind: me.insideWizard ? { + value: '{current.scsihw}', + visible: '{isSCSI}', + } : { + visible: '{isSCSI}', + }, + renderer: PVE.Utils.render_scsihw, + submitValue: false, + hidden: true, + }); + column1.push(me.scsiController); + } + + if (me.unused) { + me.unusedDisks = Ext.create('Proxmox.form.KVComboBox', { + name: 'unusedId', + fieldLabel: gettext('Disk image'), + matchFieldWidth: false, + listConfig: { + width: 350, + }, + data: [], + allowBlank: false, + }); + column1.push(me.unusedDisks); + } else if (me.isCreate) { + column1.push({ + xtype: 'pveDiskStorageSelector', + storageContent: 'images', + name: 'disk', + nodename: me.nodename, + autoSelect: me.insideWizard, + }); + } else { + column1.push({ + xtype: 'textfield', + disabled: true, + submitValue: false, + fieldLabel: gettext('Disk image'), + name: 'hdimage', + }); + } + + column2.push( + { + xtype: 'CacheTypeSelector', + name: 'cache', + value: '__default__', + fieldLabel: gettext('Cache'), + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Discard'), + reference: 'discard', + name: 'discard', + }, + { + xtype: 'proxmoxcheckbox', + name: 'iothread', + fieldLabel: 'IO thread', + clearOnDisable: true, + bind: me.insideWizard || me.isCreate ? { + disabled: '{!isVirtIO && !isSCSI}', + // Checkbox.setValue handles Arrays in a different way, therefore cast to bool + value: '{!!isVirtIO || (isSCSI && isSCSISingle)}', + } : { + disabled: '{!isVirtIO && !isSCSI}', + }, + }, + ); + + advancedColumn1.push( + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('SSD emulation'), + name: 'ssd', + clearOnDisable: true, + bind: { + disabled: '{isVirtIO}', + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'readOnly', // `ro` in the config, we map in get/set values + defaultValue: 0, + fieldLabel: gettext('Read-only'), + clearOnDisable: true, + bind: { + disabled: '{!isVirtIO && !isSCSI}', + }, + }, + ); + + advancedColumn2.push( + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Backup'), + autoEl: { + tag: 'div', + 'data-qtip': gettext('Include volume in backup job'), + }, + name: 'backup', + bind: { + value: '{isIncludedInBackup}', + }, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Skip replication'), + name: 'noreplicate', + }, + { + xtype: 'proxmoxKVComboBox', + name: 'aio', + fieldLabel: gettext('Async IO'), + allowBlank: false, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (io_uring)'], + ['io_uring', 'io_uring'], + ['native', 'native'], + ['threads', 'threads'], + ], + }, + ); + + let labelWidth = 140; + + let bwColumn1 = [ + { + xtype: 'numberfield', + name: 'mbps_rd', + minValue: 1, + step: 1, + fieldLabel: gettext('Read limit') + ' (MB/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited'), + }, + { + xtype: 'numberfield', + name: 'mbps_wr', + minValue: 1, + step: 1, + fieldLabel: gettext('Write limit') + ' (MB/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited'), + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_rd', + minValue: 10, + step: 10, + fieldLabel: gettext('Read limit') + ' (ops/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited'), + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_wr', + minValue: 10, + step: 10, + fieldLabel: gettext('Write limit') + ' (ops/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited'), + }, + ]; + + let bwColumn2 = [ + { + xtype: 'numberfield', + name: 'mbps_rd_max', + minValue: 1, + step: 1, + fieldLabel: gettext('Read max burst') + ' (MB)', + labelWidth: labelWidth, + emptyText: gettext('default'), + }, + { + xtype: 'numberfield', + name: 'mbps_wr_max', + minValue: 1, + step: 1, + fieldLabel: gettext('Write max burst') + ' (MB)', + labelWidth: labelWidth, + emptyText: gettext('default'), + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_rd_max', + minValue: 10, + step: 10, + fieldLabel: gettext('Read max burst') + ' (ops)', + labelWidth: labelWidth, + emptyText: gettext('default'), + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_wr_max', + minValue: 10, + step: 10, + fieldLabel: gettext('Write max burst') + ' (ops)', + labelWidth: labelWidth, + emptyText: gettext('default'), + }, + ]; + + me.items = [ + { + xtype: 'tabpanel', + plain: true, + bodyPadding: 10, + border: 0, + items: [ + { + title: gettext('Disk'), + xtype: 'inputpanel', + reference: 'diskpanel', + column1, + column2, + advancedColumn1, + advancedColumn2, + showAdvanced: me.showAdvanced, + getValues: () => ({}), + }, + { + title: gettext('Bandwidth'), + xtype: 'inputpanel', + reference: 'bwpanel', + column1: bwColumn1, + column2: bwColumn2, + showAdvanced: me.showAdvanced, + getValues: () => ({}), + }, + ], + }, + ]; + + me.callParent(); + }, + + setAdvancedVisible: function(visible) { + this.lookup('diskpanel').setAdvancedVisible(visible); + this.lookup('bwpanel').setAdvancedVisible(visible); + }, +}); + +Ext.define('PVE.qemu.HDEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + backgroundDelay: 5, + + width: 600, + bodyPadding: 0, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var unused = me.confid && me.confid.match(/^unused\d+$/); + + me.isCreate = me.confid ? unused : true; + + var ipanel = Ext.create('PVE.qemu.HDInputPanel', { + confid: me.confid, + nodename: nodename, + unused: unused, + isCreate: me.isCreate, + }); + + if (unused) { + me.subject = gettext('Unused Disk'); + } else if (me.isCreate) { + me.subject = gettext('Hard Disk'); + } else { + me.subject = gettext('Hard Disk') + ' (' + me.confid + ')'; + } + + me.items = [ipanel]; + + me.callParent(); + /* 'data' is assigned an empty array in same file, and here we + * use it like an object + */ + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + var value = response.result.data[me.confid]; + var drive = PVE.Parser.parseQemuDrive(me.confid, value); + if (!drive) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse drive options'); + me.close(); + return; + } + ipanel.setDrive(drive); + me.isValid(); // trigger validation + } + }, + }); + }, +}); +Ext.define('PVE.qemu.EFIDiskInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveEFIDiskInputPanel', + + insideWizard: false, + + unused: false, // ADD usused disk imaged + + vmconfig: {}, // used to select usused disks + + onGetValues: function(values) { + var me = this; + + if (me.disabled) { + return {}; + } + + var confid = 'efidisk0'; + + if (values.hdimage) { + me.drive.file = values.hdimage; + } else { + // we use 1 here, because for efi the size gets overridden from the backend + me.drive.file = values.hdstorage + ":1"; + } + + // always default to newer 4m type with secure boot support, if we're + // adding a new EFI disk there can't be any old state anyway + me.drive.efitype = '4m'; + me.drive['pre-enrolled-keys'] = values.preEnrolledKeys; + delete values.preEnrolledKeys; + + me.drive.format = values.diskformat; + let params = {}; + params[confid] = PVE.Parser.printQemuDrive(me.drive); + return params; + }, + + setNodename: function(nodename) { + var me = this; + me.down('#hdstorage').setNodename(nodename); + me.down('#hdimage').setStorage(undefined, nodename); + }, + + setDisabled: function(disabled) { + let me = this; + me.down('pveDiskStorageSelector').setDisabled(disabled); + me.down('proxmoxcheckbox[name=preEnrolledKeys]').setDisabled(disabled); + me.callParent(arguments); + }, + + initComponent: function() { + var me = this; + + me.drive = {}; + + me.items = [ + { + xtype: 'pveDiskStorageSelector', + name: 'efidisk0', + storageLabel: gettext('EFI Storage'), + storageContent: 'images', + nodename: me.nodename, + disabled: me.disabled, + hideSize: true, + }, + { + xtype: 'proxmoxcheckbox', + name: 'preEnrolledKeys', + checked: true, + fieldLabel: gettext("Pre-Enroll keys"), + disabled: me.disabled, + //boxLabel: '(e.g., Microsoft secure-boot keys')', + autoEl: { + tag: 'div', + 'data-qtip': gettext('Use EFIvars image with standard distribution and Microsoft secure boot keys enrolled.'), + }, + }, + { + xtype: 'label', + text: gettext("Warning: The VM currently does not uses 'OVMF (UEFI)' as BIOS."), + userCls: 'pmx-hint', + hidden: me.usesEFI, + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.qemu.EFIDiskEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + subject: gettext('EFI Disk'), + + width: 450, + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.items = [{ + xtype: 'pveEFIDiskInputPanel', + onlineHelp: 'qm_bios_and_uefi', + confid: me.confid, + nodename: nodename, + usesEFI: me.usesEFI, + isCreate: true, + }]; + + me.callParent(); + }, +}); +Ext.define('PVE.qemu.TPMDiskInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveTPMDiskInputPanel', + + unused: false, + vmconfig: {}, + + onGetValues: function(values) { + var me = this; + + if (me.disabled) { + return {}; + } + + var confid = 'tpmstate0'; + + if (values.hdimage) { + me.drive.file = values.hdimage; + } else { + // size is constant, so just use 1 + me.drive.file = values.hdstorage + ":1"; + } + + me.drive.version = values.version; + var params = {}; + params[confid] = PVE.Parser.printQemuDrive(me.drive); + return params; + }, + + setNodename: function(nodename) { + var me = this; + me.down('#hdstorage').setNodename(nodename); + me.down('#hdimage').setStorage(undefined, nodename); + }, + + setDisabled: function(disabled) { + let me = this; + me.down('pveDiskStorageSelector').setDisabled(disabled); + me.down('proxmoxKVComboBox[name=version]').setDisabled(disabled); + me.callParent(arguments); + }, + + initComponent: function() { + var me = this; + + me.drive = {}; + + me.items = [ + { + xtype: 'pveDiskStorageSelector', + name: me.disktype + '0', + storageLabel: gettext('TPM Storage'), + storageContent: 'images', + nodename: me.nodename, + disabled: me.disabled, + hideSize: true, + hideFormat: true, + }, + { + xtype: 'proxmoxKVComboBox', + name: 'version', + value: 'v2.0', + fieldLabel: gettext('Version'), + deleteEmpty: false, + disabled: me.disabled, + comboItems: [ + ['v1.2', 'v1.2'], + ['v2.0', 'v2.0'], + ], + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.qemu.TPMDiskEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + subject: gettext('TPM State'), + + width: 450, + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.items = [{ + xtype: 'pveTPMDiskInputPanel', + //onlineHelp: 'qm_tpm', FIXME: add once available + confid: me.confid, + nodename: nodename, + isCreate: true, + }]; + + me.callParent(); + }, +}); +Ext.define('PVE.window.HDMove', { + extend: 'Proxmox.window.Edit', + mixins: ['Proxmox.Mixin.CBind'], + + resizable: false, + modal: true, + width: 350, + border: false, + layout: 'fit', + showReset: false, + showTaskViewer: true, + method: 'POST', + + cbindData: function() { + let me = this; + return { + disk: me.disk, + isQemu: me.type === 'qemu', + nodename: me.nodename, + url: () => { + let endpoint = me.type === 'qemu' ? 'move_disk' : 'move_volume'; + return `/nodes/${me.nodename}/${me.type}/${me.vmid}/${endpoint}`; + }, + }; + }, + + cbind: { + title: get => get('isQemu') ? gettext("Move disk") : gettext('Move Volume'), + submitText: get => get('title'), + qemu: '{isQemu}', + url: '{url}', + }, + + getValues: function() { + let me = this; + let values = me.formPanel.getForm().getValues(); + + let params = { + storage: values.hdstorage, + }; + params[me.qemu ? 'disk' : 'volume'] = me.disk; + + if (values.diskformat && me.qemu) { + params.format = values.diskformat; + } + + if (values.deleteDisk) { + params.delete = 1; + } + return params; + }, + + items: [ + { + xtype: 'form', + reference: 'moveFormPanel', + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%', + }, + items: [ + { + xtype: 'displayfield', + cbind: { + name: get => get('isQemu') ? 'disk' : 'volume', + fieldLabel: get => get('isQemu') ? gettext('Disk') : gettext('Mount Point'), + value: '{disk}', + }, + allowBlank: false, + }, + { + xtype: 'pveDiskStorageSelector', + storageLabel: gettext('Target Storage'), + cbind: { + nodename: '{nodename}', + storageContent: get => get('isQemu') ? 'images' : 'rootdir', + hideFormat: get => get('disk') === 'tpmstate0', + }, + hideSize: true, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Delete source'), + name: 'deleteDisk', + uncheckedValue: 0, + checked: false, + }, + ], + }, + ], + + initComponent: function() { + let me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.type) { + throw "no type specified"; + } + + me.callParent(); + }, +}); +Ext.define('PVE.window.HDResize', { + extend: 'Ext.window.Window', + + resizable: false, + + resize_disk: function(disk, size) { + var me = this; + var params = { disk: disk, size: '+' + size + 'G' }; + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/resize', + waitMsgTarget: me, + method: 'PUT', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + me.close(); + }, + }); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + var items = [ + { + xtype: 'displayfield', + name: 'disk', + value: me.disk, + fieldLabel: gettext('Disk'), + vtype: 'StorageId', + allowBlank: false, + }, + ]; + + me.hdsizesel = Ext.createWidget('numberfield', { + name: 'size', + minValue: 0, + maxValue: 128*1024, + decimalPrecision: 3, + value: '0', + fieldLabel: gettext('Size Increment') + ' (GiB)', + allowBlank: false, + }); + + items.push(me.hdsizesel); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 140, + anchor: '100%', + }, + items: items, + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + me.title = gettext('Resize disk'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Resize disk'), + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.resize_disk(me.disk, values.size); + } + }, + }); + + Ext.apply(me, { + modal: true, + width: 250, + height: 150, + border: false, + layout: 'fit', + buttons: [submitBtn], + items: [me.formPanel], + }); + + + me.callParent(); + }, +}); +Ext.define('PVE.qemu.HardwareView', { + extend: 'Proxmox.grid.PendingObjectGrid', + alias: ['widget.PVE.qemu.HardwareView'], + + onlineHelp: 'qm_virtual_machines_settings', + + renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var rowdef = rows[key] || {}; + var iconCls = rowdef.iconCls; + var icon = ''; + var txt = rowdef.header || key; + + metaData.tdAttr = "valign=middle"; + + if (rowdef.isOnStorageBus) { + var value = me.getObjectValue(key, '', false); + if (value === '') { + value = me.getObjectValue(key, '', true); + } + if (value.match(/vm-.*-cloudinit/)) { + iconCls = 'cloud'; + txt = rowdef.cloudheader; + } else if (value.match(/media=cdrom/)) { + metaData.tdCls = 'pve-itype-icon-cdrom'; + return rowdef.cdheader; + } + } + + if (rowdef.tdCls) { + metaData.tdCls = rowdef.tdCls; + } else if (iconCls) { + icon = ""; + metaData.tdCls += " pve-itype-fa"; + } + + // only return icons in grid but not remove dialog + if (rowIndex !== undefined) { + return icon + txt; + } else { + return txt; + } + }, + + initComponent: function() { + var me = this; + + const { node: nodename, vmid } = me.pveSelNode.data; + if (!nodename) { + throw "no node name specified"; + } else if (!vmid) { + throw "no VM ID specified"; + } + + const caps = Ext.state.Manager.get('GuiCap'); + const diskCap = caps.vms['VM.Config.Disk']; + const cdromCap = caps.vms['VM.Config.CDROM']; + + let isCloudInitKey = v => v && v.toString().match(/vm-.*-cloudinit/); + + const nodeInfo = PVE.data.ResourceStore.getNodes().find(node => node.node === nodename); + let processorEditor = { + xtype: 'pveQemuProcessorEdit', + cgroupMode: nodeInfo['cgroup-mode'], + }; + + let rows = { + memory: { + header: gettext('Memory'), + editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined, + never_delete: true, + defaultValue: '512', + tdCls: 'pve-itype-icon-memory', + group: 2, + multiKey: ['memory', 'balloon', 'shares'], + renderer: function(value, metaData, record, ri, ci, store, pending) { + var res = ''; + + var max = me.getObjectValue('memory', 512, pending); + var balloon = me.getObjectValue('balloon', undefined, pending); + var shares = me.getObjectValue('shares', undefined, pending); + + res = Proxmox.Utils.format_size(max*1024*1024); + + if (balloon !== undefined && balloon > 0) { + res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res; + + if (shares) { + res += ' [shares=' + shares +']'; + } + } else if (balloon === 0) { + res += ' [balloon=0]'; + } + return res; + }, + }, + sockets: { + header: gettext('Processors'), + never_delete: true, + editor: caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType'] + ? processorEditor : undefined, + tdCls: 'pve-itype-icon-cpu', + group: 3, + defaultValue: '1', + multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'], + renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) { + var sockets = me.getObjectValue('sockets', 1, pending); + var model = me.getObjectValue('cpu', undefined, pending); + var cores = me.getObjectValue('cores', 1, pending); + var numa = me.getObjectValue('numa', undefined, pending); + var vcpus = me.getObjectValue('vcpus', undefined, pending); + var cpulimit = me.getObjectValue('cpulimit', undefined, pending); + var cpuunits = me.getObjectValue('cpuunits', undefined, pending); + + let res = Ext.String.format( + '{0} ({1} sockets, {2} cores)', sockets * cores, sockets, cores); + + if (model) { + res += ' [' + model + ']'; + } + if (numa) { + res += ' [numa=' + numa +']'; + } + if (vcpus) { + res += ' [vcpus=' + vcpus +']'; + } + if (cpulimit) { + res += ' [cpulimit=' + cpulimit +']'; + } + if (cpuunits) { + res += ' [cpuunits=' + cpuunits +']'; + } + + return res; + }, + }, + bios: { + header: 'BIOS', + group: 4, + never_delete: true, + editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined, + defaultValue: '', + iconCls: 'microchip', + renderer: PVE.Utils.render_qemu_bios, + }, + vga: { + header: gettext('Display'), + editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined, + never_delete: true, + iconCls: 'desktop', + group: 5, + defaultValue: '', + renderer: PVE.Utils.render_kvm_vga_driver, + }, + machine: { + header: gettext('Machine'), + editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.MachineEdit' : undefined, + iconCls: 'cogs', + never_delete: true, + group: 6, + defaultValue: '', + renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) { + let ostype = me.getObjectValue('ostype', undefined, pending); + if (PVE.Utils.is_windows(ostype) && + (!value || value === 'pc' || value === 'q35')) { + return value === 'q35' ? 'pc-q35-5.1' : 'pc-i440fx-5.1'; + } + return PVE.Utils.render_qemu_machine(value); + }, + }, + scsihw: { + header: gettext('SCSI Controller'), + iconCls: 'database', + editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined, + renderer: PVE.Utils.render_scsihw, + group: 7, + never_delete: true, + defaultValue: '', + }, + vmstate: { + header: gettext('Hibernation VM State'), + iconCls: 'download', + del_extra_msg: gettext('The saved VM state will be permanently lost.'), + group: 100, + }, + cores: { + visible: false, + }, + cpu: { + visible: false, + }, + numa: { + visible: false, + }, + balloon: { + visible: false, + }, + hotplug: { + visible: false, + }, + vcpus: { + visible: false, + }, + cpuunits: { + visible: false, + }, + cpulimit: { + visible: false, + }, + shares: { + visible: false, + }, + ostype: { + visible: false, + }, + }; + + PVE.Utils.forEachBus(undefined, function(type, id) { + let confid = type + id; + rows[confid] = { + group: 10, + iconCls: 'hdd-o', + editor: 'PVE.qemu.HDEdit', + isOnStorageBus: true, + header: gettext('Hard Disk') + ' (' + confid +')', + cdheader: gettext('CD/DVD Drive') + ' (' + confid +')', + cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')', + }; + }); + for (let i = 0; i < PVE.Utils.hardware_counts.net; i++) { + let confid = "net" + i.toString(); + rows[confid] = { + group: 15, + order: i, + iconCls: 'exchange', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined, + never_delete: !caps.vms['VM.Config.Network'], + header: gettext('Network Device') + ' (' + confid +')', + }; + } + rows.efidisk0 = { + group: 20, + iconCls: 'hdd-o', + editor: null, + never_delete: !caps.vms['VM.Config.Disk'], + header: gettext('EFI Disk'), + }; + rows.tpmstate0 = { + group: 22, + iconCls: 'hdd-o', + editor: null, + never_delete: !caps.vms['VM.Config.Disk'], + header: gettext('TPM State'), + }; + for (let i = 0; i < PVE.Utils.hardware_counts.usb; i++) { + let confid = "usb" + i.toString(); + rows[confid] = { + group: 25, + order: i, + iconCls: 'usb', + editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined, + never_delete: !caps.nodes['Sys.Console'], + header: gettext('USB Device') + ' (' + confid + ')', + }; + } + for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) { + let confid = "hostpci" + i.toString(); + rows[confid] = { + group: 30, + order: i, + tdCls: 'pve-itype-icon-pci', + never_delete: !caps.nodes['Sys.Console'], + editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined, + header: gettext('PCI Device') + ' (' + confid + ')', + }; + } + for (let i = 0; i < PVE.Utils.hardware_counts.serial; i++) { + let confid = "serial" + i.toString(); + rows[confid] = { + group: 35, + order: i, + tdCls: 'pve-itype-icon-serial', + never_delete: !caps.nodes['Sys.Console'], + header: gettext('Serial Port') + ' (' + confid + ')', + }; + } + rows.audio0 = { + group: 40, + iconCls: 'volume-up', + editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined, + never_delete: !caps.vms['VM.Config.HWType'], + header: gettext('Audio Device'), + }; + for (let i = 0; i < 256; i++) { + rows["unused" + i.toString()] = { + group: 99, + order: i, + iconCls: 'hdd-o', + del_extra_msg: gettext('This will permanently erase all data.'), + editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined, + header: gettext('Unused Disk') + ' ' + i.toString(), + }; + } + rows.rng0 = { + group: 45, + tdCls: 'pve-itype-icon-die', + editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.RNGEdit' : undefined, + never_delete: !caps.nodes['Sys.Console'], + header: gettext("VirtIO RNG"), + }; + + var sorterFn = function(rec1, rec2) { + var v1 = rec1.data.key; + var v2 = rec2.data.key; + var g1 = rows[v1].group || 0; + var g2 = rows[v2].group || 0; + var order1 = rows[v1].order || 0; + var order2 = rows[v2].order || 0; + + if (g1 - g2 !== 0) { + return g1 - g2; + } + + if (order1 - order2 !== 0) { + return order1 - order2; + } + + if (v1 > v2) { + return 1; + } else if (v1 < v2) { + return -1; + } else { + return 0; + } + }; + + let baseurl = `nodes/${nodename}/qemu/${vmid}/config`; + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec || !rows[rec.data.key]?.editor) { + return; + } + let rowdef = rows[rec.data.key]; + let editor = rowdef.editor; + + if (rowdef.isOnStorageBus) { + let value = me.getObjectValue(rec.data.key, '', true); + if (isCloudInitKey(value)) { + return; + } else if (value.match(/media=cdrom/)) { + editor = 'PVE.qemu.CDEdit'; + } else if (!diskCap) { + return; + } + } + + let commonOpts = { + autoShow: true, + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: `/api2/extjs/${baseurl}`, + listeners: { + destroy: () => me.reload(), + }, + }; + + if (Ext.isString(editor)) { + Ext.create(editor, commonOpts); + } else { + let win = Ext.createWidget(rowdef.editor.xtype, Ext.apply(commonOpts, rowdef.editor)); + win.load(); + } + }; + + let edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + selModel: sm, + disabled: true, + handler: run_editor, + }); + + let move_menuitem = new Ext.menu.Item({ + text: gettext('Move Storage'), + tooltip: gettext('Move disk to another storage'), + iconCls: 'fa fa-database', + selModel: sm, + handler: () => { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + Ext.create('PVE.window.HDMove', { + autoShow: true, + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + type: 'qemu', + listeners: { + destroy: () => me.reload(), + }, + }); + }, + }); + + let reassign_menuitem = new Ext.menu.Item({ + text: gettext('Reassign Owner'), + tooltip: gettext('Reassign disk to another VM'), + iconCls: 'fa fa-desktop', + selModel: sm, + handler: () => { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + Ext.create('PVE.window.GuestDiskReassign', { + autoShow: true, + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + type: 'qemu', + listeners: { + destroy: () => me.reload(), + }, + }); + }, + }); + + let resize_menuitem = new Ext.menu.Item({ + text: gettext('Resize'), + iconCls: 'fa fa-plus', + selModel: sm, + handler: () => { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + Ext.create('PVE.window.HDResize', { + autoShow: true, + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + listeners: { + destroy: () => me.reload(), + }, + }); + }, + }); + + let diskaction_btn = new Proxmox.button.Button({ + text: gettext('Disk Action'), + disabled: true, + menu: { + items: [ + move_menuitem, + reassign_menuitem, + resize_menuitem, + ], + }, + }); + + + let remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + defaultText: gettext('Remove'), + altText: gettext('Detach'), + selModel: sm, + disabled: true, + dangerous: true, + RESTMethod: 'PUT', + confirmMsg: function(rec) { + let warn = gettext('Are you sure you want to remove entry {0}'); + if (this.text === this.altText) { + warn = gettext('Are you sure you want to detach entry {0}'); + } + let rendered = me.renderKey(rec.data.key, {}, rec); + let msg = Ext.String.format(warn, `'${rendered}'`); + + if (rows[rec.data.key].del_extra_msg) { + msg += '
' + rows[rec.data.key].del_extra_msg; + } + return msg; + }, + handler: function(btn, e, rec) { + Proxmox.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: btn.RESTMethod, + params: { + 'delete': rec.data.key, + }, + callback: () => me.reload(), + failure: response => Ext.Msg.alert('Error', response.htmlStatus), + success: function(response, options) { + if (btn.RESTMethod === 'POST') { + Ext.create('Proxmox.window.TaskProgress', { + autoShow: true, + upid: response.result.data, + listeners: { + destroy: () => me.reload(), + }, + }); + } + }, + }); + }, + listeners: { + render: function(btn) { + // hack: calculate the max button width on first display to prevent the whole + // toolbar to move when we switch between the "Remove" and "Detach" labels + var def = btn.getSize().width; + + btn.setText(btn.altText); + var alt = btn.getSize().width; + + btn.setText(btn.defaultText); + + var optimal = alt > def ? alt : def; + btn.setSize({ width: optimal }); + }, + }, + }); + + let revert_btn = new PVE.button.PendingRevert({ + apiurl: '/api2/extjs/' + baseurl, + }); + + let efidisk_menuitem = Ext.create('Ext.menu.Item', { + text: gettext('EFI Disk'), + iconCls: 'fa fa-fw fa-hdd-o black', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + let { data: bios } = me.rstore.getData().map.bios || {}; + + Ext.create('PVE.qemu.EFIDiskEdit', { + autoShow: true, + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode, + usesEFI: bios?.value === 'ovmf' || bios?.pending === 'ovmf', + listeners: { + destroy: () => me.reload(), + }, + }); + }, + }); + + let counts = {}; + let isAtLimit = (type) => counts[type] >= PVE.Utils.hardware_counts[type]; + let isAtUsbLimit = () => { + let ostype = me.getObjectValue('ostype'); + let machine = me.getObjectValue('machine'); + return counts.usb >= PVE.Utils.get_max_usb_count(ostype, machine); + }; + + let set_button_status = function() { + let selection_model = me.getSelectionModel(); + let rec = selection_model.getSelection()[0]; + + counts = {}; // en/disable hardwarebuttons + let hasCloudInit = false; + me.rstore.getData().items.forEach(function({ id, data }) { + if (!hasCloudInit && (isCloudInitKey(data.value) || isCloudInitKey(data.pending))) { + hasCloudInit = true; + return; + } + + let match = id.match(/^([^\d]+)\d+$/); + if (match && PVE.Utils.hardware_counts[match[1]] !== undefined) { + let type = match[1]; + counts[type] = (counts[type] || 0) + 1; + } + }); + + // heuristic only for disabling some stuff, the backend has the final word. + const noSysConsolePerm = !caps.nodes['Sys.Console']; + const noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType']; + const noVMConfigNetPerm = !caps.vms['VM.Config.Network']; + const noVMConfigDiskPerm = !caps.vms['VM.Config.Disk']; + const noVMConfigCDROMPerm = !caps.vms['VM.Config.CDROM']; + const noVMConfigCloudinitPerm = !caps.vms['VM.Config.Cloudinit']; + + me.down('#addUsb').setDisabled(noSysConsolePerm || isAtUsbLimit()); + me.down('#addPci').setDisabled(noSysConsolePerm || isAtLimit('hostpci')); + me.down('#addAudio').setDisabled(noVMConfigHWTypePerm || isAtLimit('audio')); + me.down('#addSerial').setDisabled(noVMConfigHWTypePerm || isAtLimit('serial')); + me.down('#addNet').setDisabled(noVMConfigNetPerm || isAtLimit('net')); + me.down('#addRng').setDisabled(noSysConsolePerm || isAtLimit('rng')); + efidisk_menuitem.setDisabled(noVMConfigDiskPerm || isAtLimit('efidisk')); + me.down('#addTpmState').setDisabled(noSysConsolePerm || isAtLimit('tpmstate')); + me.down('#addCloudinitDrive').setDisabled(noVMConfigCDROMPerm || noVMConfigCloudinitPerm || hasCloudInit); + + if (!rec) { + remove_btn.disable(); + edit_btn.disable(); + diskaction_btn.disable(); + revert_btn.disable(); + return; + } + const { key, value } = rec.data; + const row = rows[key]; + + const deleted = !!rec.data.delete; + const pending = deleted || me.hasPendingChanges(key); + + const isCloudInit = isCloudInitKey(value); + const isCDRom = value && !!value.toString().match(/media=cdrom/); + + const isUnusedDisk = key.match(/^unused\d+/); + const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom; + const isDisk = isUnusedDisk || isUsedDisk; + const isEfi = key === 'efidisk0'; + const tpmMoveable = key === 'tpmstate0' && !me.pveSelNode.data.running; + + let cannotDelete = deleted || row.never_delete; + cannotDelete ||= isCDRom && !cdromCap; + cannotDelete ||= isDisk && !diskCap; + cannotDelete ||= isCloudInit && noVMConfigCloudinitPerm; + remove_btn.setDisabled(cannotDelete); + + remove_btn.setText(isUsedDisk && !isCloudInit ? remove_btn.altText : remove_btn.defaultText); + remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT'; + + edit_btn.setDisabled( + deleted || !row.editor || isCloudInit || (isCDRom && !cdromCap) || (isDisk && !diskCap)); + + diskaction_btn.setDisabled( + pending || + !diskCap || + isCloudInit || + !(isDisk || isEfi || tpmMoveable), + ); + move_menuitem.setDisabled(isUnusedDisk); + reassign_menuitem.setDisabled(pending || (isEfi || tpmMoveable)); + resize_menuitem.setDisabled(pending || !isUsedDisk); + + revert_btn.setDisabled(!pending); + }; + + let editorFactory = (classPath, extraOptions) => { + extraOptions = extraOptions || {}; + return () => Ext.create(`PVE.qemu.${classPath}`, { + autoShow: true, + url: `/api2/extjs/${baseurl}`, + pveSelNode: me.pveSelNode, + listeners: { + destroy: () => me.reload(), + }, + isAdd: true, + isCreate: true, + ...extraOptions, + }); + }; + + Ext.apply(me, { + url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`, + interval: 5000, + selModel: sm, + run_editor: run_editor, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + cls: 'pve-add-hw-menu', + items: [ + { + text: gettext('Hard Disk'), + iconCls: 'fa fa-fw fa-hdd-o black', + disabled: !caps.vms['VM.Config.Disk'], + handler: editorFactory('HDEdit'), + }, + { + text: gettext('CD/DVD Drive'), + iconCls: 'pve-itype-icon-cdrom', + disabled: !caps.vms['VM.Config.CDROM'], + handler: editorFactory('CDEdit'), + }, + { + text: gettext('Network Device'), + itemId: 'addNet', + iconCls: 'fa fa-fw fa-exchange black', + disabled: !caps.vms['VM.Config.Network'], + handler: editorFactory('NetworkEdit'), + }, + efidisk_menuitem, + { + text: gettext('TPM State'), + itemId: 'addTpmState', + iconCls: 'fa fa-fw fa-hdd-o black', + disabled: !caps.vms['VM.Config.Disk'], + handler: editorFactory('TPMDiskEdit'), + }, + { + text: gettext('USB Device'), + itemId: 'addUsb', + iconCls: 'fa fa-fw fa-usb black', + disabled: !caps.nodes['Sys.Console'], + handler: editorFactory('USBEdit'), + }, + { + text: gettext('PCI Device'), + itemId: 'addPci', + iconCls: 'pve-itype-icon-pci', + disabled: !caps.nodes['Sys.Console'], + handler: editorFactory('PCIEdit'), + }, + { + text: gettext('Serial Port'), + itemId: 'addSerial', + iconCls: 'pve-itype-icon-serial', + disabled: !caps.vms['VM.Config.Options'], + handler: editorFactory('SerialEdit'), + }, + { + text: gettext('CloudInit Drive'), + itemId: 'addCloudinitDrive', + iconCls: 'fa fa-fw fa-cloud black', + disabled: !caps.vms['VM.Config.CDROM'] || !caps.vms['VM.Config.Cloudinit'], + handler: editorFactory('CIDriveEdit'), + }, + { + text: gettext('Audio Device'), + itemId: 'addAudio', + iconCls: 'fa fa-fw fa-volume-up black', + disabled: !caps.vms['VM.Config.HWType'], + handler: editorFactory('AudioEdit'), + }, + { + text: gettext("VirtIO RNG"), + itemId: 'addRng', + iconCls: 'pve-itype-icon-die', + disabled: !caps.nodes['Sys.Console'], + handler: editorFactory('RNGEdit'), + }, + ], + }), + }, + remove_btn, + edit_btn, + diskaction_btn, + revert_btn, + ], + rows: rows, + sorterFn: sorterFn, + listeners: { + itemdblclick: run_editor, + selectionchange: set_button_status, + }, + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate, me.rstore); + me.on('destroy', me.rstore.stopUpdate, me.rstore); + + me.mon(me.getStore(), 'datachanged', set_button_status, me); + }, +}); +Ext.define('PVE.qemu.IPConfigPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveIPConfigPanel', + + insideWizard: false, + + vmconfig: {}, + + onGetValues: function(values) { + var me = this; + + if (values.ipv4mode !== 'static') { + values.ip = values.ipv4mode; + } + + if (values.ipv6mode !== 'static') { + values.ip6 = values.ipv6mode; + } + + var params = {}; + + var cfg = PVE.Parser.printIPConfig(values); + if (cfg === '') { + params.delete = [me.confid]; + } else { + params[me.confid] = cfg; + } + return params; + }, + + setVMConfig: function(config) { + var me = this; + me.vmconfig = config; + }, + + setIPConfig: function(confid, data) { + var me = this; + + me.confid = confid; + + if (data.ip === 'dhcp') { + data.ipv4mode = data.ip; + data.ip = ''; + } else { + data.ipv4mode = 'static'; + } + if (data.ip6 === 'dhcp' || data.ip6 === 'auto') { + data.ipv6mode = data.ip6; + data.ip6 = ''; + } else { + data.ipv6mode = 'static'; + } + + me.ipconfig = data; + me.setValues(me.ipconfig); + }, + + initComponent: function() { + var me = this; + + me.ipconfig = {}; + + me.column1 = [ + { + xtype: 'displayfield', + fieldLabel: gettext('Network Device'), + value: me.netid, + }, + { + layout: { + type: 'hbox', + align: 'middle', + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: gettext('IPv4') + ':', + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv4mode', + inputValue: 'static', + checked: false, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip]').setDisabled(!value); + me.down('field[name=gw]').setDisabled(!value); + }, + }, + }, + { + xtype: 'radiofield', + boxLabel: gettext('DHCP'), + name: 'ipv4mode', + inputValue: 'dhcp', + checked: false, + margin: '0 0 0 10', + }, + ], + }, + { + xtype: 'textfield', + name: 'ip', + vtype: 'IPCIDRAddress', + value: '', + disabled: true, + fieldLabel: gettext('IPv4/CIDR'), + }, + { + xtype: 'textfield', + name: 'gw', + value: '', + vtype: 'IPAddress', + disabled: true, + fieldLabel: gettext('Gateway') + ' (' + gettext('IPv4') +')', + }, + ]; + + me.column2 = [ + { + xtype: 'displayfield', + }, + { + layout: { + type: 'hbox', + align: 'middle', + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: gettext('IPv6') + ':', + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv6mode', + inputValue: 'static', + checked: false, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip6]').setDisabled(!value); + me.down('field[name=gw6]').setDisabled(!value); + }, + }, + }, + { + xtype: 'radiofield', + boxLabel: gettext('DHCP'), + name: 'ipv6mode', + inputValue: 'dhcp', + checked: false, + margin: '0 0 0 10', + }, + { + xtype: 'radiofield', + boxLabel: gettext('SLAAC'), + name: 'ipv6mode', + inputValue: 'auto', + checked: false, + margin: '0 0 0 10', + }, + ], + }, + { + xtype: 'textfield', + name: 'ip6', + value: '', + vtype: 'IP6CIDRAddress', + disabled: true, + fieldLabel: gettext('IPv6/CIDR'), + }, + { + xtype: 'textfield', + name: 'gw6', + vtype: 'IP6Address', + value: '', + disabled: true, + fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') +')', + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.qemu.IPConfigEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + initComponent: function() { + var me = this; + + // convert confid from netX to ipconfigX + var match = me.confid.match(/^net(\d+)$/); + if (match) { + me.netid = me.confid; + me.confid = 'ipconfig' + match[1]; + } + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.isCreate = !me.confid; + + var ipanel = Ext.create('PVE.qemu.IPConfigPanel', { + confid: me.confid, + netid: me.netid, + nodename: nodename, + }); + + Ext.applyIf(me, { + subject: gettext('Network Config'), + items: ipanel, + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + me.vmconfig = response.result.data; + var ipconfig = {}; + var value = me.vmconfig[me.confid]; + if (value) { + ipconfig = PVE.Parser.parseIPConfig(me.confid, value); + if (!ipconfig) { + Ext.Msg.alert(gettext('Error'), gettext('Unable to parse network configuration')); + me.close(); + return; + } + } + ipanel.setIPConfig(me.confid, ipconfig); + ipanel.setVMConfig(me.vmconfig); + }, + }); + }, +}); +Ext.define('PVE.qemu.KeyboardEdit', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + Ext.applyIf(me, { + subject: gettext('Keyboard Layout'), + items: { + xtype: 'VNCKeyboardSelector', + name: 'keyboard', + value: '__default__', + fieldLabel: gettext('Keyboard Layout'), + }, + }); + + me.callParent(); + + me.load(); + }, +}); +Ext.define('PVE.qemu.MachineInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveMachineInputPanel', + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'combobox[name=machine]': { + change: 'onMachineChange', + }, + }, + onMachineChange: function(field, value) { + let me = this; + let version = me.lookup('version'); + let store = version.getStore(); + let oldRec = store.findRecord('id', version.getValue(), 0, false, false, true); + let type = value === 'q35' ? 'q35' : 'i440fx'; + store.clearFilter(); + store.addFilter(val => val.data.id === 'latest' || val.data.type === type); + if (!me.getView().isWindows) { + version.setValue('latest'); + } else { + store.isWindows = true; + if (!oldRec) { + return; + } + let oldVers = oldRec.data.version; + // we already filtered by correct type, so just check version property + let rec = store.findRecord('version', oldVers, 0, false, false, true); + if (rec) { + version.select(rec); + } + } + }, + }, + + onGetValues: function(values) { + if (values.version && values.version !== 'latest') { + values.machine = values.version; + delete values.delete; + } + delete values.version; + return values; + }, + + setValues: function(values) { + let me = this; + + me.isWindows = values.isWindows; + if (values.machine === 'pc') { + values.machine = '__default__'; + } + + if (me.isWindows) { + if (values.machine === '__default__') { + values.version = 'pc-i440fx-5.1'; + } else if (values.machine === 'q35') { + values.version = 'pc-q35-5.1'; + } + } + if (values.machine !== '__default__' && values.machine !== 'q35') { + values.version = values.machine; + values.machine = values.version.match(/q35/) ? 'q35' : '__default__'; + + // avoid hiding a pinned version + me.setAdvancedVisible(true); + } + + this.callParent(arguments); + }, + + items: { + xtype: 'proxmoxKVComboBox', + name: 'machine', + reference: 'machine', + fieldLabel: gettext('Machine'), + comboItems: [ + ['__default__', PVE.Utils.render_qemu_machine('')], + ['q35', 'q35'], + ], + }, + + advancedItems: [ + { + xtype: 'combobox', + name: 'version', + reference: 'version', + fieldLabel: gettext('Version'), + emptyText: gettext('Latest'), + value: 'latest', + editable: false, + valueField: 'id', + displayField: 'version', + queryParam: false, + store: { + autoLoad: true, + fields: ['id', 'type', 'version'], + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/localhost/capabilities/qemu/machines", + }, + listeners: { + load: function(records) { + if (!this.isWindows) { + this.insert(0, { id: 'latest', type: 'any', version: gettext('Latest') }); + } + }, + }, + }, + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Note'), + value: gettext('Machine version change may affect hardware layout and settings in the guest OS.'), + }, + ], +}); + +Ext.define('PVE.qemu.MachineEdit', { + extend: 'Proxmox.window.Edit', + + subject: gettext('Machine'), + + items: { + xtype: 'pveMachineInputPanel', + }, + + width: 400, + + initComponent: function() { + let me = this; + + me.callParent(); + + me.load({ + success: function(response) { + let conf = response.result.data; + let values = { + machine: conf.machine || '__default__', + }; + values.isWindows = PVE.Utils.is_windows(conf.ostype); + me.setValues(values); + }, + }); + }, +}); +Ext.define('PVE.qemu.MemoryInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuMemoryPanel', + onlineHelp: 'qm_memory', + + insideWizard: false, + + viewModel: {}, // inherit data from createWizard if insideWizard + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + '#': { + afterrender: 'setMemory', + }, + }, + + setMemory: function() { + let me = this; + let view = me.getView(), viewModel = me.getViewModel(); + if (view.insideWizard) { + let memory = view.down('pveMemoryField[name=memory]'); + // NOTE: we only set memory but that then sets balloon in its change handler + if (viewModel.get('current.ostype') === 'win11') { + memory.setValue('4096'); + } else { + memory.setValue('2048'); + } + } + }, + }, + + onGetValues: function(values) { + var me = this; + + var res = {}; + + res.memory = values.memory; + res.balloon = values.balloon; + + if (!values.ballooning) { + res.balloon = 0; + res.delete = 'shares'; + } else if (values.memory === values.balloon) { + delete res.balloon; + res.delete = 'balloon,shares'; + } else if (Ext.isDefined(values.shares) && values.shares !== "") { + res.shares = values.shares; + } else { + res.delete = "shares"; + } + + return res; + }, + + initComponent: function() { + var me = this; + var labelWidth = 160; + + me.items= [ + { + xtype: 'pveMemoryField', + labelWidth: labelWidth, + fieldLabel: gettext('Memory') + ' (MiB)', + name: 'memory', + value: '512', // better defaults get set via the view controllers afterrender + minValue: 1, + step: 32, + hotplug: me.hotplug, + listeners: { + change: function(f, value, old) { + var bf = me.down('field[name=balloon]'); + var balloon = bf.getValue(); + bf.setMaxValue(value); + if (balloon === old) { + bf.setValue(value); + } + bf.validate(); + }, + }, + }, + ]; + + me.advancedItems= [ + { + xtype: 'pveMemoryField', + name: 'balloon', + minValue: 1, + maxValue: me.insideWizard ? 2048 : 512, + value: '512', // better defaults get set (indirectly) via the view controllers afterrender + step: 32, + fieldLabel: gettext('Minimum memory') + ' (MiB)', + hotplug: me.hotplug, + labelWidth: labelWidth, + allowBlank: false, + listeners: { + change: function(f, value) { + var memory = me.down('field[name=memory]').getValue(); + var shares = me.down('field[name=shares]'); + shares.setDisabled(value === memory); + }, + }, + }, + { + xtype: 'proxmoxintegerfield', + name: 'shares', + disabled: true, + minValue: 0, + maxValue: 50000, + value: '', + step: 10, + fieldLabel: gettext('Shares'), + labelWidth: labelWidth, + allowBlank: true, + emptyText: Proxmox.Utils.defaultText + ' (1000)', + submitEmptyText: false, + }, + { + xtype: 'proxmoxcheckbox', + labelWidth: labelWidth, + value: '1', + name: 'ballooning', + fieldLabel: gettext('Ballooning Device'), + listeners: { + change: function(f, value) { + var bf = me.down('field[name=balloon]'); + var shares = me.down('field[name=shares]'); + var memory = me.down('field[name=memory]'); + bf.setDisabled(!value); + shares.setDisabled(!value || bf.getValue() === memory.getValue()); + }, + }, + }, + ]; + + if (me.insideWizard) { + me.column1 = me.items; + me.items = undefined; + me.advancedColumn1 = me.advancedItems; + me.advancedItems = undefined; + } + me.callParent(); + }, + +}); + +Ext.define('PVE.qemu.MemoryEdit', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + var memoryhotplug; + if (me.hotplug) { + Ext.each(me.hotplug.split(','), function(el) { + if (el === 'memory') { + memoryhotplug = 1; + } + }); + } + + var ipanel = Ext.create('PVE.qemu.MemoryInputPanel', { + hotplug: memoryhotplug, + }); + + Ext.apply(me, { + subject: gettext('Memory'), + items: [ipanel], + // uncomment the following to use the async configiguration API + // backgroundDelay: 5, + width: 400, + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var data = response.result.data; + + var values = { + ballooning: data.balloon === 0 ? '0' : '1', + shares: data.shares, + memory: data.memory || '512', + balloon: data.balloon > 0 ? data.balloon : data.memory || '512', + }; + + ipanel.setValues(values); + }, + }); + }, +}); +Ext.define('PVE.qemu.Monitor', { + extend: 'Ext.panel.Panel', + + alias: 'widget.pveQemuMonitor', + + // start to trim saved command output once there are *both*, more than `commandLimit` commands + // executed and the total of saved in+output is over `lineLimit` lines; repeat by dropping one + // full command output until either condition is false again + commandLimit: 10, + lineLimit: 5000, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var history = []; + var histNum = -1; + let commands = []; + + var textbox = Ext.createWidget('panel', { + region: 'center', + xtype: 'panel', + autoScroll: true, + border: true, + margins: '5 5 5 5', + bodyStyle: 'font-family: monospace;', + }); + + var scrollToEnd = function() { + var el = textbox.getTargetEl(); + var dom = Ext.getDom(el); + + var clientHeight = dom.clientHeight; + // BrowserBug: clientHeight reports 0 in IE9 StrictMode + // Instead we are using offsetHeight and hardcoding borders + if (Ext.isIE9 && Ext.isStrict) { + clientHeight = dom.offsetHeight + 2; + } + dom.scrollTop = dom.scrollHeight - clientHeight; + }; + + var refresh = function() { + textbox.update(`
${commands.flat(2).join('\n')}
`); + scrollToEnd(); + }; + + let recordInput = line => { + commands.push([line]); + + // drop oldest commands and their output until we're not over both limits anymore + while (commands.length > me.commandLimit && commands.flat(2).length > me.lineLimit) { + commands.shift(); + } + }; + + let addResponse = lines => commands[commands.length - 1].push(lines); + + var executeCmd = function(cmd) { + recordInput("# " + Ext.htmlEncode(cmd), true); + if (cmd) { + history.unshift(cmd); + if (history.length > 20) { + history.splice(20); + } + } + histNum = -1; + + refresh(); + Proxmox.Utils.API2Request({ + params: { command: cmd }, + url: '/nodes/' + nodename + '/qemu/' + vmid + "/monitor", + method: 'POST', + waitMsgTarget: me, + success: function(response, opts) { + var res = response.result.data; + addResponse(res.split('\n').map(line => Ext.htmlEncode(line))); + refresh(); + }, + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + }); + }; + + Ext.apply(me, { + layout: { type: 'border' }, + border: false, + items: [ + textbox, + { + region: 'south', + margins: '0 5 5 5', + border: false, + xtype: 'textfield', + name: 'cmd', + value: '', + fieldStyle: 'font-family: monospace;', + allowBlank: true, + listeners: { + afterrender: function(f) { + f.focus(false); + recordInput("Type 'help' for help."); + refresh(); + }, + specialkey: function(f, e) { + var key = e.getKey(); + switch (key) { + case e.ENTER: + var cmd = f.getValue(); + f.setValue(''); + executeCmd(cmd); + break; + case e.PAGE_UP: + textbox.scrollBy(0, -0.9*textbox.getHeight(), false); + break; + case e.PAGE_DOWN: + textbox.scrollBy(0, 0.9*textbox.getHeight(), false); + break; + case e.UP: + if (histNum + 1 < history.length) { + f.setValue(history[++histNum]); + } + e.preventDefault(); + break; + case e.DOWN: + if (histNum > 0) { + f.setValue(history[--histNum]); + } + e.preventDefault(); + break; + default: + break; + } + }, + }, + }, + ], + listeners: { + show: function() { + var field = me.query('textfield[name="cmd"]')[0]; + field.focus(false, true); + }, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.qemu.MultiHDPanel', { + extend: 'PVE.panel.MultiDiskPanel', + alias: 'widget.pveMultiHDPanel', + + onlineHelp: 'qm_hard_disk', + + controller: { + xclass: 'Ext.app.ViewController', + + // maxCount is the sum of all controller ids - 1 (ide2 is fixed in the wizard) + maxCount: Object.values(PVE.Utils.diskControllerMaxIDs) + .reduce((previous, current) => previous+current, 0) - 1, + + getNextFreeDisk: function(vmconfig) { + let clist = PVE.Utils.sortByPreviousUsage(vmconfig); + return PVE.Utils.nextFreeDisk(clist, vmconfig); + }, + + addPanel: function(itemId, vmconfig, nextFreeDisk) { + let me = this; + return me.getView().add({ + vmconfig, + border: false, + showAdvanced: Ext.state.Manager.getProvider().get('proxmox-advanced-cb'), + xtype: 'pveQemuHDInputPanel', + bind: { + nodename: '{nodename}', + }, + padding: '0 0 0 5', + itemId, + isCreate: true, + insideWizard: true, + }); + }, + + getBaseVMConfig: function() { + let me = this; + let vm = me.getViewModel(); + + return { + ide2: 'media=cdrom', + scsihw: vm.get('current.scsihw'), + ostype: vm.get('current.ostype'), + }; + }, + + diskSorter: { + sorterFn: function(rec1, rec2) { + let [, name1, id1] = PVE.Utils.bus_match.exec(rec1.data.name); + let [, name2, id2] = PVE.Utils.bus_match.exec(rec2.data.name); + + if (name1 === name2) { + return parseInt(id1, 10) - parseInt(id2, 10); + } + + return name1 < name2 ? -1 : 1; + }, + }, + + deleteDisabled: () => false, + }, +}); +Ext.define('PVE.qemu.NetworkInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuNetworkInputPanel', + onlineHelp: 'qm_network_device', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + me.network.model = values.model; + if (values.nonetwork) { + return {}; + } else { + me.network.bridge = values.bridge; + me.network.tag = values.tag; + me.network.firewall = values.firewall; + } + me.network.macaddr = values.macaddr; + me.network.disconnect = values.disconnect; + me.network.queues = values.queues; + me.network.mtu = values.mtu; + + if (values.rate) { + me.network.rate = values.rate; + } else { + delete me.network.rate; + } + + var params = {}; + + params[me.confid] = PVE.Parser.printQemuNetwork(me.network); + + return params; + }, + + viewModel: { + data: { + networkModel: undefined, + mtu: '', + }, + formulas: { + isVirtio: get => get('networkModel') === 'virtio', + showMtuHint: get => get('mtu') === 1, + }, + }, + + setNetwork: function(confid, data) { + var me = this; + + me.confid = confid; + + if (data) { + data.networkmode = data.bridge ? 'bridge' : 'nat'; + } else { + data = {}; + data.networkmode = 'bridge'; + } + me.network = data; + + me.setValues(me.network); + }, + + setNodename: function(nodename) { + var me = this; + + me.bridgesel.setNodename(nodename); + }, + + initComponent: function() { + var me = this; + + me.network = {}; + me.confid = 'net0'; + + me.column1 = []; + me.column2 = []; + + me.bridgesel = Ext.create('PVE.form.BridgeSelector', { + name: 'bridge', + fieldLabel: gettext('Bridge'), + nodename: me.nodename, + autoSelect: true, + allowBlank: false, + }); + + me.column1 = [ + me.bridgesel, + { + xtype: 'pveVlanField', + name: 'tag', + value: '', + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Firewall'), + name: 'firewall', + checked: me.insideWizard || me.isCreate, + }, + ]; + + me.advancedColumn1 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Disconnect'), + name: 'disconnect', + }, + { + xtype: 'proxmoxintegerfield', + name: 'mtu', + fieldLabel: 'MTU', + bind: { + disabled: '{!isVirtio}', + value: '{mtu}', + }, + emptyText: '1500 (1 = bridge MTU)', + minValue: 1, + maxValue: 65520, + allowBlank: true, + validator: val => val === '' || val >= 576 || val === '1' + ? true + : gettext('MTU needs to be >= 576 or 1 to inherit the MTU from the underlying bridge.'), + }, + ]; + + if (me.insideWizard) { + me.column1.unshift({ + xtype: 'checkbox', + name: 'nonetwork', + inputValue: 'none', + boxLabel: gettext('No network device'), + listeners: { + change: function(cb, value) { + var fields = [ + 'disconnect', + 'bridge', + 'tag', + 'firewall', + 'model', + 'macaddr', + 'rate', + 'queues', + 'mtu', + ]; + fields.forEach(function(fieldname) { + me.down('field[name='+fieldname+']').setDisabled(value); + }); + me.down('field[name=bridge]').validate(); + }, + }, + }); + me.column2.unshift({ + xtype: 'displayfield', + }); + } + + me.column2.push( + { + xtype: 'pveNetworkCardSelector', + name: 'model', + fieldLabel: gettext('Model'), + bind: '{networkModel}', + value: PVE.qemu.OSDefaults.generic.networkCard, + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'macaddr', + fieldLabel: gettext('MAC address'), + vtype: 'MacAddress', + allowBlank: true, + emptyText: 'auto', + }); + me.advancedColumn2 = [ + { + xtype: 'numberfield', + name: 'rate', + fieldLabel: gettext('Rate limit') + ' (MB/s)', + minValue: 0, + maxValue: 10*1024, + value: '', + emptyText: 'unlimited', + allowBlank: true, + }, + { + xtype: 'proxmoxintegerfield', + name: 'queues', + fieldLabel: 'Multiqueue', + minValue: 1, + maxValue: 64, + value: '', + allowBlank: true, + }, + ]; + me.advancedColumnB = [ + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext("Use the special value '1' to inherit the MTU value from the underlying bridge"), + bind: { + hidden: '{!showMtuHint}', + }, + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.qemu.NetworkEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.isCreate = !me.confid; + + var ipanel = Ext.create('PVE.qemu.NetworkInputPanel', { + confid: me.confid, + nodename: nodename, + isCreate: me.isCreate, + }); + + Ext.applyIf(me, { + subject: gettext('Network Device'), + items: ipanel, + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var i, confid; + me.vmconfig = response.result.data; + if (!me.isCreate) { + var value = me.vmconfig[me.confid]; + var network = PVE.Parser.parseQemuNetwork(me.confid, value); + if (!network) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse network options'); + me.close(); + return; + } + ipanel.setNetwork(me.confid, network); + } else { + for (i = 0; i < 100; i++) { + confid = 'net' + i.toString(); + if (!Ext.isDefined(me.vmconfig[confid])) { + me.confid = confid; + break; + } + } + + let ostype = me.vmconfig.ostype; + let defaults = PVE.qemu.OSDefaults.getDefaults(ostype); + let data = { + model: defaults.networkCard, + }; + + ipanel.setNetwork(me.confid, data); + } + }, + }); + }, +}); +/* + * This class holds performance *recommended* settings for the PVE Qemu wizards + * the *mandatory* settings are set in the PVE::QemuServer + * config_to_command sub + * We store this here until we get the data from the API server +*/ + +// this is how you would add an hypothetic FreeBSD > 10 entry +// +//virtio-blk is stable but virtIO net still +// problematic as of 10.3 +// see https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=165059 +// addOS({ +// parent: 'generic', // inherits defaults +// pveOS: 'freebsd10', // must match a radiofield in OSTypeEdit.js +// busType: 'virtio' // must match a pveBusController value +// // networkCard muss match a pveNetworkCardSelector + + +Ext.define('PVE.qemu.OSDefaults', { + singleton: true, // will also force creation when loaded + + constructor: function() { + let me = this; + + let addOS = function(settings) { + if (Object.prototype.hasOwnProperty.call(settings, 'parent')) { + var child = Ext.clone(me[settings.parent]); + me[settings.pveOS] = Ext.apply(child, settings); + } else { + throw "Could not find your genitor"; + } + }; + + // default values + me.generic = { + busType: 'ide', + networkCard: 'e1000', + busPriority: { + ide: 4, + sata: 3, + scsi: 2, + virtio: 1, + }, + scsihw: 'virtio-scsi-single', + }; + + // virtio-net is in kernel since 2.6.25 + // virtio-scsi since 3.2 but backported in RHEL with 2.6 kernel + addOS({ + pveOS: 'l26', + parent: 'generic', + busType: 'scsi', + busPriority: { + scsi: 4, + virtio: 3, + sata: 2, + ide: 1, + }, + networkCard: 'virtio', + }); + + // recommandation from http://wiki.qemu.org/Windows2000 + addOS({ + pveOS: 'w2k', + parent: 'generic', + networkCard: 'rtl8139', + scsihw: '', + }); + // https://pve.proxmox.com/wiki/Windows_XP_Guest_Notes + addOS({ + pveOS: 'wxp', + parent: 'w2k', + }); + + me.getDefaults = function(ostype) { + if (PVE.qemu.OSDefaults[ostype]) { + return PVE.qemu.OSDefaults[ostype]; + } else { + return PVE.qemu.OSDefaults.generic; + } + }; + }, +}); +Ext.define('PVE.qemu.OSTypeInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuOSTypePanel', + onlineHelp: 'qm_os_settings', + insideWizard: false, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'combobox[name=osbase]': { + change: 'onOSBaseChange', + }, + 'combobox[name=ostype]': { + afterrender: 'onOSTypeChange', + change: 'onOSTypeChange', + }, + }, + onOSBaseChange: function(field, value) { + this.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]); + }, + onOSTypeChange: function(field) { + var me = this, ostype = field.getValue(); + if (!me.getView().insideWizard) { + return; + } + var targetValues = PVE.qemu.OSDefaults.getDefaults(ostype); + + me.setWidget('pveBusSelector', targetValues.busType); + me.setWidget('pveNetworkCardSelector', targetValues.networkCard); + var scsihw = targetValues.scsihw || '__default__'; + this.getViewModel().set('current.scsihw', scsihw); + this.getViewModel().set('current.ostype', ostype); + }, + setWidget: function(widget, newValue) { + // changing a widget is safe only if ComponentQuery.query returns us + // a single value array + var widgets = Ext.ComponentQuery.query('pveQemuCreateWizard ' + widget); + if (widgets.length === 1) { + widgets[0].setValue(newValue); + } else { + // ignore multiple disks, we only want to set the type if there is a single disk + } + }, + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: 'displayfield', + value: gettext('Guest OS') + ':', + hidden: !me.insideWizard, + }, + { + xtype: 'combobox', + submitValue: false, + name: 'osbase', + fieldLabel: gettext('Type'), + editable: false, + queryMode: 'local', + value: 'Linux', + store: Object.keys(PVE.Utils.kvm_ostypes), + }, + { + xtype: 'combobox', + name: 'ostype', + reference: 'ostype', + fieldLabel: gettext('Version'), + value: 'l26', + allowBlank: false, + editable: false, + queryMode: 'local', + valueField: 'val', + displayField: 'desc', + store: { + fields: ['desc', 'val'], + data: PVE.Utils.kvm_ostypes.Linux, + listeners: { + datachanged: function(store) { + var ostype = me.lookup('ostype'); + var old_val = ostype.getValue(); + if (!me.insideWizard && old_val && store.find('val', old_val) !== -1) { + ostype.setValue(old_val); + } else { + ostype.setValue(store.getAt(0)); + } + }, + }, + }, + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.qemu.OSTypeEdit', { + extend: 'Proxmox.window.Edit', + + subject: 'OS Type', + + items: [{ xtype: 'pveQemuOSTypePanel' }], + + initComponent: function() { + var me = this; + + me.callParent(); + + me.load({ + success: function(response, options) { + var value = response.result.data.ostype || 'other'; + var osinfo = PVE.Utils.get_kvm_osinfo(value); + me.setValues({ ostype: value, osbase: osinfo.base }); + }, + }); + }, +}); +Ext.define('PVE.qemu.Options', { + extend: 'Proxmox.grid.PendingObjectGrid', + alias: ['widget.PVE.qemu.Options'], + + onlineHelp: 'qm_options', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + var rows = { + name: { + required: true, + defaultValue: me.pveSelNode.data.name, + header: gettext('Name'), + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Name'), + items: { + xtype: 'inputpanel', + items: { + xtype: 'textfield', + name: 'name', + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Name'), + allowBlank: true, + }, + onGetValues: function(values) { + var params = values; + if (values.name === undefined || + values.name === null || + values.name === '') { + params = { 'delete': 'name' }; + } + return params; + }, + }, + } : undefined, + }, + onboot: { + header: gettext('Start at boot'), + defaultValue: '', + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Start at boot'), + items: { + xtype: 'proxmoxcheckbox', + name: 'onboot', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Start at boot'), + }, + } : undefined, + }, + startup: { + header: gettext('Start/Shutdown order'), + defaultValue: '', + renderer: PVE.Utils.render_kvm_startup, + editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] + ? { + xtype: 'pveWindowStartupEdit', + onlineHelp: 'qm_startup_and_shutdown', + } : undefined, + }, + ostype: { + header: gettext('OS Type'), + editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.OSTypeEdit' : undefined, + renderer: PVE.Utils.render_kvm_ostype, + defaultValue: 'other', + }, + bootdisk: { + visible: false, + }, + boot: { + header: gettext('Boot Order'), + defaultValue: 'cdn', + editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.BootOrderEdit' : undefined, + multiKey: ['boot', 'bootdisk'], + renderer: function(order, metaData, record, rowIndex, colIndex, store, pending) { + if (/^\s*$/.test(order)) { + return gettext('(No boot device selected)'); + } + let boot = PVE.Parser.parsePropertyString(order, "legacy"); + if (boot.order) { + let list = boot.order.split(';'); + let ret = ''; + list.forEach(dev => { + if (ret) { + ret += ', '; + } + ret += dev; + }); + return ret; + } + + // legacy style and fallback + let i; + var text = ''; + var bootdisk = me.getObjectValue('bootdisk', undefined, pending); + order = boot.legacy || 'cdn'; + for (i = 0; i < order.length; i++) { + if (text) { + text += ', '; + } + var sel = order.substring(i, i + 1); + if (sel === 'c') { + if (bootdisk) { + text += bootdisk; + } else { + text += gettext('first disk'); + } + } else if (sel === 'n') { + text += gettext('any net'); + } else if (sel === 'a') { + text += gettext('Floppy'); + } else if (sel === 'd') { + text += gettext('any CD-ROM'); + } else { + text += sel; + } + } + return text; + }, + }, + tablet: { + header: gettext('Use tablet for pointer'), + defaultValue: true, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Use tablet for pointer'), + items: { + xtype: 'proxmoxcheckbox', + name: 'tablet', + checked: true, + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled'), + }, + } : undefined, + }, + hotplug: { + header: gettext('Hotplug'), + defaultValue: 'disk,network,usb', + renderer: PVE.Utils.render_hotplug_features, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Hotplug'), + items: { + xtype: 'pveHotplugFeatureSelector', + name: 'hotplug', + value: '', + multiSelect: true, + fieldLabel: gettext('Hotplug'), + allowBlank: true, + }, + } : undefined, + }, + acpi: { + header: gettext('ACPI support'), + defaultValue: true, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('ACPI support'), + items: { + xtype: 'proxmoxcheckbox', + name: 'acpi', + checked: true, + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled'), + }, + } : undefined, + }, + kvm: { + header: gettext('KVM hardware virtualization'), + defaultValue: true, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('KVM hardware virtualization'), + items: { + xtype: 'proxmoxcheckbox', + name: 'kvm', + checked: true, + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled'), + }, + } : undefined, + }, + freeze: { + header: gettext('Freeze CPU at startup'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.PowerMgmt'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Freeze CPU at startup'), + items: { + xtype: 'proxmoxcheckbox', + name: 'freeze', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + labelWidth: 140, + fieldLabel: gettext('Freeze CPU at startup'), + }, + } : undefined, + }, + localtime: { + header: gettext('Use local time for RTC'), + defaultValue: '__default__', + renderer: PVE.Utils.render_localtime, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Use local time for RTC'), + width: 400, + items: { + xtype: 'proxmoxKVComboBox', + name: 'localtime', + value: '__default__', + comboItems: [ + ['__default__', PVE.Utils.render_localtime('__default__')], + [1, PVE.Utils.render_localtime(1)], + [0, PVE.Utils.render_localtime(0)], + ], + labelWidth: 140, + fieldLabel: gettext('Use local time for RTC'), + }, + } : undefined, + }, + startdate: { + header: gettext('RTC start date'), + defaultValue: 'now', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('RTC start date'), + items: { + xtype: 'proxmoxtextfield', + name: 'startdate', + deleteEmpty: true, + value: 'now', + fieldLabel: gettext('RTC start date'), + vtype: 'QemuStartDate', + allowBlank: true, + }, + } : undefined, + }, + smbios1: { + header: gettext('SMBIOS settings (type1)'), + defaultValue: '', + renderer: Ext.String.htmlEncode, + editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.Smbios1Edit' : undefined, + }, + agent: { + header: 'QEMU Guest Agent', + defaultValue: false, + renderer: PVE.Utils.render_qga_features, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Qemu Agent'), + width: 350, + onlineHelp: 'qm_qemu_agent', + items: { + xtype: 'pveAgentFeatureSelector', + name: 'agent', + }, + } : undefined, + }, + protection: { + header: gettext('Protection'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Protection'), + items: { + xtype: 'proxmoxcheckbox', + name: 'protection', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled'), + }, + } : undefined, + }, + spice_enhancements: { + header: gettext('Spice Enhancements'), + defaultValue: false, + renderer: PVE.Utils.render_spice_enhancements, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Spice Enhancements'), + onlineHelp: 'qm_spice_enhancements', + items: { + xtype: 'pveSpiceEnhancementSelector', + name: 'spice_enhancements', + }, + } : undefined, + }, + vmstatestorage: { + header: gettext('VM State storage'), + defaultValue: '', + renderer: val => val || gettext('Automatic'), + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('VM State storage'), + onlineHelp: 'chapter_virtual_machines', // FIXME: use 'qm_vmstatestorage' once available + width: 350, + items: { + xtype: 'pveStorageSelector', + storageContent: 'images', + allowBlank: true, + emptyText: gettext("Automatic (Storage used by the VM, or 'local')"), + autoSelect: false, + deleteEmpty: true, + skipEmptyText: true, + nodename: nodename, + name: 'vmstatestorage', + }, + } : undefined, + }, + hookscript: { + header: gettext('Hookscript'), + }, + }; + + var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config'; + + var edit_btn = new Ext.Button({ + text: gettext('Edit'), + disabled: true, + handler: function() { me.run_editor(); }, + }); + + var revert_btn = new PVE.button.PendingRevert(); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + + var key = rec.data.key; + var pending = rec.data.delete || me.hasPendingChanges(key); + var rowdef = rows[key]; + + edit_btn.setDisabled(!rowdef.editor); + revert_btn.setDisabled(!pending); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/pending", + interval: 5000, + cwidth1: 250, + tbar: [edit_btn, revert_btn], + rows: rows, + editorConfig: { + url: "/api2/extjs/" + baseurl, + }, + listeners: { + itemdblclick: me.run_editor, + selectionchange: set_button_status, + }, + }); + + me.callParent(); + + me.on('activate', () => me.rstore.startUpdate()); + me.on('destroy', () => me.rstore.stopUpdate()); + me.on('deactivate', () => me.rstore.stopUpdate()); + + me.mon(me.getStore(), 'datachanged', function() { + set_button_status(); + }); + }, +}); + +Ext.define('PVE.qemu.PCIInputPanel', { + extend: 'Proxmox.panel.InputPanel', + + onlineHelp: 'qm_pci_passthrough_vm_config', + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + + var hostpci = me.vmconfig[me.confid] || ''; + + var values = PVE.Parser.parsePropertyString(hostpci, 'host'); + if (values.host) { + if (!values.host.match(/^[0-9a-f]{4}:/i)) { // add optional domain + values.host = "0000:" + values.host; + } + if (values.host.length < 11) { // 0000:00:00 format not 0000:00:00.0 + values.host += ".0"; + values.multifunction = true; + } + } + + values['x-vga'] = PVE.Parser.parseBoolean(values['x-vga'], 0); + values.pcie = PVE.Parser.parseBoolean(values.pcie, 0); + values.rombar = PVE.Parser.parseBoolean(values.rombar, 1); + + me.setValues(values); + if (!me.vmconfig.machine || me.vmconfig.machine.indexOf('q35') === -1) { + // machine is not set to some variant of q35, so we disable pcie + var pcie = me.down('field[name=pcie]'); + pcie.setDisabled(true); + pcie.setBoxLabel(gettext('Q35 only')); + } + + if (values.romfile) { + me.down('field[name=romfile]').setVisible(true); + } + }, + + onGetValues: function(values) { + let me = this; + if (!me.confid) { + for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) { + if (!me.vmconfig['hostpci' + i.toString()]) { + me.confid = 'hostpci' + i.toString(); + break; + } + } + // FIXME: what if no confid was found?? + } + values.host.replace(/^0000:/, ''); // remove optional '0000' domain + + if (values.multifunction) { + values.host = values.host.substring(0, values.host.indexOf('.')); // skip the '.X' + delete values.multifunction; + } + + if (values.rombar) { + delete values.rombar; + } else { + values.rombar = 0; + } + + if (!values.romfile) { + delete values.romfile; + } + + let ret = {}; + ret[me.confid] = PVE.Parser.printPropertyString(values, 'host'); + return ret; + }, + + initComponent: function() { + let me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + me.column1 = [ + { + xtype: 'pvePCISelector', + fieldLabel: gettext('Device'), + name: 'host', + nodename: me.nodename, + allowBlank: false, + onLoadCallBack: function(store, records, success) { + if (!success || !records.length) { + return; + } + if (records.every((val) => val.data.iommugroup === -1)) { // no IOMMU groups + let warning = Ext.create('Ext.form.field.Display', { + columnWidth: 1, + padding: '0 0 10 0', + value: 'No IOMMU detected, please activate it.' + + 'See Documentation for further information.', + userCls: 'pmx-hint', + }); + me.items.insert(0, warning); + me.updateLayout(); // insert does not trigger that + } + }, + listeners: { + change: function(pcisel, value) { + if (!value) { + return; + } + let pciDev = pcisel.getStore().getById(value); + let mdevfield = me.down('field[name=mdev]'); + mdevfield.setDisabled(!pciDev || !pciDev.data.mdev); + if (!pciDev) { + return; + } + if (pciDev.data.mdev) { + mdevfield.setPciID(value); + } + let iommu = pciDev.data.iommugroup; + if (iommu === -1) { + return; + } + // try to find out if there are more devices in that iommu group + let id = pciDev.data.id.substring(0, 5); // 00:00 + let count = 0; + pcisel.getStore().each(({ data }) => { + if (data.iommugroup === iommu && data.id.substring(0, 5) !== id) { + count++; + return false; + } + return true; + }); + let warning = me.down('#iommuwarning'); + if (count && !warning) { + warning = Ext.create('Ext.form.field.Display', { + columnWidth: 1, + padding: '0 0 10 0', + itemId: 'iommuwarning', + value: 'The selected Device is not in a seperate IOMMU group, make sure this is intended.', + userCls: 'pmx-hint', + }); + me.items.insert(0, warning); + me.updateLayout(); // insert does not trigger that + } else if (!count && warning) { + me.remove(warning); + } + }, + }, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('All Functions'), + name: 'multifunction', + }, + ]; + + me.column2 = [ + { + xtype: 'pveMDevSelector', + name: 'mdev', + disabled: true, + fieldLabel: gettext('MDev Type'), + nodename: me.nodename, + listeners: { + change: function(field, value) { + let multiFunction = me.down('field[name=multifunction]'); + if (value) { + multiFunction.setValue(false); + } + multiFunction.setDisabled(!!value); + }, + }, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Primary GPU'), + name: 'x-vga', + }, + ]; + + me.advancedColumn1 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'ROM-Bar', + name: 'rombar', + }, + { + xtype: 'displayfield', + submitValue: true, + hidden: true, + fieldLabel: 'ROM-File', + name: 'romfile', + }, + { + xtype: 'textfield', + name: 'vendor-id', + fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Vendor')), + emptyText: gettext('From Device'), + vtype: 'PciId', + allowBlank: true, + submitEmpty: false, + }, + { + xtype: 'textfield', + name: 'device-id', + fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Device')), + emptyText: gettext('From Device'), + vtype: 'PciId', + allowBlank: true, + submitEmpty: false, + }, + ]; + + me.advancedColumn2 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'PCI-Express', + name: 'pcie', + }, + { + xtype: 'textfield', + name: 'sub-vendor-id', + fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Sub-Vendor')), + emptyText: gettext('From Device'), + vtype: 'PciId', + allowBlank: true, + submitEmpty: false, + }, + { + xtype: 'textfield', + name: 'sub-device-id', + fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Sub-Device')), + emptyText: gettext('From Device'), + vtype: 'PciId', + allowBlank: true, + submitEmpty: false, + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.qemu.PCIEdit', { + extend: 'Proxmox.window.Edit', + + subject: gettext('PCI Device'), + + vmconfig: undefined, + isAdd: true, + + initComponent: function() { + let me = this; + + me.isCreate = !me.confid; + + let ipanel = Ext.create('PVE.qemu.PCIInputPanel', { + confid: me.confid, + pveSelNode: me.pveSelNode, + }); + + Ext.apply(me, { + items: [ipanel], + }); + + me.callParent(); + + me.load({ + success: ({ result }) => ipanel.setVMConfig(result.data), + }); + }, +}); +// The view model of the parent shoul contain a 'cgroupMode' variable (or params for v2 are used). +Ext.define('PVE.qemu.ProcessorInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuProcessorPanel', + onlineHelp: 'qm_cpu', + + insideWizard: false, + + viewModel: { + data: { + socketCount: 1, + coreCount: 1, + showCustomModelPermWarning: false, + userIsRoot: false, + }, + formulas: { + totalCoreCount: get => get('socketCount') * get('coreCount'), + cpuunitsDefault: (get) => get('cgroupMode') === 1 ? 1024 : 100, + cpuunitsMin: (get) => get('cgroupMode') === 1 ? 2 : 1, + cpuunitsMax: (get) => get('cgroupMode') === 1 ? 262144 : 10000, + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + init: function() { + let me = this; + let viewModel = me.getViewModel(); + + viewModel.set('userIsRoot', Proxmox.UserName === 'root@pam'); + }, + }, + + onGetValues: function(values) { + let me = this; + let cpuunitsDefault = me.getViewModel().get('cpuunitsDefault'); + + if (Array.isArray(values.delete)) { + values.delete = values.delete.join(','); + } + + PVE.Utils.delete_if_default(values, 'cpulimit', '0', me.insideWizard); + PVE.Utils.delete_if_default(values, 'cpuunits', `${cpuunitsDefault}`, me.insideWizard); + + // build the cpu options: + me.cpu.cputype = values.cputype; + + if (values.flags) { + me.cpu.flags = values.flags; + } else { + delete me.cpu.flags; + } + + delete values.cputype; + delete values.flags; + var cpustring = PVE.Parser.printQemuCpu(me.cpu); + + // remove cputype delete request: + var del = values.delete; + delete values.delete; + if (del) { + del = del.split(','); + Ext.Array.remove(del, 'cputype'); + } else { + del = []; + } + + if (cpustring) { + values.cpu = cpustring; + } else { + del.push('cpu'); + } + + var delarr = del.join(','); + if (delarr) { + values.delete = delarr; + } + + return values; + }, + + setValues: function(values) { + let me = this; + + let type = values.cputype; + let typeSelector = me.lookupReference('cputype'); + let typeStore = typeSelector.getStore(); + typeStore.on('load', (store, records, success) => { + if (!success || !type || records.some(x => x.data.name === type)) { + return; + } + + // if we get here, a custom CPU model is selected for the VM but we + // don't have permission to configure it - it will not be in the + // list retrieved from the API, so add it manually to allow changing + // other processor options + typeStore.add({ + name: type, + displayname: type.replace(/^custom-/, ''), + custom: 1, + vendor: gettext("Unknown"), + }); + typeSelector.select(type); + }); + + me.callParent([values]); + }, + + cpu: {}, + + column1: [ + { + xtype: 'proxmoxintegerfield', + name: 'sockets', + minValue: 1, + maxValue: 4, + value: '1', + fieldLabel: gettext('Sockets'), + allowBlank: false, + bind: { + value: '{socketCount}', + }, + }, + { + xtype: 'proxmoxintegerfield', + name: 'cores', + minValue: 1, + maxValue: 128, + value: '1', + fieldLabel: gettext('Cores'), + allowBlank: false, + bind: { + value: '{coreCount}', + }, + }, + ], + + column2: [ + { + xtype: 'CPUModelSelector', + name: 'cputype', + reference: 'cputype', + fieldLabel: gettext('Type'), + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Total cores'), + name: 'totalcores', + isFormField: false, + bind: { + value: '{totalCoreCount}', + }, + }, + ], + + columnB: [ + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext('WARNING: You do not have permission to configure custom CPU types, if you change the type you will not be able to go back!'), + hidden: true, + bind: { + hidden: '{!showCustomModelPermWarning}', + }, + }, + ], + + advancedColumn1: [ + { + xtype: 'proxmoxintegerfield', + name: 'vcpus', + minValue: 1, + maxValue: 1, + value: '', + fieldLabel: gettext('VCPUs'), + deleteEmpty: true, + allowBlank: true, + emptyText: '1', + bind: { + emptyText: '{totalCoreCount}', + maxValue: '{totalCoreCount}', + }, + }, + { + xtype: 'numberfield', + name: 'cpulimit', + minValue: 0, + maxValue: 128, // api maximum + value: '', + step: 1, + fieldLabel: gettext('CPU limit'), + allowBlank: true, + emptyText: gettext('unlimited'), + }, + { + xtype: 'proxmoxtextfield', + name: 'affinity', + vtype: 'CpuSet', + value: '', + fieldLabel: gettext('CPU Affinity'), + allowBlank: true, + emptyText: gettext("All Cores"), + deleteEmpty: true, + bind: { + disabled: '{!userIsRoot}', + }, + }, + ], + + advancedColumn2: [ + { + xtype: 'proxmoxintegerfield', + name: 'cpuunits', + fieldLabel: gettext('CPU units'), + minValue: '1', + maxValue: '10000', + value: '', + emptyText: '100', + bind: { + minValue: '{cpuunitsMin}', + maxValue: '{cpuunitsMax}', + emptyText: '{cpuunitsDefault}', + }, + deleteEmpty: true, + allowBlank: true, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enable NUMA'), + name: 'numa', + uncheckedValue: 0, + }, + ], + advancedColumnB: [ + { + xtype: 'label', + text: 'Extra CPU Flags:', + }, + { + xtype: 'vmcpuflagselector', + name: 'flags', + }, + ], +}); + +Ext.define('PVE.qemu.ProcessorEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveQemuProcessorEdit', + + width: 700, + + viewModel: { + data: { + cgroupMode: 2, + }, + }, + + initComponent: function() { + let me = this; + me.getViewModel().set('cgroupMode', me.cgroupMode); + + var ipanel = Ext.create('PVE.qemu.ProcessorInputPanel'); + + Ext.apply(me, { + subject: gettext('Processors'), + items: ipanel, + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var data = response.result.data; + var value = data.cpu; + if (value) { + var cpu = PVE.Parser.parseQemuCpu(value); + ipanel.cpu = cpu; + data.cputype = cpu.cputype; + if (cpu.flags) { + data.flags = cpu.flags; + } + + let caps = Ext.state.Manager.get('GuiCap'); + if (data.cputype.indexOf('custom-') === 0 && + !caps.nodes['Sys.Audit']) { + let vm = ipanel.getViewModel(); + vm.set("showCustomModelPermWarning", true); + } + } + me.setValues(data); + }, + }); + }, +}); +Ext.define('PVE.qemu.BiosEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveQemuBiosEdit', + + onlineHelp: 'qm_bios_and_uefi', + subject: 'BIOS', + autoLoad: true, + + viewModel: { + data: { + bios: '__default__', + efidisk0: false, + }, + formulas: { + showEFIDiskHint: (get) => get('bios') === 'ovmf' && !get('efidisk0'), + }, + }, + + items: [ + { + xtype: 'pveQemuBiosSelector', + onlineHelp: 'qm_bios_and_uefi', + name: 'bios', + value: '__default__', + bind: '{bios}', + fieldLabel: 'BIOS', + }, + { + xtype: 'displayfield', + name: 'efidisk0', + bind: '{efidisk0}', + hidden: true, + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext('You need to add an EFI disk for storing the EFI settings. See the online help for details.'), + bind: { + hidden: '{!showEFIDiskHint}', + }, + }, + ], +}); +Ext.define('PVE.qemu.RNGInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveRNGInputPanel', + + onlineHelp: 'qm_virtio_rng', + + onGetValues: function(values) { + if (values.max_bytes === "") { + values.max_bytes = "0"; + } else if (values.max_bytes === "1024" && values.period === "") { + delete values.max_bytes; + } + + var ret = PVE.Parser.printPropertyString(values); + + return { + rng0: ret, + }; + }, + + setValues: function(values) { + if (values.max_bytes === 0) { + values.max_bytes = null; + } + + this.callParent(arguments); + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + '#max_bytes': { + change: function(el, newVal) { + let limitWarning = this.lookupReference('limitWarning'); + limitWarning.setHidden(!!newVal); + }, + }, + '#source': { + change: function(el, newVal) { + let limitWarning = this.lookupReference('sourceWarning'); + limitWarning.setHidden(newVal !== '/dev/random'); + }, + }, + }, + }, + + items: [{ + itemId: 'source', + name: 'source', + xtype: 'proxmoxKVComboBox', + value: '/dev/urandom', + fieldLabel: gettext('Entropy source'), + labelWidth: 130, + comboItems: [ + ['/dev/urandom', '/dev/urandom'], + ['/dev/random', '/dev/random'], + ['/dev/hwrng', '/dev/hwrng'], + ], + }, + { + xtype: 'numberfield', + itemId: 'max_bytes', + name: 'max_bytes', + minValue: 0, + step: 1, + value: 1024, + fieldLabel: gettext('Limit (Bytes/Period)'), + labelWidth: 130, + emptyText: gettext('unlimited'), + }, + { + xtype: 'numberfield', + name: 'period', + minValue: 1, + step: 1, + fieldLabel: gettext('Period') + ' (ms)', + labelWidth: 130, + emptyText: '1000', + }, + { + xtype: 'displayfield', + reference: 'sourceWarning', + value: gettext('Using /dev/random as entropy source is discouraged, as it can lead to host entropy starvation. /dev/urandom is preferred, and does not lead to a decrease in security in practice.'), + userCls: 'pmx-hint', + hidden: true, + }, + { + xtype: 'displayfield', + reference: 'limitWarning', + value: gettext('Disabling the limiter can potentially allow a guest to overload the host. Proceed with caution.'), + userCls: 'pmx-hint', + hidden: true, + }], +}); + +Ext.define('PVE.qemu.RNGEdit', { + extend: 'Proxmox.window.Edit', + + subject: gettext('VirtIO RNG'), + + items: [{ + xtype: 'pveRNGInputPanel', + }], + + initComponent: function() { + var me = this; + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response) { + me.vmconfig = response.result.data; + + var rng0 = me.vmconfig.rng0; + if (rng0) { + me.setValues(PVE.Parser.parsePropertyString(rng0)); + } + }, + }); + } + }, +}); +Ext.define('PVE.qemu.SSHKeyInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveQemuSSHKeyInputPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + if (values.sshkeys) { + values.sshkeys.trim(); + } + if (!values.sshkeys.length) { + values = {}; + values.delete = 'sshkeys'; + return values; + } else { + values.sshkeys = encodeURIComponent(values.sshkeys); + } + return values; + }, + + items: [ + { + xtype: 'textarea', + itemId: 'sshkeys', + name: 'sshkeys', + height: 250, + }, + { + xtype: 'filebutton', + itemId: 'filebutton', + name: 'file', + text: gettext('Load SSH Key File'), + fieldLabel: 'test', + listeners: { + change: function(btn, e, value) { + let view = this.up('inputpanel'); + e = e.event; + Ext.Array.each(e.target.files, function(file) { + PVE.Utils.loadSSHKeyFromFile(file, function(res) { + let keysField = view.down('#sshkeys'); + var old = keysField.getValue(); + keysField.setValue(old + res); + }); + }); + btn.reset(); + }, + }, + }, + ], + + initComponent: function() { + var me = this; + + me.callParent(); + if (!window.FileReader) { + me.down('#filebutton').setVisible(false); + } + }, +}); + +Ext.define('PVE.qemu.SSHKeyEdit', { + extend: 'Proxmox.window.Edit', + + width: 800, + + initComponent: function() { + var me = this; + + var ipanel = Ext.create('PVE.qemu.SSHKeyInputPanel'); + + Ext.apply(me, { + subject: gettext('SSH Keys'), + items: [ipanel], + }); + + me.callParent(); + + if (!me.create) { + me.load({ + success: function(response, options) { + var data = response.result.data; + if (data.sshkeys) { + data.sshkeys = decodeURIComponent(data.sshkeys); + ipanel.setValues(data); + } + }, + }); + } + }, +}); +Ext.define('PVE.qemu.ScsiHwEdit', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + Ext.applyIf(me, { + subject: gettext('SCSI Controller Type'), + items: { + xtype: 'pveScsiHwSelector', + name: 'scsihw', + value: '__default__', + fieldLabel: gettext('Type'), + }, + }); + + me.callParent(); + + me.load(); + }, +}); +Ext.define('PVE.qemu.SerialnputPanel', { + extend: 'Proxmox.panel.InputPanel', + + autoComplete: false, + + setVMConfig: function(vmconfig) { + var me = this, i; + me.vmconfig = vmconfig; + + for (i = 0; i < 4; i++) { + var port = 'serial' + i.toString(); + if (!me.vmconfig[port]) { + me.down('field[name=serialid]').setValue(i); + break; + } + } + }, + + onGetValues: function(values) { + var me = this; + + var id = 'serial' + values.serialid; + delete values.serialid; + values[id] = 'socket'; + return values; + }, + + items: [ + { + xtype: 'proxmoxintegerfield', + name: 'serialid', + fieldLabel: gettext('Serial Port'), + minValue: 0, + maxValue: 3, + allowBlank: false, + validator: function(id) { + if (!this.rendered) { + return true; + } + let view = this.up('panel'); + if (view.vmconfig !== undefined && Ext.isDefined(view.vmconfig['serial' + id])) { + return "This device is already in use."; + } + return true; + }, + }, + ], +}); + +Ext.define('PVE.qemu.SerialEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + isAdd: true, + + subject: gettext('Serial Port'), + + initComponent: function() { + var me = this; + + // for now create of (socket) serial port only + me.isCreate = true; + + var ipanel = Ext.create('PVE.qemu.SerialnputPanel', {}); + + Ext.apply(me, { + items: [ipanel], + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + }, + }); + }, +}); +Ext.define('PVE.qemu.Smbios1InputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.PVE.qemu.Smbios1InputPanel', + + insideWizard: false, + + smbios1: {}, + + onGetValues: function(values) { + var me = this; + + var params = { + smbios1: PVE.Parser.printQemuSmbios1(values), + }; + + return params; + }, + + setSmbios1: function(data) { + var me = this; + + me.smbios1 = data; + + me.setValues(me.smbios1); + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: 'UUID', + regex: /^[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$/, + name: 'uuid', + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Manufacturer'), + fieldStyle: { + height: '2em', + minHeight: '2em', + }, + name: 'manufacturer', + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Product'), + fieldStyle: { + height: '2em', + minHeight: '2em', + }, + name: 'product', + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Version'), + fieldStyle: { + height: '2em', + minHeight: '2em', + }, + name: 'version', + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Serial'), + fieldStyle: { + height: '2em', + minHeight: '2em', + }, + name: 'serial', + }, + { + xtype: 'textareafield', + fieldLabel: 'SKU', + fieldStyle: { + height: '2em', + minHeight: '2em', + }, + name: 'sku', + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Family'), + fieldStyle: { + height: '2em', + minHeight: '2em', + }, + name: 'family', + }, + ], +}); + +Ext.define('PVE.qemu.Smbios1Edit', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + var ipanel = Ext.create('PVE.qemu.Smbios1InputPanel', {}); + + Ext.applyIf(me, { + subject: gettext('SMBIOS settings (type1)'), + width: 450, + items: ipanel, + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + me.vmconfig = response.result.data; + var value = me.vmconfig.smbios1; + if (value) { + var data = PVE.Parser.parseQemuSmbios1(value); + if (!data) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse smbios options'); + me.close(); + return; + } + ipanel.setSmbios1(data); + } + }, + }); + }, +}); +Ext.define('PVE.qemu.SystemInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveQemuSystemPanel', + + onlineHelp: 'qm_system_settings', + + viewModel: { + data: { + efi: false, + addefi: true, + }, + + formulas: { + efidisk: function(get) { + return get('efi') && get('addefi'); + }, + }, + }, + + onGetValues: function(values) { + if (values.vga && values.vga.substr(0, 6) === 'serial') { + values['serial' + values.vga.substr(6, 1)] = 'socket'; + } + + delete values.hdimage; + delete values.hdstorage; + delete values.diskformat; + + delete values.preEnrolledKeys; // efidisk + delete values.version; // tpmstate + + return values; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + scsihwChange: function(field, value) { + var me = this; + if (me.getView().insideWizard) { + me.getViewModel().set('current.scsihw', value); + } + }, + + biosChange: function(field, value) { + var me = this; + if (me.getView().insideWizard) { + me.getViewModel().set('efi', value === 'ovmf'); + } + }, + + control: { + 'pveScsiHwSelector': { + change: 'scsihwChange', + }, + 'pveQemuBiosSelector': { + change: 'biosChange', + }, + '#': { + afterrender: 'setMachine', + }, + }, + + setMachine: function() { + let me = this; + let vm = this.getViewModel(); + let ostype = vm.get('current.ostype'); + if (ostype === 'win11') { + me.lookup('machine').setValue('q35'); + me.lookup('bios').setValue('ovmf'); + me.lookup('addtpmbox').setValue(true); + } + }, + }, + + column1: [ + { + xtype: 'proxmoxKVComboBox', + value: '__default__', + deleteEmpty: false, + fieldLabel: gettext('Graphic card'), + name: 'vga', + comboItems: Object.entries(PVE.Utils.kvm_vga_drivers), + }, + { + xtype: 'proxmoxKVComboBox', + name: 'machine', + reference: 'machine', + value: '__default__', + fieldLabel: gettext('Machine'), + comboItems: [ + ['__default__', PVE.Utils.render_qemu_machine('')], + ['q35', 'q35'], + ], + }, + { + xtype: 'displayfield', + value: gettext('Firmware'), + }, + { + xtype: 'pveQemuBiosSelector', + name: 'bios', + reference: 'bios', + value: '__default__', + fieldLabel: 'BIOS', + }, + { + xtype: 'proxmoxcheckbox', + bind: { + value: '{addefi}', + hidden: '{!efi}', + disabled: '{!efi}', + }, + hidden: true, + submitValue: false, + disabled: true, + fieldLabel: gettext('Add EFI Disk'), + }, + { + xtype: 'pveEFIDiskInputPanel', + name: 'efidisk0', + storageContent: 'images', + bind: { + nodename: '{nodename}', + hidden: '{!efi}', + disabled: '{!efidisk}', + }, + autoSelect: false, + disabled: true, + hidden: true, + hideSize: true, + usesEFI: true, + }, + ], + + column2: [ + { + xtype: 'pveScsiHwSelector', + name: 'scsihw', + value: '__default__', + bind: { + value: '{current.scsihw}', + }, + fieldLabel: gettext('SCSI Controller'), + }, + { + xtype: 'proxmoxcheckbox', + name: 'agent', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Qemu Agent'), + }, + { + // fake for spacing + xtype: 'displayfield', + value: ' ', + }, + { + xtype: 'proxmoxcheckbox', + reference: 'addtpmbox', + bind: { + value: '{addtpm}', + }, + submitValue: false, + fieldLabel: gettext('Add TPM'), + }, + { + xtype: 'pveTPMDiskInputPanel', + name: 'tpmstate0', + storageContent: 'images', + bind: { + nodename: '{nodename}', + hidden: '{!addtpm}', + disabled: '{!addtpm}', + }, + disabled: true, + hidden: true, + }, + ], + +}); +Ext.define('PVE.qemu.USBInputPanel', { + extend: 'Proxmox.panel.InputPanel', + mixins: ['Proxmox.Mixin.CBind'], + + autoComplete: false, + onlineHelp: 'qm_usb_passthrough', + + viewModel: { + data: {}, + }, + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + let max_usb = PVE.Utils.get_max_usb_count(me.vmconfig.ostype, me.vmconfig.machine); + if (max_usb > PVE.Utils.hardware_counts.usb_old) { + me.down('field[name=usb3]').setDisabled(true); + } + }, + + onGetValues: function(values) { + var me = this; + if (!me.confid) { + let max_usb = PVE.Utils.get_max_usb_count(me.vmconfig.ostype, me.vmconfig.machine); + for (let i = 0; i < max_usb; i++) { + let id = 'usb' + i.toString(); + if (!me.vmconfig[id]) { + me.confid = id; + break; + } + } + } + var val = ""; + var type = me.down('radiofield').getGroupValue(); + switch (type) { + case 'spice': + val = 'spice'; + break; + case 'hostdevice': + case 'port': + val = 'host=' + values[type]; + delete values[type]; + break; + default: + throw "invalid type selected"; + } + + if (values.usb3) { + delete values.usb3; + val += ',usb3=1'; + } + values[me.confid] = val; + return values; + }, + + items: [ + { + xtype: 'fieldcontainer', + defaultType: 'radiofield', + layout: 'fit', + items: [ + { + name: 'usb', + inputValue: 'spice', + boxLabel: gettext('Spice Port'), + submitValue: false, + checked: true, + }, + { + name: 'usb', + inputValue: 'hostdevice', + boxLabel: gettext('Use USB Vendor/Device ID'), + reference: 'hostdevice', + submitValue: false, + }, + { + xtype: 'pveUSBSelector', + disabled: true, + type: 'device', + name: 'hostdevice', + cbind: { pveSelNode: '{pveSelNode}' }, + bind: { disabled: '{!hostdevice.checked}' }, + editable: true, + allowBlank: false, + fieldLabel: gettext('Choose Device'), + labelAlign: 'right', + }, + { + name: 'usb', + inputValue: 'port', + boxLabel: gettext('Use USB Port'), + reference: 'port', + submitValue: false, + }, + { + xtype: 'pveUSBSelector', + disabled: true, + name: 'port', + cbind: { pveSelNode: '{pveSelNode}' }, + bind: { disabled: '{!port.checked}' }, + editable: true, + type: 'port', + allowBlank: false, + fieldLabel: gettext('Choose Port'), + labelAlign: 'right', + }, + { + xtype: 'checkbox', + name: 'usb3', + inputValue: true, + checked: true, + reference: 'usb3', + fieldLabel: gettext('Use USB3'), + }, + ], + }, + ], +}); + +Ext.define('PVE.qemu.USBEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + isAdd: true, + width: 400, + subject: gettext('USB Device'), + + initComponent: function() { + var me = this; + + me.isCreate = !me.confid; + + var ipanel = Ext.create('PVE.qemu.USBInputPanel', { + confid: me.confid, + pveSelNode: me.pveSelNode, + }); + + Ext.apply(me, { + items: [ipanel], + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.isCreate) { + return; + } + + var data = response.result.data[me.confid].split(','); + var port, hostdevice, usb3 = false; + var type = 'spice'; + + for (let i = 0; i < data.length; i++) { + if (/^(host=)?(0x)?[a-zA-Z0-9]{4}:(0x)?[a-zA-Z0-9]{4}$/.test(data[i])) { + hostdevice = data[i]; + hostdevice = hostdevice.replace('host=', '').replace('0x', ''); + type = 'hostdevice'; + } else if (/^(host=)?(\d+)-(\d+(\.\d+)*)$/.test(data[i])) { + port = data[i]; + port = port.replace('host=', ''); + type = 'port'; + } + + if (/^usb3=(1|on|true)$/.test(data[i])) { + usb3 = true; + } + } + var values = { + usb: type, + hostdevice: hostdevice, + port: port, + usb3: usb3, + }; + + ipanel.setValues(values); + }, + }); + }, +}); +Ext.define('PVE.sdn.Browser', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.sdn.Browser', + + onlineHelp: 'chapter_pvesdn', + + initComponent: function() { + let me = this; + + let nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + let sdnId = me.pveSelNode.data.sdn; + if (!sdnId) { + throw "no sdn ID specified"; + } + + me.items = []; + + Ext.apply(me, { + title: Ext.String.format(gettext("Zone {0} on node {1}"), `'${sdnId}'`, `'${nodename}'`), + hstateid: 'sdntab', + }); + + const caps = Ext.state.Manager.get('GuiCap'); + + if (caps.sdn['SDN.Audit']) { + me.items.push({ + xtype: 'pveSDNZoneContentView', + title: gettext('Content'), + iconCls: 'fa fa-th', + itemId: 'content', + }); + } + if (caps.sdn['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: `/sdn/zones/${sdnId}`, + }); + } + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.ControllerView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveSDNControllerView'], + + onlineHelp: 'pvesdn_config_controllers', + + stateful: true, + stateId: 'grid-sdn-controller', + + createSDNControllerEditWindow: function(type, sid) { + var schema = PVE.Utils.sdncontrollerSchema[type]; + if (!schema || !schema.ipanel) { + throw "no editor registered for controller type: " + type; + } + + Ext.create('PVE.sdn.controllers.BaseEdit', { + paneltype: 'PVE.sdn.controllers.' + schema.ipanel, + type: type, + controllerid: sid, + autoShow: true, + listeners: { + destroy: this.reloadStore, + }, + }); + }, + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-sdn-controller', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/sdn/controllers?pending=1", + }, + sorters: { + property: 'controller', + direction: 'ASC', + }, + }); + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + let type = rec.data.type, controller = rec.data.controller; + me.createSDNControllerEditWindow(type, controller); + }; + + let edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/sdn/controllers/', + callback: () => store.load(), + }); + + // else we cannot dynamically generate the add menu handlers + let addHandleGenerator = function(type) { + return function() { me.createSDNControllerEditWindow(type); }; + }; + let addMenuItems = []; + for (const [type, controller] of Object.entries(PVE.Utils.sdncontrollerSchema)) { + if (controller.hideAdd) { + continue; + } + addMenuItems.push({ + text: PVE.Utils.format_sdncontroller_type(type), + iconCls: 'fa fa-fw fa-' + controller.faIcon, + handler: addHandleGenerator(type), + }); + } + + Ext.apply(me, { + store: store, + reloadStore: () => store.load(), + selModel: sm, + viewConfig: { + trackOver: false, + }, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: addMenuItems, + }), + }, + remove_btn, + edit_btn, + ], + columns: [ + { + header: 'ID', + flex: 2, + sortable: true, + dataIndex: 'controller', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'controller', 1); + }, + }, + { + header: gettext('Type'), + flex: 1, + sortable: true, + dataIndex: 'type', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'type', 1); + }, + }, + { + header: gettext('Node'), + flex: 1, + sortable: true, + dataIndex: 'node', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'node', 1); + }, + }, + { + header: gettext('State'), + width: 100, + dataIndex: 'state', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending_state(rec, value); + }, + }, + ], + listeners: { + activate: () => store.load(), + itemdblclick: run_editor, + }, + }); + store.load(); + me.callParent(); + }, +}); +Ext.define('PVE.sdn.Status', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveSDNStatus', + + onlineHelp: 'chapter_pvesdn', + + layout: { + type: 'vbox', + align: 'stretch', + }, + + initComponent: function() { + var me = this; + + me.rstore = Ext.create('Proxmox.data.ObjectStore', { + interval: me.interval, + model: 'pve-sdn-status', + storeid: 'pve-store-' + ++Ext.idSeed, + groupField: 'type', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/resources', + }, + }); + + me.items = [{ + xtype: 'pveSDNStatusView', + title: gettext('Status'), + rstore: me.rstore, + border: 0, + collapsible: true, + padding: '0 0 20 0', + }]; + + me.callParent(); + me.on('activate', me.rstore.startUpdate); + }, +}); +Ext.define('PVE.sdn.StatusView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveSDNStatusView', + + sortPriority: { + sdn: 1, + node: 2, + status: 3, + }, + + initComponent: function() { + var me = this; + + if (!me.rstore) { + throw "no rstore given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + sortAfterUpdate: true, + sorters: [{ + sorterFn: function(rec1, rec2) { + var p1 = me.sortPriority[rec1.data.type]; + var p2 = me.sortPriority[rec2.data.type]; + return p1 !== p2 ? p1 > p2 ? 1 : -1 : 0; + }, + }], + filters: { + property: 'type', + value: 'sdn', + operator: '==', + }, + }); + + Ext.apply(me, { + store: store, + stateful: false, + tbar: [ + { + text: gettext('Apply'), + handler: function() { + Proxmox.Utils.API2Request({ + url: '/cluster/sdn/', + method: 'PUT', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, + }, + ], + viewConfig: { + trackOver: false, + }, + columns: [ + { + header: 'SDN', + width: 80, + dataIndex: 'sdn', + }, + { + header: gettext('Node'), + width: 80, + dataIndex: 'node', + }, + { + header: gettext('Status'), + width: 80, + flex: 1, + dataIndex: 'status', + }, + ], + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + }, +}, function() { + Ext.define('pve-sdn-status', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'type', 'node', 'status', 'sdn', + ], + idProperty: 'id', + }); +}); +Ext.define('PVE.sdn.VnetInputPanel', { + extend: 'Proxmox.panel.InputPanel', + mixins: ['Proxmox.Mixin.CBind'], + + onGetValues: function(values) { + let me = this; + + if (me.isCreate) { + values.type = 'vnet'; + } + + if (!values.vlanaware) { + delete values.vlanaware; + } + + return values; + }, + + items: [ + { + xtype: 'pmxDisplayEditField', + name: 'vnet', + cbind: { + editable: '{isCreate}', + }, + maxLength: 8, + flex: 1, + allowBlank: false, + fieldLabel: gettext('Name'), + }, + { + xtype: 'textfield', + name: 'alias', + fieldLabel: gettext('Alias'), + allowBlank: true, + }, + { + xtype: 'pveSDNZoneSelector', + fieldLabel: gettext('Zone'), + name: 'zone', + value: '', + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'tag', + minValue: 1, + maxValue: 16777216, + fieldLabel: gettext('Tag'), + allowBlank: true, + }, + { + xtype: 'proxmoxcheckbox', + name: 'vlanaware', + uncheckedValue: 0, + checked: false, + fieldLabel: gettext('VLAN Aware'), + }, + ], +}); + +Ext.define('PVE.sdn.VnetEdit', { + extend: 'Proxmox.window.Edit', + + subject: gettext('VNet'), + + vnet: undefined, + + width: 350, + + initComponent: function() { + var me = this; + + me.isCreate = me.vnet === undefined; + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/sdn/vnets'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/sdn/vnets/' + me.vnet; + me.method = 'PUT'; + } + + let ipanel = Ext.create('PVE.sdn.VnetInputPanel', { + isCreate: me.isCreate, + }); + + Ext.apply(me, { + items: [ + ipanel, + ], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + let values = response.result.data; + ipanel.setValues(values); + }, + }); + } + }, +}); +Ext.define('PVE.sdn.VnetView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveSDNVnetView', + + onlineHelp: 'pvesdn_config_vnet', + + stateful: true, + stateId: 'grid-sdn-vnet', + + subnetview_panel: undefined, + + initComponent: function() { + let me = this; + + let store = new Ext.data.Store({ + model: 'pve-sdn-vnet', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/sdn/vnets?pending=1", + }, + sorters: { + property: 'vnet', + direction: 'ASC', + }, + }); + + let reload = () => store.load(); + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + + let win = Ext.create('PVE.sdn.VnetEdit', { + autoShow: true, + onlineHelp: 'pvesdn_config_vnet', + vnet: rec.data.vnet, + }); + win.on('destroy', reload); + }; + + let edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/sdn/vnets/', + callback: reload, + }); + + let set_button_status = function() { + var rec = me.selModel.getSelection()[0]; + + if (!rec || rec.data.state === 'deleted') { + edit_btn.disable(); + remove_btn.disable(); + } + }; + + Ext.apply(me, { + store: store, + reloadStore: reload, + selModel: sm, + viewConfig: { + trackOver: false, + }, + tbar: [ + { + text: gettext('Create'), + handler: function() { + let win = Ext.create('PVE.sdn.VnetEdit', { + autoShow: true, + onlineHelp: 'pvesdn_config_vnet', + type: 'vnet', + }); + win.on('destroy', reload); + }, + }, + remove_btn, + edit_btn, + ], + columns: [ + { + header: 'ID', + flex: 2, + dataIndex: 'vnet', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'vnet', 1); + }, + }, + { + header: gettext('Alias'), + flex: 1, + dataIndex: 'alias', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'alias'); + }, + }, + { + header: gettext('Zone'), + flex: 1, + dataIndex: 'zone', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'zone'); + }, + }, + { + header: gettext('Tag'), + flex: 1, + dataIndex: 'tag', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'tag'); + }, + }, + { + header: gettext('VLAN Aware'), + flex: 1, + dataIndex: 'vlanaware', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'vlanaware'); + }, + }, + { + header: gettext('State'), + width: 100, + dataIndex: 'state', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending_state(rec, value); + }, + }, + ], + listeners: { + activate: reload, + itemdblclick: run_editor, + selectionchange: set_button_status, + show: reload, + select: function(_sm, rec) { + let url = `/cluster/sdn/vnets/${rec.data.vnet}/subnets`; + me.subnetview_panel.setBaseUrl(url); + }, + deselect: function() { + me.subnetview_panel.setBaseUrl(undefined); + }, + }, + }); + store.load(); + me.callParent(); + }, +}); +Ext.define('PVE.sdn.Vnet', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveSDNVnet', + + title: 'Vnet', + + onlineHelp: 'pvesdn_config_vnet', + + initComponent: function() { + var me = this; + + var subnetview_panel = Ext.createWidget('pveSDNSubnetView', { + title: gettext('Subnets'), + region: 'center', + border: false, + }); + + var vnetview_panel = Ext.createWidget('pveSDNVnetView', { + title: 'Vnets', + region: 'west', + subnetview_panel: subnetview_panel, + width: '50%', + border: false, + split: true, + }); + + Ext.apply(me, { + layout: 'border', + items: [vnetview_panel, subnetview_panel], + listeners: { + show: function() { + subnetview_panel.fireEvent('show', subnetview_panel); + }, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.SubnetInputPanel', { + extend: 'Proxmox.panel.InputPanel', + mixins: ['Proxmox.Mixin.CBind'], + + onGetValues: function(values) { + let me = this; + + if (me.isCreate) { + values.type = 'subnet'; + values.subnet = values.cidr; + delete values.cidr; + } + + if (!values.gateway) { + delete values.gateway; + } + if (!values.snat) { + delete values.snat; + } + + return values; + }, + + items: [ + { + xtype: 'pmxDisplayEditField', + name: 'cidr', + cbind: { + editable: '{isCreate}', + }, + flex: 1, + allowBlank: false, + fieldLabel: gettext('Subnet'), + }, + { + xtype: 'textfield', + name: 'gateway', + vtype: 'IP64Address', + fieldLabel: gettext('Gateway'), + allowBlank: true, + }, + { + xtype: 'proxmoxcheckbox', + name: 'snat', + uncheckedValue: 0, + checked: false, + fieldLabel: 'SNAT', + }, + { + xtype: 'proxmoxtextfield', + name: 'dnszoneprefix', + skipEmptyText: true, + fieldLabel: gettext('DNS zone prefix'), + allowBlank: true, + }, + ], +}); + +Ext.define('PVE.sdn.SubnetEdit', { + extend: 'Proxmox.window.Edit', + + subject: gettext('Subnet'), + + subnet: undefined, + + width: 350, + + base_url: undefined, + + initComponent: function() { + var me = this; + + me.isCreate = me.subnet === undefined; + + if (me.isCreate) { + me.url = me.base_url; + me.method = 'POST'; + } else { + me.url = me.base_url + '/' + me.subnet; + me.method = 'PUT'; + } + + let ipanel = Ext.create('PVE.sdn.SubnetInputPanel', { + isCreate: me.isCreate, + }); + + Ext.apply(me, { + items: [ + ipanel, + ], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + let values = response.result.data; + ipanel.setValues(values); + }, + }); + } + }, +}); +Ext.define('PVE.sdn.SubnetView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveSDNSubnetView', + + stateful: true, + stateId: 'grid-sdn-subnet', + + base_url: undefined, + + remove_btn: undefined, + + setBaseUrl: function(url) { + let me = this; + + me.base_url = url; + + if (url === undefined) { + me.store.removeAll(); + me.create_btn.disable(); + } else { + me.remove_btn.baseurl = url + '/'; + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/' + url + '?pending=1', + }); + me.create_btn.enable(); + me.store.load(); + } + }, + + initComponent: function() { + let me = this; + + let store = new Ext.data.Store({ + model: 'pve-sdn-subnet', + }); + + let reload = function() { + store.load(); + }; + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + + let win = Ext.create('PVE.sdn.SubnetEdit', { + autoShow: true, + subnet: rec.data.subnet, + base_url: me.base_url, + }); + win.on('destroy', reload); + }; + + me.create_btn = new Proxmox.button.Button({ + text: gettext('Create'), + disabled: true, + handler: function() { + let win = Ext.create('PVE.sdn.SubnetEdit', { + autoShow: true, + base_url: me.base_url, + type: 'subnet', + }); + win.on('destroy', reload); + }, + }); + + let edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + me.remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: () => store.load(), + }); + + let set_button_status = function() { + var rec = me.selModel.getSelection()[0]; + + if (!rec || rec.data.state === 'deleted') { + edit_btn.disable(); + me.remove_btn.disable(); + } + }; + + Ext.apply(me, { + store: store, + reloadStore: reload, + selModel: sm, + viewConfig: { + trackOver: false, + }, + tbar: [ + me.create_btn, + me.remove_btn, + edit_btn, + ], + columns: [ + { + header: 'ID', + flex: 2, + dataIndex: 'cidr', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'cidr', 1); + }, + }, + { + header: gettext('Gateway'), + flex: 1, + dataIndex: 'gateway', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'gateway'); + }, + }, + { + header: 'SNAT', + flex: 1, + dataIndex: 'snat', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'snat'); + }, + }, + { + header: gettext('Dns prefix'), + flex: 1, + dataIndex: 'dnszoneprefix', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'dnszoneprefix'); + }, + }, + { + header: gettext('State'), + width: 100, + dataIndex: 'state', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending_state(rec, value); + }, + }, + + ], + listeners: { + activate: reload, + itemdblclick: run_editor, + selectionchange: set_button_status, + }, + }); + + me.callParent(); + + if (me.base_url) { + me.setBaseUrl(me.base_url); // load + } + }, +}, function() { + Ext.define('pve-sdn-subnet', { + extend: 'Ext.data.Model', + fields: [ + 'cidr', + 'gateway', + 'snat', + ], + idProperty: 'subnet', + }); +}); +Ext.define('PVE.sdn.ZoneContentView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveSDNZoneContentView', + + stateful: true, + stateId: 'grid-sdnzone-content', + viewConfig: { + trackOver: false, + loadMask: false, + }, + features: [ + { + ftype: 'grouping', + groupHeaderTpl: '{name} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})', + }, + ], + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var zone = me.pveSelNode.data.sdn; + if (!zone) { + throw "no zone ID specified"; + } + + var baseurl = "/nodes/" + nodename + "/sdn/zones/" + zone + "/content"; + var store = Ext.create('Ext.data.Store', { + model: 'pve-sdnzone-content', + groupField: 'content', + proxy: { + type: 'proxmox', + url: '/api2/json' + baseurl, + }, + sorters: { + property: 'vnet', + direction: 'ASC', + }, + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + store.load(); + }; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + ], + columns: [ + { + header: 'VNet', + width: 100, + sortable: true, + dataIndex: 'vnet', + }, + { + header: 'Alias', + width: 300, + sortable: true, + dataIndex: 'alias', + }, + { + header: gettext('Status'), + width: 100, + sortable: true, + dataIndex: 'status', + }, + { + header: gettext('Details'), + flex: 1, + dataIndex: 'statusmsg', + }, + ], + listeners: { + activate: reload, + }, + }); + + me.callParent(); + }, +}, function() { + Ext.define('pve-sdnzone-content', { + extend: 'Ext.data.Model', + fields: [ + 'vnet', 'status', 'statusmsg', + { + name: 'text', + convert: function(value, record) { + // check for volid, because if you click on a grouping header, + // it calls convert (but with an empty volid) + if (value || record.data.vnet === null) { + return value; + } + return PVE.Utils.format_sdnvnet_type(value, {}, record); + }, + }, + ], + idProperty: 'vnet', + }); +}); +Ext.define('PVE.sdn.ZoneView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveSDNZoneView'], + + onlineHelp: 'pvesdn_config_zone', + + stateful: true, + stateId: 'grid-sdn-zone', + + createSDNEditWindow: function(type, sid) { + let schema = PVE.Utils.sdnzoneSchema[type]; + if (!schema || !schema.ipanel) { + throw "no editor registered for zone type: " + type; + } + + Ext.create('PVE.sdn.zones.BaseEdit', { + paneltype: 'PVE.sdn.zones.' + schema.ipanel, + type: type, + zone: sid, + autoShow: true, + listeners: { + destroy: this.reloadStore, + }, + }); + }, + + initComponent: function() { + let me = this; + + let store = new Ext.data.Store({ + model: 'pve-sdn-zone', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/sdn/zones?pending=1", + }, + sorters: { + property: 'zone', + direction: 'ASC', + }, + }); + + let reload = function() { + store.load(); + }; + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + let type = rec.data.type, + zone = rec.data.zone; + + me.createSDNEditWindow(type, zone); + }; + + let edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/sdn/zones/', + callback: reload, + }); + + let set_button_status = function() { + var rec = me.selModel.getSelection()[0]; + + if (!rec || rec.data.state === 'deleted') { + edit_btn.disable(); + remove_btn.disable(); + } + }; + + // else we cannot dynamically generate the add menu handlers + let addHandleGenerator = function(type) { + return function() { me.createSDNEditWindow(type); }; + }; + let addMenuItems = []; + for (const [type, zone] of Object.entries(PVE.Utils.sdnzoneSchema)) { + if (zone.hideAdd) { + continue; + } + addMenuItems.push({ + text: PVE.Utils.format_sdnzone_type(type), + iconCls: 'fa fa-fw fa-' + zone.faIcon, + handler: addHandleGenerator(type), + }); + } + + Ext.apply(me, { + store: store, + reloadStore: reload, + selModel: sm, + viewConfig: { + trackOver: false, + }, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: addMenuItems, + }), + }, + remove_btn, + edit_btn, + ], + columns: [ + { + header: 'ID', + width: 100, + dataIndex: 'zone', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'zone', 1); + }, + }, + { + header: gettext('Type'), + width: 100, + dataIndex: 'type', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'type', 1); + }, + }, + { + header: 'MTU', + width: 50, + dataIndex: 'mtu', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'mtu'); + }, + }, + { + header: 'Ipam', + flex: 3, + dataIndex: 'ipam', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'ipam'); + }, + }, + { + header: gettext('Domain'), + flex: 3, + dataIndex: 'dnszone', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'dnszone'); + }, + }, + { + header: gettext('Dns'), + flex: 3, + dataIndex: 'dns', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'dns'); + }, + }, + { + header: gettext('Reverse dns'), + flex: 3, + dataIndex: 'reversedns', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'reversedns'); + }, + }, + { + header: gettext('Nodes'), + flex: 3, + dataIndex: 'nodes', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'nodes'); + }, + }, + { + header: gettext('State'), + width: 100, + dataIndex: 'state', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending_state(rec, value); + }, + }, + ], + listeners: { + activate: reload, + itemdblclick: run_editor, + selectionchange: set_button_status, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.Options', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveSDNOptions', + + title: 'Options', + + layout: { + type: 'vbox', + align: 'stretch', + }, + + onlineHelp: 'pvesdn_config_controllers', + + items: [ + { + xtype: 'pveSDNControllerView', + title: gettext('Controllers'), + flex: 1, + padding: '0 0 20 0', + border: 0, + }, + { + xtype: 'pveSDNIpamView', + title: 'IPAMs', + flex: 1, + padding: '0 0 20 0', + border: 0, + }, { + xtype: 'pveSDNDnsView', + title: 'DNS', + flex: 1, + border: 0, + }, + ], +}); +Ext.define('PVE.panel.SDNControllerBase', { + extend: 'Proxmox.panel.InputPanel', + + type: '', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.controller; + } + + return values; + }, +}); + +Ext.define('PVE.sdn.controllers.BaseEdit', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + me.isCreate = !me.controllerid; + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/sdn/controllers'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/sdn/controllers/' + me.controllerid; + me.method = 'PUT'; + } + + var ipanel = Ext.create(me.paneltype, { + type: me.type, + isCreate: me.isCreate, + controllerid: me.controllerid, + }); + + Ext.apply(me, { + subject: PVE.Utils.format_sdncontroller_type(me.type), + isAdd: true, + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + var ctypes = values.content || ''; + + values.content = ctypes.split(','); + + if (values.nodes) { + values.nodes = values.nodes.split(','); + } + values.enable = values.disable ? 0 : 1; + + ipanel.setValues(values); + }, + }); + } + }, +}); +Ext.define('PVE.sdn.controllers.EvpnInputPanel', { + extend: 'PVE.panel.SDNControllerBase', + + onlineHelp: 'pvesdn_controller_plugin_evpn', + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'controller', + maxLength: 8, + value: me.controllerid || '', + fieldLabel: 'ID', + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'asn', + minValue: 1, + maxValue: 4294967295, + value: 65000, + fieldLabel: 'ASN #', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'peers', + fieldLabel: gettext('Peers'), + allowBlank: false, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.controllers.BgpInputPanel', { + extend: 'PVE.panel.SDNControllerBase', + + onlineHelp: 'pvesdn_controller_plugin_evpn', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + values.controller = 'bgp' + values.node; + } else { + delete values.controller; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: 'pveNodeSelector', + name: 'node', + fieldLabel: gettext('Node'), + multiSelect: false, + autoSelect: false, + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'asn', + minValue: 1, + maxValue: 4294967295, + value: 65000, + fieldLabel: 'ASN #', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'peers', + fieldLabel: gettext('Peers'), + allowBlank: false, + }, + { + xtype: 'proxmoxcheckbox', + name: 'ebgp', + uncheckedValue: 0, + checked: false, + fieldLabel: 'EBGP', + }, + + ]; + + me.advancedItems = [ + + { + xtype: 'textfield', + name: 'loopback', + fieldLabel: gettext('Loopback Interface'), + }, + { + xtype: 'proxmoxintegerfield', + name: 'ebgp-multihop', + minValue: 1, + maxValue: 100, + fieldLabel: 'ebgp-multihop', + allowBlank: true, + }, + { + xtype: 'proxmoxcheckbox', + name: 'bgp-multipath-as-path-relax', + uncheckedValue: 0, + checked: false, + fieldLabel: 'bgp-multipath-as-path-relax', + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.IpamView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveSDNIpamView'], + + stateful: true, + stateId: 'grid-sdn-ipam', + + createSDNEditWindow: function(type, sid) { + let schema = PVE.Utils.sdnipamSchema[type]; + if (!schema || !schema.ipanel) { + throw "no editor registered for ipam type: " + type; + } + + Ext.create('PVE.sdn.ipams.BaseEdit', { + paneltype: 'PVE.sdn.ipams.' + schema.ipanel, + type: type, + ipam: sid, + autoShow: true, + listeners: { + destroy: this.reloadStore, + }, + }); + }, + + initComponent: function() { + let me = this; + + let store = new Ext.data.Store({ + model: 'pve-sdn-ipam', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/sdn/ipams", + }, + sorters: { + property: 'ipam', + direction: 'ASC', + }, + }); + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + let type = rec.data.type, ipam = rec.data.ipam; + me.createSDNEditWindow(type, ipam); + }; + + let edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/sdn/ipams/', + callback: () => store.load(), + }); + + // else we cannot dynamically generate the add menu handlers + let addHandleGenerator = function(type) { + return function() { me.createSDNEditWindow(type); }; + }; + let addMenuItems = []; + for (const [type, ipam] of Object.entries(PVE.Utils.sdnipamSchema)) { + if (ipam.hideAdd) { + continue; + } + addMenuItems.push({ + text: PVE.Utils.format_sdnipam_type(type), + iconCls: 'fa fa-fw fa-' + ipam.faIcon, + handler: addHandleGenerator(type), + }); + } + + Ext.apply(me, { + store: store, + reloadStore: () => store.load(), + selModel: sm, + viewConfig: { + trackOver: false, + }, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: addMenuItems, + }), + }, + remove_btn, + edit_btn, + ], + columns: [ + { + header: 'ID', + flex: 2, + dataIndex: 'ipam', + }, + { + header: gettext('Type'), + flex: 1, + dataIndex: 'type', + renderer: PVE.Utils.format_sdnipam_type, + }, + { + header: 'url', + flex: 1, + dataIndex: 'url', + }, + ], + listeners: { + activate: () => store.load(), + itemdblclick: run_editor, + }, + }); + + store.load(); + me.callParent(); + }, +}); +Ext.define('PVE.panel.SDNIpamBase', { + extend: 'Proxmox.panel.InputPanel', + + type: '', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.ipam; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.callParent(); + }, +}); + +Ext.define('PVE.sdn.ipams.BaseEdit', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + me.isCreate = !me.ipam; + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/sdn/ipams'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/sdn/ipams/' + me.ipam; + me.method = 'PUT'; + } + + var ipanel = Ext.create(me.paneltype, { + type: me.type, + isCreate: me.isCreate, + ipam: me.ipam, + }); + + Ext.apply(me, { + subject: PVE.Utils.format_sdnipam_type(me.type), + isAdd: true, + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + var ctypes = values.content || ''; + + values.content = ctypes.split(','); + + if (values.nodes) { + values.nodes = values.nodes.split(','); + } + values.enable = values.disable ? 0 : 1; + + ipanel.setValues(values); + }, + }); + } + }, +}); +Ext.define('PVE.sdn.ipams.NetboxInputPanel', { + extend: 'PVE.panel.SDNIpamBase', + + onlineHelp: 'pvesdn_ipam_plugin_netbox', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.ipam; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'ipam', + maxLength: 10, + value: me.zone || '', + fieldLabel: 'ID', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'url', + fieldLabel: gettext('Url'), + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'token', + fieldLabel: gettext('Token'), + allowBlank: false, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.ipams.PVEIpamInputPanel', { + extend: 'PVE.panel.SDNIpamBase', + + onlineHelp: 'pvesdn_ipam_plugin_pveipam', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.ipam; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'ipam', + maxLength: 10, + value: me.zone || '', + fieldLabel: 'ID', + allowBlank: false, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.ipams.PhpIpamInputPanel', { + extend: 'PVE.panel.SDNIpamBase', + + onlineHelp: 'pvesdn_ipam_plugin_phpipam', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.ipam; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'ipam', + maxLength: 10, + value: me.zone || '', + fieldLabel: 'ID', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'url', + fieldLabel: gettext('Url'), + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'token', + fieldLabel: gettext('Token'), + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'section', + fieldLabel: gettext('Section'), + allowBlank: false, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.DnsView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveSDNDnsView'], + + stateful: true, + stateId: 'grid-sdn-dns', + + createSDNEditWindow: function(type, sid) { + let schema = PVE.Utils.sdndnsSchema[type]; + if (!schema || !schema.ipanel) { + throw "no editor registered for dns type: " + type; + } + + Ext.create('PVE.sdn.dns.BaseEdit', { + paneltype: 'PVE.sdn.dns.' + schema.ipanel, + type: type, + dns: sid, + autoShow: true, + listeners: { + destroy: this.reloadStore, + }, + }); + }, + + initComponent: function() { + let me = this; + + let store = new Ext.data.Store({ + model: 'pve-sdn-dns', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/sdn/dns", + }, + sorters: { + property: 'dns', + direction: 'ASC', + }, + }); + + let sm = Ext.create('Ext.selection.RowModel', {}); + + let run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + let type = rec.data.type, + dns = rec.data.dns; + + me.createSDNEditWindow(type, dns); + }; + + let edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + }); + + let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/sdn/dns/', + callback: () => store.load(), + }); + + // else we cannot dynamically generate the add menu handlers + let addHandleGenerator = function(type) { + return function() { me.createSDNEditWindow(type); }; + }; + let addMenuItems = []; + for (const [type, dns] of Object.entries(PVE.Utils.sdndnsSchema)) { + if (dns.hideAdd) { + continue; + } + addMenuItems.push({ + text: PVE.Utils.format_sdndns_type(type), + iconCls: 'fa fa-fw fa-' + dns.faIcon, + handler: addHandleGenerator(type), + }); + } + + Ext.apply(me, { + store: store, + reloadStore: () => store.load(), + selModel: sm, + viewConfig: { + trackOver: false, + }, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: addMenuItems, + }), + }, + remove_btn, + edit_btn, + ], + columns: [ + { + header: 'ID', + flex: 2, + dataIndex: 'dns', + }, + { + header: gettext('Type'), + flex: 1, + dataIndex: 'type', + renderer: PVE.Utils.format_sdndns_type, + }, + { + header: 'url', + flex: 1, + dataIndex: 'url', + }, + ], + listeners: { + activate: () => store.load(), + itemdblclick: run_editor, + }, + }); + + store.load(); + me.callParent(); + }, +}); +Ext.define('PVE.panel.SDNDnsBase', { + extend: 'Proxmox.panel.InputPanel', + + type: '', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.dns; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.callParent(); + }, +}); + +Ext.define('PVE.sdn.dns.BaseEdit', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + me.isCreate = !me.dns; + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/sdn/dns'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/sdn/dns/' + me.dns; + me.method = 'PUT'; + } + + var ipanel = Ext.create(me.paneltype, { + type: me.type, + isCreate: me.isCreate, + dns: me.dns, + }); + + Ext.apply(me, { + subject: PVE.Utils.format_sdndns_type(me.type), + isAdd: true, + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + var ctypes = values.content || ''; + + values.content = ctypes.split(','); + + if (values.nodes) { + values.nodes = values.nodes.split(','); + } + values.enable = values.disable ? 0 : 1; + + ipanel.setValues(values); + }, + }); + } + }, +}); +Ext.define('PVE.sdn.dns.PowerdnsInputPanel', { + extend: 'PVE.panel.SDNDnsBase', + + onlineHelp: 'pvesdn_dns_plugin_powerdns', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.dns; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'dns', + maxLength: 10, + value: me.dns || '', + fieldLabel: 'ID', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'url', + fieldLabel: 'url', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'key', + fieldLabel: gettext('api key'), + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'ttl', + fieldLabel: 'ttl', + allowBlank: true, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.panel.SDNZoneBase', { + extend: 'Proxmox.panel.InputPanel', + + type: '', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.zone; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.advancedItems = [ + { + xtype: 'pveSDNIpamSelector', + fieldLabel: gettext('Ipam'), + name: 'ipam', + value: 'pve', + allowBlank: false, + }, + { + xtype: 'pveSDNDnsSelector', + fieldLabel: gettext('Dns server'), + name: 'dns', + value: '', + allowBlank: true, + }, + { + xtype: 'pveSDNDnsSelector', + fieldLabel: gettext('Reverse Dns server'), + name: 'reversedns', + value: '', + allowBlank: true, + }, + { + xtype: 'proxmoxtextfield', + name: 'dnszone', + skipEmptyText: true, + fieldLabel: gettext('DNS zone'), + allowBlank: true, + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.sdn.zones.BaseEdit', { + extend: 'Proxmox.window.Edit', + + width: 400, + + initComponent: function() { + var me = this; + + me.isCreate = !me.zone; + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/sdn/zones'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/sdn/zones/' + me.zone; + me.method = 'PUT'; + } + + var ipanel = Ext.create(me.paneltype, { + type: me.type, + isCreate: me.isCreate, + zone: me.zone, + }); + + Ext.apply(me, { + subject: PVE.Utils.format_sdnzone_type(me.type), + isAdd: true, + items: [ipanel], + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + var ctypes = values.content || ''; + + values.content = ctypes.split(','); + + if (values.nodes) { + values.nodes = values.nodes.split(','); + } + + if (values.exitnodes) { + values.exitnodes = values.exitnodes.split(','); + } + + values.enable = values.disable ? 0 : 1; + + ipanel.setValues(values); + }, + }); + } + }, +}); +Ext.define('PVE.sdn.zones.EvpnInputPanel', { + extend: 'PVE.panel.SDNZoneBase', + + onlineHelp: 'pvesdn_zone_plugin_evpn', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.zone; + } + + if (!values.mac) { + delete values.mac; + } + + if (values['advertise-subnets'] === 0) { + delete values['advertise-subnets']; + } + + if (values['exitnodes-local-routing'] === 0) { + delete values['exitnodes-local-routing']; + } + + if (values['disable-arp-nd-suppression'] === 0) { + delete values['disable-arp-nd-suppression']; + } + + if (values['exitnodes-primary'] === '') { + delete values['exitnodes-primary']; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'zone', + maxLength: 8, + value: me.zone || '', + fieldLabel: 'ID', + allowBlank: false, + }, + { + xtype: 'pveSDNControllerSelector', + fieldLabel: gettext('Controller'), + name: 'controller', + value: '', + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'vrf-vxlan', + minValue: 1, + maxValue: 16000000, + fieldLabel: 'VRF-VXLAN Tag', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'mac', + fieldLabel: gettext('Vnet MAC address'), + vtype: 'MacAddress', + allowBlank: true, + emptyText: 'auto', + }, + { + xtype: 'pveNodeSelector', + name: 'exitnodes', + fieldLabel: gettext('Exit Nodes'), + multiSelect: true, + autoSelect: false, + }, + { + xtype: 'pveNodeSelector', + name: 'exitnodes-primary', + fieldLabel: gettext('Primary Exit Node'), + multiSelect: false, + autoSelect: false, + }, + { + xtype: 'proxmoxcheckbox', + name: 'exitnodes-local-routing', + uncheckedValue: 0, + checked: false, + fieldLabel: gettext('Exit Nodes local routing'), + }, + { + xtype: 'proxmoxcheckbox', + name: 'advertise-subnets', + uncheckedValue: 0, + checked: false, + fieldLabel: gettext('Advertise subnets'), + }, + { + xtype: 'proxmoxcheckbox', + name: 'disable-arp-nd-suppression', + uncheckedValue: 0, + checked: false, + fieldLabel: gettext('Disable arp-nd suppression'), + }, + { + xtype: 'textfield', + name: 'rt-import', + fieldLabel: gettext('Route-target import'), + allowBlank: true, + }, + { + xtype: 'proxmoxintegerfield', + name: 'mtu', + minValue: 100, + maxValue: 65000, + fieldLabel: 'MTU', + skipEmptyText: true, + allowBlank: true, + emptyText: 'auto', + }, + { + xtype: 'pveNodeSelector', + name: 'nodes', + fieldLabel: gettext('Nodes'), + emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', + multiSelect: true, + autoSelect: false, + }, + + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.zones.QinQInputPanel', { + extend: 'PVE.panel.SDNZoneBase', + + onlineHelp: 'pvesdn_zone_plugin_qinq', + + onGetValues: function(values) { + let me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.sdn; + } + + return values; + }, + + initComponent: function() { + let me = this; + + me.items = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'zone', + maxLength: 8, + value: me.zone || '', + fieldLabel: 'ID', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'bridge', + fieldLabel: 'Bridge', + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'tag', + minValue: 0, + maxValue: 4096, + fieldLabel: gettext('Service VLAN'), + allowBlank: false, + }, + { + xtype: 'proxmoxKVComboBox', + name: 'vlan-protocol', + fieldLabel: gettext('Service-VLAN Protocol'), + allowBlank: true, + value: '802.1q', + comboItems: [ + ['802.1q', '802.1q'], + ['802.1ad', '802.1ad'], + ], + }, + { + xtype: 'proxmoxintegerfield', + name: 'mtu', + minValue: 100, + maxValue: 65000, + fieldLabel: 'MTU', + skipEmptyText: true, + allowBlank: true, + emptyText: 'auto', + }, + { + xtype: 'pveNodeSelector', + name: 'nodes', + fieldLabel: gettext('Nodes'), + emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', + multiSelect: true, + autoSelect: false, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.zones.SimpleInputPanel', { + extend: 'PVE.panel.SDNZoneBase', + + onlineHelp: 'pvesdn_zone_plugin_simple', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.zone; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'zone', + maxLength: 10, + value: me.zone || '', + fieldLabel: 'ID', + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'mtu', + minValue: 100, + maxValue: 65000, + fieldLabel: 'MTU', + skipEmptyText: true, + allowBlank: true, + emptyText: 'auto', + }, + { + xtype: 'pveNodeSelector', + name: 'nodes', + fieldLabel: gettext('Nodes'), + emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', + multiSelect: true, + autoSelect: false, + }, + + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.zones.VlanInputPanel', { + extend: 'PVE.panel.SDNZoneBase', + + onlineHelp: 'pvesdn_zone_plugin_vlan', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.zone; + } + + return values; + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'zone', + maxLength: 10, + value: me.zone || '', + fieldLabel: 'ID', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'bridge', + fieldLabel: 'Bridge', + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'mtu', + minValue: 100, + maxValue: 65000, + fieldLabel: 'MTU', + skipEmptyText: true, + allowBlank: true, + emptyText: 'auto', + }, + { + xtype: 'pveNodeSelector', + name: 'nodes', + fieldLabel: gettext('Nodes'), + emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', + multiSelect: true, + autoSelect: false, + }, + + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.sdn.zones.VxlanInputPanel', { + extend: 'PVE.panel.SDNZoneBase', + + onlineHelp: 'pvesdn_zone_plugin_vxlan', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.zone; + } + + delete values.mode; + + return values; + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + maxLength: 8, + name: 'zone', + value: me.zone || '', + fieldLabel: 'ID', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'peers', + fieldLabel: gettext('Peer Address List'), + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'mtu', + minValue: 100, + maxValue: 65000, + fieldLabel: 'MTU', + skipEmptyText: true, + allowBlank: true, + emptyText: 'auto', + }, + { + xtype: 'pveNodeSelector', + name: 'nodes', + fieldLabel: gettext('Nodes'), + emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', + multiSelect: true, + autoSelect: false, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.storage.ContentView', { + extend: 'Ext.grid.GridPanel', + + alias: 'widget.pveStorageContentView', + + viewConfig: { + trackOver: false, + loadMask: false, + }, + initComponent: function() { + var me = this; + + if (!me.nodename) { + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + } + const nodename = me.nodename; + + if (!me.storage) { + me.storage = me.pveSelNode.data.storage; + if (!me.storage) { + throw "no storage ID specified"; + } + } + const storage = me.storage; + + var content = me.content; + if (!content) { + throw "no content type specified"; + } + + const baseurl = `/nodes/${nodename}/storage/${storage}/content`; + let store = me.store = Ext.create('Ext.data.Store', { + model: 'pve-storage-content', + proxy: { + type: 'proxmox', + url: '/api2/json' + baseurl, + extraParams: { + content: content, + }, + }, + sorters: { + property: 'volid', + direction: 'ASC', + }, + }); + + if (!me.sm) { + me.sm = Ext.create('Ext.selection.RowModel', {}); + } + let sm = me.sm; + + let reload = () => store.load(); + + Proxmox.Utils.monStoreErrors(me, store); + + if (!me.tbar) { + me.tbar = []; + } + if (me.useUploadButton) { + me.tbar.unshift( + { + xtype: 'button', + text: gettext('Upload'), + disabled: !me.enableUploadButton, + handler: function() { + Ext.create('PVE.window.UploadToStorage', { + nodename: nodename, + storage: storage, + content: content, + autoShow: true, + taskDone: () => reload(), + }); + }, + }, + { + xtype: 'button', + text: gettext('Download from URL'), + disabled: !me.enableDownloadUrlButton, + handler: function() { + Ext.create('PVE.window.DownloadUrlToStorage', { + nodename: nodename, + storage: storage, + content: content, + autoShow: true, + taskDone: () => reload(), + }); + }, + }, + '-', + ); + } + if (!me.useCustomRemoveButton) { + me.tbar.push({ + xtype: 'proxmoxStdRemoveButton', + selModel: sm, + enableFn: rec => !rec?.data?.protected, + delay: 5, + callback: () => reload(), + baseurl: baseurl + '/', + }); + } + me.tbar.push( + '->', + gettext('Search') + ':', + ' ', + { + xtype: 'textfield', + width: 200, + enableKeyEvents: true, + emptyText: content === 'backup' ? gettext('Name, Format, Notes') : gettext('Name, Format'), + listeners: { + keyup: { + buffer: 500, + fn: function(field) { + let needle = field.getValue().toLocaleLowerCase(); + store.clearFilter(true); + store.filter([ + { + filterFn: ({ data }) => + data.text?.toLocaleLowerCase().includes(needle) || + data.notes?.toLocaleLowerCase().includes(needle), + }, + ]); + }, + }, + change: function(field, newValue, oldValue) { + if (newValue !== this.originalValue) { + this.triggers.clear.setVisible(true); + } + }, + }, + triggers: { + clear: { + cls: 'pmx-clear-trigger', + weight: -1, + hidden: true, + handler: function() { + this.triggers.clear.setVisible(false); + this.setValue(this.originalValue); + store.clearFilter(); + }, + }, + }, + }, + ); + + let availableColumns = { + 'name': { + header: gettext('Name'), + flex: 2, + sortable: true, + renderer: PVE.Utils.render_storage_content, + dataIndex: 'text', + }, + 'notes': { + header: gettext('Notes'), + flex: 1, + renderer: Ext.htmlEncode, + dataIndex: 'notes', + }, + 'protected': { + header: ``, + tooltip: gettext('Protected'), + width: 30, + renderer: v => v ? `` : '', + sorter: (a, b) => (b.data.protected || 0) - (a.data.protected || 0), + dataIndex: 'protected', + }, + 'date': { + header: gettext('Date'), + width: 150, + dataIndex: 'vdate', + }, + 'format': { + header: gettext('Format'), + width: 100, + dataIndex: 'format', + }, + 'size': { + header: gettext('Size'), + width: 100, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size', + }, + }; + + let showColumns = me.showColumns || ['name', 'date', 'format', 'size']; + + Object.keys(availableColumns).forEach(function(key) { + if (!showColumns.includes(key)) { + delete availableColumns[key]; + } + }); + + if (me.extraColumns && typeof me.extraColumns === 'object') { + Object.assign(availableColumns, me.extraColumns); + } + const columns = Object.values(availableColumns); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: me.tbar, + columns: columns, + listeners: { + activate: reload, + }, + }); + + me.callParent(); + }, +}, function() { + Ext.define('pve-storage-content', { + extend: 'Ext.data.Model', + fields: [ + 'volid', 'content', 'format', 'size', 'used', 'vmid', + 'channel', 'id', 'lun', 'notes', 'verification', + { + name: 'text', + convert: function(value, record) { + // check for volid, because if you click on a grouping header, + // it calls convert (but with an empty volid) + if (value || record.data.volid === null) { + return value; + } + return PVE.Utils.render_storage_content(value, {}, record); + }, + }, + { + name: 'vdate', + convert: function(value, record) { + // check for volid, because if you click on a grouping header, + // it calls convert (but with an empty volid) + if (value || record.data.volid === null) { + return value; + } + let t = record.data.content; + if (t === "backup") { + let v = record.data.volid; + let match = v.match(/(\d{4}_\d{2}_\d{2})-(\d{2}_\d{2}_\d{2})/); + if (match) { + let date = match[1].replace(/_/g, '-'); + let time = match[2].replace(/_/g, ':'); + return date + " " + time; + } + } + if (record.data.ctime) { + let ctime = new Date(record.data.ctime * 1000); + return Ext.Date.format(ctime, 'Y-m-d H:i:s'); + } + return ''; + }, + }, + ], + idProperty: 'volid', + }); +}); +Ext.define('PVE.storage.BackupView', { + extend: 'PVE.storage.ContentView', + + alias: 'widget.pveStorageBackupView', + + showColumns: ['name', 'notes', 'protected', 'date', 'format', 'size'], + + initComponent: function() { + let me = this; + + let nodename = me.nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + let storage = me.storage = me.pveSelNode.data.storage; + if (!storage) { + throw "no storage ID specified"; + } + + me.content = 'backup'; + + let sm = me.sm = Ext.create('Ext.selection.RowModel', {}); + + let pruneButton = Ext.create('Proxmox.button.Button', { + text: gettext('Prune group'), + disabled: true, + selModel: sm, + setBackupGroup: function(backup) { + if (backup) { + let name = backup.text; + let vmid = backup.vmid; + let format = backup.format; + + let vmtype; + if (name.startsWith('vzdump-lxc-') || format === "pbs-ct") { + vmtype = 'lxc'; + } else if (name.startsWith('vzdump-qemu-') || format === "pbs-vm") { + vmtype = 'qemu'; + } + + if (vmid && vmtype) { + this.setText(gettext('Prune group') + ` ${vmtype}/${vmid}`); + this.vmid = vmid; + this.vmtype = vmtype; + this.setDisabled(false); + return; + } + } + this.setText(gettext('Prune group')); + this.vmid = null; + this.vmtype = null; + this.setDisabled(true); + }, + handler: function(b, e, rec) { + Ext.create('PVE.window.Prune', { + autoShow: true, + nodename, + storage, + backup_id: this.vmid, + backup_type: this.vmtype, + listeners: { + destroy: () => me.store.load(), + }, + }); + }, + }); + + me.on('selectionchange', function(model, srecords, eOpts) { + if (srecords.length === 1) { + pruneButton.setBackupGroup(srecords[0].data); + } else { + pruneButton.setBackupGroup(null); + } + }); + + let isPBS = me.pluginType === 'pbs'; + + me.tbar = [ + { + xtype: 'proxmoxButton', + text: gettext('Restore'), + selModel: sm, + disabled: true, + handler: function(b, e, rec) { + let vmtype; + if (PVE.Utils.volume_is_qemu_backup(rec.data.volid, rec.data.format)) { + vmtype = 'qemu'; + } else if (PVE.Utils.volume_is_lxc_backup(rec.data.volid, rec.data.format)) { + vmtype = 'lxc'; + } else { + return; + } + + Ext.create('PVE.window.Restore', { + autoShow: true, + nodename, + volid: rec.data.volid, + volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec), + vmtype, + isPBS, + listeners: { + destroy: () => me.store.load(), + }, + }); + }, + }, + ]; + if (isPBS) { + me.tbar.push({ + xtype: 'proxmoxButton', + text: gettext('File Restore'), + disabled: true, + selModel: sm, + handler: function(b, e, rec) { + let isVMArchive = PVE.Utils.volume_is_qemu_backup(rec.data.volid, rec.data.format); + Ext.create('Proxmox.window.FileBrowser', { + title: gettext('File Restore') + " - " + rec.data.text, + listURL: `/api2/json/nodes/localhost/storage/${me.storage}/file-restore/list`, + downloadURL: `/api2/json/nodes/localhost/storage/${me.storage}/file-restore/download`, + extraParams: { + volume: rec.data.volid, + }, + archive: isVMArchive ? 'all' : undefined, + autoShow: true, + }); + }, + }); + } + me.tbar.push( + { + xtype: 'proxmoxButton', + text: gettext('Show Configuration'), + disabled: true, + selModel: sm, + handler: function(b, e, rec) { + Ext.create('PVE.window.BackupConfig', { + autoShow: true, + volume: rec.data.volid, + pveSelNode: me.pveSelNode, + }); + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit Notes'), + disabled: true, + selModel: sm, + handler: function(b, e, rec) { + let volid = rec.data.volid; + Ext.create('Proxmox.window.Edit', { + autoShow: true, + autoLoad: true, + width: 600, + height: 400, + resizable: true, + title: gettext('Notes'), + url: `/api2/extjs/nodes/${nodename}/storage/${me.storage}/content/${volid}`, + layout: 'fit', + items: [ + { + xtype: 'textarea', + layout: 'fit', + name: 'notes', + height: '100%', + }, + ], + listeners: { + destroy: () => me.store.load(), + }, + }); + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Change Protection'), + disabled: true, + handler: function(button, event, record) { + const volid = record.data.volid; + Proxmox.Utils.API2Request({ + url: `/api2/extjs/nodes/${nodename}/storage/${me.storage}/content/${volid}`, + method: 'PUT', + waitMsgTarget: me, + params: { 'protected': record.data.protected ? 0 : 1 }, + failure: response => Ext.Msg.alert('Error', response.htmlStatus), + success: () => { + me.store.load({ + callback: () => sm.fireEvent('selectionchange', sm, [record]), + }); + }, + }); + }, + }, + '-', + pruneButton, + ); + + if (isPBS) { + me.extraColumns = { + encrypted: { + header: gettext('Encrypted'), + dataIndex: 'encrypted', + renderer: PVE.Utils.render_backup_encryption, + sorter: { + property: 'encrypted', + transform: encrypted => encrypted ? 1 : 0, + }, + }, + verification: { + header: gettext('Verify State'), + dataIndex: 'verification', + renderer: PVE.Utils.render_backup_verification, + sorter: { + property: 'verification', + transform: value => { + let state = value?.state ?? 'none'; + let order = PVE.Utils.verificationStateOrder; + return order[state] ?? order.__default__; + }, + }, + }, + }; + } + + me.callParent(); + + me.store.getSorters().clear(); + me.store.setSorters([ + { + property: 'vmid', + direction: 'ASC', + }, + { + property: 'vdate', + direction: 'DESC', + }, + ]); + }, +}); +Ext.define('PVE.panel.StorageBase', { + extend: 'Proxmox.panel.InputPanel', + controller: 'storageEdit', + + type: '', + + onGetValues: function(values) { + let me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.storage; + } + + values.disable = values.enable ? 0 : 1; + delete values.enable; + + return values; + }, + + initComponent: function() { + let me = this; + + me.column1.unshift({ + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'storage', + value: me.storageId || '', + fieldLabel: 'ID', + vtype: 'StorageId', + allowBlank: false, + }); + + me.column2 = me.column2 || []; + me.column2.unshift( + { + xtype: 'pveNodeSelector', + name: 'nodes', + reference: 'storageNodeRestriction', + disabled: me.storageId === 'local', + fieldLabel: gettext('Nodes'), + emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', + multiSelect: true, + autoSelect: false, + }, + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: true, + uncheckedValue: 0, + fieldLabel: gettext('Enable'), + }, + ); + + const qemuImgStorageTypes = ['dir', 'btrfs', 'nfs', 'cifs', 'glusterfs']; + + if (qemuImgStorageTypes.includes(me.type)) { + const preallocSelector = { + xtype: 'pvePreallocationSelector', + name: 'preallocation', + fieldLabel: gettext('Preallocation'), + allowBlank: false, + deleteEmpty: !me.isCreate, + value: '__default__', + }; + + me.advancedColumn1 = me.advancedColumn1 || []; + me.advancedColumn2 = me.advancedColumn2 || []; + if (me.advancedColumn2.length < me.advancedColumn1.length) { + me.advancedColumn2.unshift(preallocSelector); + } else { + me.advancedColumn1.unshift(preallocSelector); + } + } + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.BaseEdit', { + extend: 'Proxmox.window.Edit', + + apiCallDone: function(success, response, options) { + let me = this; + if (typeof me.ipanel.apiCallDone === "function") { + me.ipanel.apiCallDone(success, response, options); + } + }, + + initComponent: function() { + let me = this; + + me.isCreate = !me.storageId; + + if (me.isCreate) { + me.url = '/api2/extjs/storage'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/storage/' + me.storageId; + me.method = 'PUT'; + } + + me.ipanel = Ext.create(me.paneltype, { + title: gettext('General'), + type: me.type, + isCreate: me.isCreate, + storageId: me.storageId, + }); + + Ext.apply(me, { + subject: PVE.Utils.format_storage_type(me.type), + isAdd: true, + bodyPadding: 0, + items: { + xtype: 'tabpanel', + region: 'center', + layout: 'fit', + bodyPadding: 10, + items: [ + me.ipanel, + { + xtype: 'pveBackupJobPrunePanel', + title: gettext('Backup Retention'), + hasMaxProtected: true, + isCreate: me.isCreate, + keepAllDefaultForCreate: true, + showPBSHint: me.ipanel.isPBS, + fallbackHintHtml: gettext('Without any keep option, the node\'s vzdump.conf or `keep-all` is used as fallback for backup jobs'), + }, + ], + }, + }); + + if (me.ipanel.extraTabs) { + me.ipanel.extraTabs.forEach(panel => { + panel.isCreate = me.isCreate; + me.items.items.push(panel); + }); + } + + me.callParent(); + + if (!me.canDoBackups) { + // cannot mask now, not fully rendered until activated + me.down('pmxPruneInputPanel').needMask = true; + } + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + let values = response.result.data; + let ctypes = values.content || ''; + + values.content = ctypes.split(','); + + if (values.nodes) { + values.nodes = values.nodes.split(','); + } + values.enable = values.disable ? 0 : 1; + if (values['prune-backups']) { + let retention = PVE.Parser.parsePropertyString(values['prune-backups']); + delete values['prune-backups']; + Object.assign(values, retention); + } else if (values.maxfiles !== undefined) { + if (values.maxfiles > 0) { + values['keep-last'] = values.maxfiles; + } + delete values.maxfiles; + } + + me.query('inputpanel').forEach(panel => { + panel.setValues(values); + }); + }, + }); + } + }, +}); +Ext.define('PVE.storage.Browser', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.storage.Browser', + + onlineHelp: 'chapter_storage', + + initComponent: function() { + let me = this; + + let nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + let storeid = me.pveSelNode.data.storage; + if (!storeid) { + throw "no storage ID specified"; + } + + me.items = [ + { + title: gettext('Summary'), + xtype: 'pveStorageSummary', + iconCls: 'fa fa-book', + itemId: 'summary', + }, + ]; + + let caps = Ext.state.Manager.get('GuiCap'); + + Ext.apply(me, { + title: Ext.String.format(gettext("Storage {0} on node {1}"), `'${storeid}'`, `'${nodename}'`), + hstateid: 'storagetab', + }); + + if ( + caps.storage['Datastore.Allocate'] || + caps.storage['Datastore.AllocateSpace'] || + caps.storage['Datastore.Audit'] + ) { + let storageInfo = PVE.data.ResourceStore.findRecord( + 'id', + `storage/${nodename}/${storeid}`, + 0, // startIndex + false, // anyMatch + true, // caseSensitive + true, // exactMatch + ); + let res = storageInfo.data; + let plugin = res.plugintype; + let contents = res.content.split(','); + + let enableUpload = !!caps.storage['Datastore.AllocateTemplate']; + let enableDownloadUrl = enableUpload && !!(caps.nodes['Sys.Audit'] && caps.nodes['Sys.Modify']); + + if (contents.includes('backup')) { + me.items.push({ + xtype: 'pveStorageBackupView', + title: gettext('Backups'), + iconCls: 'fa fa-floppy-o', + itemId: 'contentBackup', + pluginType: plugin, + }); + } + if (contents.includes('images')) { + me.items.push({ + xtype: 'pveStorageImageView', + title: gettext('VM Disks'), + iconCls: 'fa fa-hdd-o', + itemId: 'contentImages', + content: 'images', + pluginType: plugin, + }); + } + if (contents.includes('rootdir')) { + me.items.push({ + xtype: 'pveStorageImageView', + title: gettext('CT Volumes'), + iconCls: 'fa fa-hdd-o lxc', + itemId: 'contentRootdir', + content: 'rootdir', + pluginType: plugin, + }); + } + if (contents.includes('iso')) { + me.items.push({ + xtype: 'pveStorageContentView', + title: gettext('ISO Images'), + iconCls: 'pve-itype-treelist-item-icon-cdrom', + itemId: 'contentIso', + content: 'iso', + pluginType: plugin, + enableUploadButton: enableUpload, + enableDownloadUrlButton: enableDownloadUrl, + useUploadButton: true, + }); + } + if (contents.includes('vztmpl')) { + me.items.push({ + xtype: 'pveStorageTemplateView', + title: gettext('CT Templates'), + iconCls: 'fa fa-file-o lxc', + itemId: 'contentVztmpl', + pluginType: plugin, + enableUploadButton: enableUpload, + enableDownloadUrlButton: enableDownloadUrl, + useUploadButton: true, + }); + } + if (contents.includes('snippets')) { + me.items.push({ + xtype: 'pveStorageContentView', + title: gettext('Snippets'), + iconCls: 'fa fa-file-code-o', + itemId: 'contentSnippets', + content: 'snippets', + pluginType: plugin, + }); + } + } + + if (caps.storage['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: `/storage/${storeid}`, + }); + } + + me.callParent(); + }, +}); +Ext.define('PVE.storage.CIFSScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCIFSScan', + + queryParam: 'server', + + valueField: 'share', + displayField: 'share', + matchFieldWidth: false, + listConfig: { + loadingText: gettext('Scanning...'), + width: 350, + }, + doRawQuery: Ext.emptyFn, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.cifsServer) { + me.store.removeAll(); + } + + var params = {}; + if (me.cifsUsername) { + params.username = me.cifsUsername; + } + if (me.cifsPassword) { + params.password = me.cifsPassword; + } + if (me.cifsDomain) { + params.domain = me.cifsDomain; + } + + me.store.getProxy().setExtraParams(params); + me.allQuery = me.cifsServer; + + me.callParent(); + }, + + resetProxy: function() { + let me = this; + me.lastQuery = null; + if (!me.readOnly && !me.disabled) { + if (me.isExpanded) { + me.collapse(); + } + } + }, + + setServer: function(server) { + if (this.cifsServer !== server) { + this.cifsServer = server; + this.resetProxy(); + } + }, + setUsername: function(username) { + if (this.cifsUsername !== username) { + this.cifsUsername = username; + this.resetProxy(); + } + }, + setPassword: function(password) { + if (this.cifsPassword !== password) { + this.cifsPassword = password; + this.resetProxy(); + } + }, + setDomain: function(domain) { + if (this.cifsDomain !== domain) { + this.cifsDomain = domain; + this.resetProxy(); + } + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + let store = Ext.create('Ext.data.Store', { + fields: ['description', 'share'], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/cifs', + }, + }); + store.sort('share', 'ASC'); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + + let picker = me.getPicker(); + // don't use monStoreErrors directly, it doesn't copes well with comboboxes + picker.mon(store, 'beforeload', function(s, operation, eOpts) { + picker.unmask(); + delete picker.minHeight; + }); + picker.mon(store.proxy, 'afterload', function(proxy, request, success) { + if (success) { + Proxmox.Utils.setErrorMask(picker, false); + return; + } + let error = request._operation.getError(); + let msg = Proxmox.Utils.getResponseErrorMessage(error); + if (msg) { + picker.minHeight = 100; + } + Proxmox.Utils.setErrorMask(picker, msg); + }); + }, +}); + +Ext.define('PVE.storage.CIFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_cifs', + + onGetValues: function(values) { + let me = this; + + if (values.password?.length === 0) { + delete values.password; + } + if (values.username?.length === 0) { + delete values.username; + } + + return me.callParent([values]); + }, + + initComponent: function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var exportField = me.down('field[name=share]'); + exportField.setServer(value); + } + }, + }, + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + value: '', + fieldLabel: gettext('Username'), + emptyText: gettext('Guest user'), + listeners: { + change: function(f, value) { + if (!me.isCreate) { + return; + } + var exportField = me.down('field[name=share]'); + exportField.setUsername(value); + }, + }, + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + inputType: 'password', + name: 'password', + value: me.isCreate ? '' : '********', + emptyText: me.isCreate ? gettext('None') : '', + fieldLabel: gettext('Password'), + minLength: 1, + listeners: { + change: function(f, value) { + let exportField = me.down('field[name=share]'); + exportField.setPassword(value); + }, + }, + }, + { + xtype: me.isCreate ? 'pveCIFSScan' : 'displayfield', + name: 'share', + value: '', + fieldLabel: 'Share', + allowBlank: false, + }, + ]; + + me.column2 = [ + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false, + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'domain', + value: me.isCreate ? '' : undefined, + fieldLabel: gettext('Domain'), + allowBlank: true, + listeners: { + change: function(f, value) { + if (me.isCreate) { + let exportField = me.down('field[name=share]'); + exportField.setDomain(value); + } + }, + }, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.storage.CephFSInputPanel', { + extend: 'PVE.panel.StorageBase', + controller: 'cephstorage', + + onlineHelp: 'storage_cephfs', + + viewModel: { + type: 'cephstorage', + }, + + setValues: function(values) { + if (values.monhost) { + this.viewModel.set('pveceph', false); + this.lookupReference('pvecephRef').setValue(false); + this.lookupReference('pvecephRef').resetOriginalValue(); + } + this.callParent([values]); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + me.type = 'cephfs'; + + me.column1 = []; + + me.column1.push( + { + xtype: 'textfield', + name: 'monhost', + vtype: 'HostList', + value: '', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}', + }, + fieldLabel: 'Monitor(s)', + allowBlank: false, + }, + { + xtype: 'displayfield', + reference: 'monhost', + bind: { + disabled: '{!pveceph}', + hidden: '{!pveceph}', + }, + value: '', + fieldLabel: 'Monitor(s)', + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + value: 'admin', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + }, + fieldLabel: gettext('User name'), + allowBlank: true, + }, + ); + + if (me.isCreate) { + me.column1.push({ + xtype: 'pveCephFSSelector', + nodename: me.nodename, + name: 'fs-name', + bind: { + disabled: '{!pveceph}', + submitValue: '{pveceph}', + hidden: '{!pveceph}', + }, + fieldLabel: gettext('FS Name'), + allowBlank: false, + }, { + xtype: 'textfield', + nodename: me.nodename, + name: 'fs-name', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}', + }, + fieldLabel: gettext('FS Name'), + }); + } + + me.column2 = [ + { + xtype: 'pveContentTypeSelector', + cts: ['backup', 'iso', 'vztmpl', 'snippets'], + fieldLabel: gettext('Content'), + name: 'content', + value: 'backup', + multiSelect: true, + allowBlank: false, + }, + ]; + + me.columnB = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'keyring', + fieldLabel: gettext('Secret Key'), + value: me.isCreate ? '' : '***********', + allowBlank: false, + bind: { + hidden: '{pveceph}', + disabled: '{pveceph}', + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'pveceph', + reference: 'pvecephRef', + bind: { + disabled: '{!pvecephPossible}', + value: '{pveceph}', + }, + checked: true, + uncheckedValue: 0, + submitValue: false, + hidden: !me.isCreate, + boxLabel: gettext('Use Proxmox VE managed hyper-converged cephFS'), + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.storage.DirInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_directory', + + initComponent: function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'path', + value: '', + fieldLabel: gettext('Directory'), + allowBlank: false, + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false, + }, + ]; + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'shared', + uncheckedValue: 0, + fieldLabel: gettext('Shared'), + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.storage.GlusterFsScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveGlusterFsScan', + + queryParam: 'server', + + valueField: 'volname', + displayField: 'volname', + matchFieldWidth: false, + listConfig: { + loadingText: 'Scanning...', + width: 350, + }, + doRawQuery: function() { + // nothing + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.glusterServer) { + me.store.removeAll(); + } + + me.allQuery = me.glusterServer; + + me.callParent(); + }, + + setServer: function(server) { + var me = this; + + me.glusterServer = server; + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: ['volname'], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/glusterfs', + }, + }); + + store.sort('volname', 'ASC'); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.GlusterFsInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_glusterfs', + + initComponent: function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var volumeField = me.down('field[name=volume]'); + volumeField.setServer(value); + volumeField.setValue(''); + } + }, + }, + }, + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + name: 'server2', + value: '', + fieldLabel: gettext('Second Server'), + allowBlank: true, + }, + { + xtype: me.isCreate ? 'pveGlusterFsScan' : 'displayfield', + name: 'volume', + value: '', + fieldLabel: 'Volume name', + allowBlank: false, + }, + { + xtype: 'pveContentTypeSelector', + cts: ['images', 'iso', 'backup', 'vztmpl', 'snippets'], + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.storage.ImageView', { + extend: 'PVE.storage.ContentView', + + alias: 'widget.pveStorageImageView', + + initComponent: function() { + var me = this; + + var nodename = me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + var storage = me.storage = me.pveSelNode.data.storage; + if (!me.storage) { + throw "no storage ID specified"; + } + + if (!me.content || (me.content !== 'images' && me.content !== 'rootdir')) { + throw "content needs to be either 'images' or 'rootdir'"; + } + + var sm = me.sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + me.store.load(); + }; + + me.tbar = [ + { + xtype: 'proxmoxButton', + selModel: sm, + text: gettext('Remove'), + disabled: true, + handler: function(btn, event, rec) { + let url = `/nodes/${nodename}/storage/${storage}/content/${rec.data.volid}`; + var vmid = rec.data.vmid; + + var store = PVE.data.ResourceStore; + + if (vmid && store.findVMID(vmid)) { + var guest_node = store.guestNode(vmid); + var storage_path = 'storage/' + nodename + '/' + storage; + + // allow to delete local backed images if a VMID exists on another node. + if (store.storageIsShared(storage_path) || guest_node === nodename) { + var msg = Ext.String.format( + gettext("Cannot remove image, a guest with VMID '{0}' exists!"), vmid); + msg += '
' + gettext("You can delete the image from the guest's hardware pane"); + + Ext.Msg.show({ + title: gettext('Cannot remove disk image.'), + icon: Ext.Msg.ERROR, + msg: msg, + }); + return; + } + } + var win = Ext.create('Proxmox.window.SafeDestroy', { + title: Ext.String.format(gettext("Destroy '{0}'"), rec.data.volid), + showProgress: true, + url: url, + item: { type: 'Image', id: vmid }, + taskName: 'unknownimgdel', + }).show(); + win.on('destroy', reload); + }, + }, + ]; + me.useCustomRemoveButton = true; + + me.callParent(); + }, +}); +Ext.define('PVE.storage.IScsiScan', { + extend: 'PVE.form.ComboBoxSetStoreNode', + alias: 'widget.pveIScsiScan', + + queryParam: 'portal', + valueField: 'target', + displayField: 'target', + matchFieldWidth: false, + allowBlank: false, + + listConfig: { + width: 350, + columns: [ + { + dataIndex: 'target', + flex: 1, + }, + ], + emptyText: PVE.Utils.renderNotFound(gettext('iSCSI Target')), + }, + + config: { + apiSuffix: '/scan/iscsi', + }, + + showNodeSelector: true, + + reload: function() { + let me = this; + if (!me.isDisabled()) { + me.getStore().load(); + } + }, + + setPortal: function(portal) { + let me = this; + me.portal = portal; + me.getStore().getProxy().setExtraParams({ portal }); + me.reload(); + }, + + setNodeName: function(value) { + let me = this; + me.callParent([value]); + me.reload(); + }, + + initComponent: function() { + let me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + let store = Ext.create('Ext.data.Store', { + fields: ['target', 'portal'], + proxy: { + type: 'proxmox', + url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`, + }, + }); + store.sort('target', 'ASC'); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.IScsiInputPanel', { + extend: 'PVE.panel.StorageBase', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'storage_open_iscsi', + + onGetValues: function(values) { + let me = this; + + values.content = values.luns ? 'images' : 'none'; + delete values.luns; + + return me.callParent([values]); + }, + + setValues: function(values) { + values.luns = values.content.indexOf('images') !== -1; + this.callParent([values]); + }, + + column1: [ + { + xtype: 'pmxDisplayEditField', + cbind: { + editable: '{isCreate}', + }, + + name: 'portal', + value: '', + fieldLabel: 'Portal', + allowBlank: false, + + editConfig: { + listeners: { + change: { + fn: function(f, value) { + let panel = this.up('inputpanel'); + let exportField = panel.lookup('iScsiTargetScan'); + if (exportField) { + exportField.setDisabled(!value); + exportField.setPortal(value); + exportField.setValue(''); + } + }, + buffer: 500, + }, + }, + }, + }, + { + cbind: { + xtype: (get) => get('isCreate') ? 'pveIScsiScan' : 'displayfield', + readOnly: '{!isCreate}', + disabled: '{isCreate}', + }, + + name: 'target', + value: '', + fieldLabel: gettext('Target'), + allowBlank: false, + reference: 'iScsiTargetScan', + listeners: { + nodechanged: function(value) { + this.up('inputpanel').lookup('storageNodeRestriction').setValue(value); + }, + }, + }, + ], + + column2: [ + { + xtype: 'checkbox', + name: 'luns', + checked: true, + fieldLabel: gettext('Use LUNs directly'), + }, + ], +}); +Ext.define('PVE.storage.VgSelector', { + extend: 'PVE.form.ComboBoxSetStoreNode', + alias: 'widget.pveVgSelector', + valueField: 'vg', + displayField: 'vg', + queryMode: 'local', + editable: false, + + listConfig: { + columns: [ + { + dataIndex: 'vg', + flex: 1, + }, + ], + emptyText: PVE.Utils.renderNotFound('VGs'), + }, + + config: { + apiSuffix: '/scan/lvm', + }, + + showNodeSelector: true, + + setNodeName: function(value) { + let me = this; + me.callParent([value]); + me.getStore().load(); + }, + + initComponent: function() { + let me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + let store = Ext.create('Ext.data.Store', { + autoLoad: {}, // true, + fields: ['vg', 'size', 'free'], + proxy: { + type: 'proxmox', + url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`, + }, + }); + + store.sort('vg', 'ASC'); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.BaseStorageSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveBaseStorageSelector', + + existingGroupsText: gettext("Existing volume groups"), + queryMode: 'local', + editable: false, + value: '', + valueField: 'storage', + displayField: 'text', + initComponent: function() { + let me = this; + + let store = Ext.create('Ext.data.Store', { + autoLoad: { + addRecords: true, + params: { + type: 'iscsi', + }, + }, + fields: ['storage', 'type', 'content', + { + name: 'text', + convert: function(value, record) { + if (record.data.storage) { + return record.data.storage + " (iSCSI)"; + } else { + return me.existingGroupsText; + } + }, + }], + proxy: { + type: 'proxmox', + url: '/api2/json/storage/', + }, + }); + + store.loadData([{ storage: '' }], true); + + store.sort('storage', 'ASC'); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.LunSelector', { + extend: 'PVE.form.FileSelector', + alias: 'widget.pveStorageLunSelector', + + nodename: 'localhost', + storageContent: 'images', + allowBlank: false, + + initComponent: function() { + let me = this; + + if (PVE.data.ResourceStore.getNodes().length > 1) { + me.errorHeight = 140; + Ext.apply(me.listConfig ?? {}, { + tbar: { + xtype: 'toolbar', + items: [ + { + xtype: "pveStorageScanNodeSelector", + autoSelect: false, + fieldLabel: gettext('Node to scan'), + listeners: { + change: (_field, value) => me.setNodename(value), + }, + }, + ], + }, + emptyText: me.listConfig?.emptyText ?? PVE.Utils.renderNotFound(gettext('Volume')), + }); + } + + me.callParent(); + }, + +}); + +Ext.define('PVE.storage.LVMInputPanel', { + extend: 'PVE.panel.StorageBase', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'storage_lvm', + + column1: [ + { + xtype: 'pveBaseStorageSelector', + name: 'basesel', + fieldLabel: gettext('Base storage'), + cbind: { + disabled: '{!isCreate}', + hidden: '{!isCreate}', + }, + submitValue: false, + listeners: { + change: function(f, value) { + let me = this; + let vgField = me.up('inputpanel').lookup('volumeGroupSelector'); + let vgNameField = me.up('inputpanel').lookup('vgName'); + let baseField = me.up('inputpanel').lookup('lunSelector'); + + vgField.setVisible(!value); + vgField.setDisabled(!!value); + + baseField.setVisible(!!value); + baseField.setDisabled(!value); + baseField.setStorage(value); + + vgNameField.setVisible(!!value); + vgNameField.setDisabled(!value); + }, + }, + }, + { + xtype: 'pveStorageLunSelector', + name: 'base', + fieldLabel: gettext('Base volume'), + reference: 'lunSelector', + hidden: true, + disabled: true, + }, + { + xtype: 'pveVgSelector', + name: 'vgname', + fieldLabel: gettext('Volume group'), + reference: 'volumeGroupSelector', + cbind: { + disabled: '{!isCreate}', + hidden: '{!isCreate}', + }, + allowBlank: false, + listeners: { + nodechanged: function(value) { + this.up('inputpanel').lookup('storageNodeRestriction').setValue(value); + }, + }, + }, + { + name: 'vgname', + fieldLabel: gettext('Volume group'), + reference: 'vgName', + cbind: { + xtype: (get) => get('isCreate') ? 'textfield' : 'displayfield', + hidden: '{isCreate}', + disabled: '{isCreate}', + }, + value: '', + allowBlank: false, + }, + { + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false, + }, + ], + + column2: [ + { + xtype: 'proxmoxcheckbox', + name: 'shared', + uncheckedValue: 0, + fieldLabel: gettext('Shared'), + }, + ], +}); +Ext.define('PVE.storage.TPoolSelector', { + extend: 'PVE.form.ComboBoxSetStoreNode', + alias: 'widget.pveTPSelector', + + queryParam: 'vg', + valueField: 'lv', + displayField: 'lv', + editable: false, + allowBlank: false, + + listConfig: { + emptyText: PVE.Utils.renderNotFound('Thin-Pool'), + columns: [ + { + dataIndex: 'lv', + flex: 1, + }, + ], + }, + + config: { + apiSuffix: '/scan/lvmthin', + }, + + reload: function() { + let me = this; + if (!me.isDisabled()) { + me.getStore().load(); + } + }, + + setVG: function(myvg) { + let me = this; + me.vg = myvg; + me.getStore().getProxy().setExtraParams({ vg: myvg }); + me.reload(); + }, + + setNodeName: function(value) { + let me = this; + me.callParent([value]); + me.reload(); + }, + + initComponent: function() { + let me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + let store = Ext.create('Ext.data.Store', { + fields: ['lv'], + proxy: { + type: 'proxmox', + url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`, + }, + }); + + store.sort('lv', 'ASC'); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.BaseVGSelector', { + extend: 'PVE.form.ComboBoxSetStoreNode', + alias: 'widget.pveBaseVGSelector', + + valueField: 'vg', + displayField: 'vg', + queryMode: 'local', + editable: false, + allowBlank: false, + + listConfig: { + columns: [ + { + dataIndex: 'vg', + flex: 1, + }, + ], + }, + + showNodeSelector: true, + + config: { + apiSuffix: '/scan/lvm', + }, + + setNodeName: function(value) { + let me = this; + me.callParent([value]); + me.getStore().load(); + }, + + initComponent: function() { + let me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + let store = Ext.create('Ext.data.Store', { + autoLoad: {}, + fields: ['vg', 'size', 'free'], + proxy: { + type: 'proxmox', + url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`, + }, + }); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.LvmThinInputPanel', { + extend: 'PVE.panel.StorageBase', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'storage_lvmthin', + + column1: [ + { + xtype: 'pmxDisplayEditField', + cbind: { + editable: '{isCreate}', + }, + + name: 'vgname', + fieldLabel: gettext('Volume group'), + + editConfig: { + xtype: 'pveBaseVGSelector', + listeners: { + nodechanged: function(value) { + let panel = this.up('inputpanel'); + panel.lookup('thinPoolSelector').setNodeName(value); + panel.lookup('storageNodeRestriction').setValue(value); + }, + change: function(f, value) { + let vgField = this.up('inputpanel').lookup('thinPoolSelector'); + if (vgField && !f.isDisabled()) { + vgField.setDisabled(!value); + vgField.setVG(value); + vgField.setValue(''); + } + }, + }, + }, + }, + { + xtype: 'pmxDisplayEditField', + cbind: { + editable: '{isCreate}', + }, + + name: 'thinpool', + fieldLabel: gettext('Thin Pool'), + allowBlank: false, + + editConfig: { + xtype: 'pveTPSelector', + reference: 'thinPoolSelector', + disabled: true, + }, + }, + { + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false, + }, + ], +}); +Ext.define('PVE.storage.BTRFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_btrfs', + + initComponent: function() { + let me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'path', + value: '', + fieldLabel: gettext('Path'), + allowBlank: false, + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false, + }, + ]; + + me.columnB = [ + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: `BTRFS integration is currently a technology preview.`, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.storage.NFSScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveNFSScan', + + queryParam: 'server', + + valueField: 'path', + displayField: 'path', + matchFieldWidth: false, + listConfig: { + loadingText: gettext('Scanning...'), + width: 350, + }, + doRawQuery: function() { + // do nothing + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.nfsServer) { + me.store.removeAll(); + } + + me.allQuery = me.nfsServer; + + me.callParent(); + }, + + setServer: function(server) { + var me = this; + + me.nfsServer = server; + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: ['path', 'options'], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/nfs', + }, + }); + + store.sort('path', 'ASC'); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.NFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_nfs', + + options: [], + + onGetValues: function(values) { + var me = this; + + var i; + var res = []; + for (i = 0; i < me.options.length; i++) { + var item = me.options[i]; + if (!item.match(/^vers=(.*)$/)) { + res.push(item); + } + } + if (values.nfsversion && values.nfsversion !== '__default__') { + res.push('vers=' + values.nfsversion); + } + delete values.nfsversion; + values.options = res.join(','); + if (values.options === '') { + delete values.options; + if (!me.isCreate) { + values.delete = "options"; + } + } + + return me.callParent([values]); + }, + + setValues: function(values) { + var me = this; + if (values.options) { + me.options = values.options.split(','); + me.options.forEach(function(item) { + var match = item.match(/^vers=(.*)$/); + if (match) { + values.nfsversion = match[1]; + } + }); + } + return me.callParent([values]); + }, + + initComponent: function() { + var me = this; + + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var exportField = me.down('field[name=export]'); + exportField.setServer(value); + exportField.setValue(''); + } + }, + }, + }, + { + xtype: me.isCreate ? 'pveNFSScan' : 'displayfield', + name: 'export', + value: '', + fieldLabel: 'Export', + allowBlank: false, + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false, + }, + ]; + + me.advancedColumn2 = [ + { + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('NFS Version'), + name: 'nfsversion', + value: '__default__', + deleteEmpty: false, + comboItems: [ + ['__default__', Proxmox.Utils.defaultText], + ['3', '3'], + ['4', '4'], + ['4.1', '4.1'], + ['4.2', '4.2'], + ], + }, + ]; + + me.callParent(); + }, +}); +/*global QRCode*/ +Ext.define('PVE.Storage.PBSKeyShow', { + extend: 'Ext.window.Window', + xtype: 'pvePBSKeyShow', + mixins: ['Proxmox.Mixin.CBind'], + + width: 600, + modal: true, + resizable: false, + title: gettext('Important: Save your Encryption Key'), + + // avoid that esc closes this by mistake, force user to more manual action + onEsc: Ext.emptyFn, + closable: false, + + items: [ + { + xtype: 'form', + layout: { + type: 'vbox', + align: 'stretch', + }, + bodyPadding: 10, + border: false, + defaults: { + anchor: '100%', + border: false, + padding: '10 0 0 0', + }, + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Key'), + labelWidth: 80, + inputId: 'encryption-key-value', + cbind: { + value: '{key}', + }, + editable: false, + }, + { + xtype: 'component', + html: gettext('Keep your encryption key safe, but easily accessible for disaster recovery.') + + '
' + gettext('We recommend the following safe-keeping strategy:'), + }, + { + xtyp: 'container', + layout: 'hbox', + items: [ + { + xtype: 'component', + html: '1. ' + gettext('Save the key in your password manager.'), + flex: 1, + }, + { + xtype: 'button', + text: gettext('Copy Key'), + iconCls: 'fa fa-clipboard x-btn-icon-el-default-toolbar-small', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + width: 110, + handler: function(b) { + document.getElementById('encryption-key-value').select(); + document.execCommand("copy"); + }, + }, + ], + }, + { + xtype: 'container', + layout: 'hbox', + items: [ + { + xtype: 'component', + html: '2. ' + gettext('Download the key to a USB (pen) drive, placed in secure vault.'), + flex: 1, + }, + { + xtype: 'button', + text: gettext('Download'), + iconCls: 'fa fa-download x-btn-icon-el-default-toolbar-small', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + width: 110, + handler: function(b) { + let win = this.up('window'); + + let pveID = PVE.ClusterName || window.location.hostname; + let name = `pve-${pveID}-storage-${win.sid}.enc`; + + let hiddenElement = document.createElement('a'); + hiddenElement.href = 'data:attachment/text,' + encodeURI(win.key); + hiddenElement.target = '_blank'; + hiddenElement.download = name; + hiddenElement.click(); + }, + }, + ], + }, + { + xtype: 'container', + layout: 'hbox', + items: [ + { + xtype: 'component', + html: '3. ' + gettext('Print as paperkey, laminated and placed in secure vault.'), + flex: 1, + }, + { + xtype: 'button', + text: gettext('Print Key'), + iconCls: 'fa fa-print x-btn-icon-el-default-toolbar-small', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + width: 110, + handler: function(b) { + let win = this.up('window'); + win.paperkey(win.key); + }, + }, + ], + }, + ], + }, + { + xtype: 'component', + border: false, + padding: '10 10 10 10', + userCls: 'pmx-hint', + html: gettext('Please save the encryption key - losing it will render any backup created with it unusable'), + }, + ], + buttons: [ + { + text: gettext('Close'), + handler: function(b) { + let win = this.up('window'); + win.close(); + }, + }, + ], + paperkey: function(keyString) { + let me = this; + + const key = JSON.parse(keyString); + + const qrwidth = 500; + let qrdiv = document.createElement('div'); + let qrcode = new QRCode(qrdiv, { + width: qrwidth, + height: qrwidth, + correctLevel: QRCode.CorrectLevel.H, + }); + qrcode.makeCode(keyString); + + let shortKeyFP = ''; + if (key.fingerprint) { + shortKeyFP = PVE.Utils.render_pbs_fingerprint(key.fingerprint); + } + + let printFrame = document.createElement("iframe"); + Object.assign(printFrame.style, { + position: "fixed", + right: "0", + bottom: "0", + width: "0", + height: "0", + border: "0", + }); + const prettifiedKey = JSON.stringify(key, null, 2); + const keyQrBase64 = qrdiv.children[0].toDataURL("image/png"); + const html = ` +

Encryption Key - Storage '${me.sid}' (${shortKeyFP})

+

+-----BEGIN PROXMOX BACKUP KEY----- +${prettifiedKey} +-----END PROXMOX BACKUP KEY-----

+
+ `; + + printFrame.src = "data:text/html;base64," + btoa(html); + document.body.appendChild(printFrame); + me.on('destroy', () => document.body.removeChild(printFrame)); + }, +}); + +Ext.define('PVE.panel.PBSEncryptionKeyTab', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pvePBSEncryptionKeyTab', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'storage_pbs_encryption', + + onGetValues: function(form) { + let values = {}; + if (form.cryptMode === 'upload') { + values['encryption-key'] = form['crypt-key-upload']; + } else if (form.cryptMode === 'autogenerate') { + values['encryption-key'] = 'autogen'; + } else if (form.cryptMode === 'none') { + if (!this.isCreate) { + values.delete = ['encryption-key']; + } + } + return values; + }, + + setValues: function(values) { + let me = this; + let vm = me.getViewModel(); + + let cryptKeyInfo = values['encryption-key']; + if (cryptKeyInfo) { + let icon = ' '; + if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) { // new style fingerprint + let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo); + values['crypt-key-fp'] = icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`; + } else { + // old key without FP + values['crypt-key-fp'] = icon + gettext('Active'); + } + } else { + values['crypt-key-fp'] = gettext('None'); + let cryptModeNone = me.down('radiofield[inputValue=none]'); + cryptModeNone.setBoxLabel(gettext('Do not encrypt backups')); + cryptModeNone.setValue(true); + } + vm.set('keepCryptVisible', !!cryptKeyInfo); + vm.set('allowEdit', !cryptKeyInfo); + + me.callParent([values]); + }, + + viewModel: { + data: { + allowEdit: true, + keepCryptVisible: false, + }, + formulas: { + showDangerousHint: get => { + let allowEdit = get('allowEdit'); + return get('keepCryptVisible') && allowEdit; + }, + }, + }, + + items: [ + { + xtype: 'displayfield', + name: 'crypt-key-fp', + fieldLabel: gettext('Encryption Key'), + padding: '2 0', + }, + { + xtype: 'checkbox', + name: 'crypt-allow-edit', + boxLabel: gettext('Edit existing encryption key (dangerous!)'), + hidden: true, + submitValue: false, + isDirty: () => false, + bind: { + hidden: '{!keepCryptVisible}', + value: '{allowEdit}', + }, + }, + { + xtype: 'radiofield', + name: 'cryptMode', + inputValue: 'keep', + boxLabel: gettext('Keep encryption key'), + padding: '0 0 0 25', + cbind: { + hidden: '{isCreate}', + checked: '{!isCreate}', + }, + bind: { + hidden: '{!keepCryptVisible}', + disabled: '{!allowEdit}', + }, + }, + { + xtype: 'radiofield', + name: 'cryptMode', + inputValue: 'none', + checked: true, + padding: '0 0 0 25', + cbind: { + disabled: '{!isCreate}', + checked: '{isCreate}', + boxLabel: get => get('isCreate') + ? gettext('Do not encrypt backups') + : gettext('Delete existing encryption key'), + }, + bind: { + disabled: '{!allowEdit}', + }, + }, + { + xtype: 'radiofield', + name: 'cryptMode', + inputValue: 'autogenerate', + boxLabel: gettext('Auto-generate a client encryption key'), + padding: '0 0 0 25', + cbind: { + disabled: '{!isCreate}', + }, + bind: { + disabled: '{!allowEdit}', + }, + }, + { + xtype: 'radiofield', + name: 'cryptMode', + inputValue: 'upload', + boxLabel: gettext('Upload an existing client encryption key'), + padding: '0 0 0 25', + cbind: { + disabled: '{!isCreate}', + }, + bind: { + disabled: '{!allowEdit}', + }, + listeners: { + change: function(f, value) { + let panel = this.up('inputpanel'); + if (!panel.rendered) { + return; + } + let uploadKeyField = panel.down('field[name=crypt-key-upload]'); + uploadKeyField.setDisabled(!value); + uploadKeyField.setHidden(!value); + + let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]'); + uploadKeyButton.setDisabled(!value); + uploadKeyButton.setHidden(!value); + + if (value) { + uploadKeyField.validate(); + } else { + uploadKeyField.reset(); + } + }, + }, + }, + { + xtype: 'fieldcontainer', + layout: 'hbox', + items: [ + { + xtype: 'proxmoxtextfield', + name: 'crypt-key-upload', + fieldLabel: gettext('Key'), + value: '', + disabled: true, + hidden: true, + allowBlank: false, + labelAlign: 'right', + flex: 1, + emptyText: gettext('You can drag-and-drop a key file here.'), + validator: function(value) { + if (value.length) { + let key; + try { + key = JSON.parse(value); + } catch (e) { + return "Failed to parse key - " + e; + } + if (key.data === undefined) { + return "Does not seems like a valid Proxmox Backup key!"; + } + } + return true; + }, + afterRender: function() { + if (!window.FileReader) { + // No FileReader support in this browser + return; + } + let cancel = function(ev) { + ev = ev.event; + if (ev.preventDefault) { + ev.preventDefault(); + } + }; + this.inputEl.on('dragover', cancel); + this.inputEl.on('dragenter', cancel); + this.inputEl.on('drop', ev => { + cancel(ev); + let files = ev.event.dataTransfer.files; + PVE.Utils.loadTextFromFile(files[0], v => this.setValue(v)); + }); + }, + }, + { + xtype: 'filebutton', + name: 'crypt-upload-button', + iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + margin: '0 0 0 4', + disabled: true, + hidden: true, + listeners: { + change: function(btn, e, value) { + let ev = e.event; + let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]'); + PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v)); + btn.reset(); + }, + }, + }, + ], + }, + { + xtype: 'component', + border: false, + padding: '5 2', + userCls: 'pmx-hint', + html: // `${gettext('Warning')}: ` + + ` ` + + gettext('Deleting or replacing the encryption key will break restoring backups created with it!'), + hidden: true, + bind: { + hidden: '{!showDangerousHint}', + }, + }, + ], +}); + +Ext.define('PVE.storage.PBSInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_pbs', + + apiCallDone: function(success, response, options) { + let res = response.result.data; + if (!(res && res.config && res.config['encryption-key'])) { + return; + } + let key = res.config['encryption-key']; + Ext.create('PVE.Storage.PBSKeyShow', { + autoShow: true, + sid: res.storage, + key: key, + }); + }, + + isPBS: true, // HACK + + extraTabs: [ + { + xtype: 'pvePBSEncryptionKeyTab', + title: gettext('Encryption'), + }, + ], + + setValues: function(values) { + let me = this; + + let server = values.server; + if (values.port !== undefined) { + if (Proxmox.Utils.IP6_match.test(server)) { + server = `[${server}]`; + } + server += `:${values.port}`; + } + values.hostport = server; + + return me.callParent([values]); + }, + + initComponent: function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + fieldLabel: gettext('Server'), + allowBlank: false, + name: 'hostport', + submitValue: false, + vtype: 'HostPort', + listeners: { + change: function(field, newvalue) { + let server = newvalue; + let port; + + let match = Proxmox.Utils.HostPort_match.exec(newvalue); + if (match === null) { + match = Proxmox.Utils.HostPortBrackets_match.exec(newvalue); + if (match === null) { + match = Proxmox.Utils.IP6_dotnotation_match.exec(newvalue); + } + } + + if (match !== null) { + server = match[1]; + if (match[2] !== undefined) { + port = match[2]; + } + } + + field.up('inputpanel').down('field[name=server]').setValue(server); + field.up('inputpanel').down('field[name=port]').setValue(port); + }, + }, + }, + { + xtype: 'proxmoxtextfield', + hidden: true, + name: 'server', + submitValue: me.isCreate, // it is fixed + }, + { + xtype: 'proxmoxtextfield', + hidden: true, + deleteEmpty: !me.isCreate, + name: 'port', + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + value: '', + emptyText: gettext('Example') + ': admin@pbs', + fieldLabel: gettext('Username'), + regex: /\S+@\w+/, + regexText: gettext('Example') + ': admin@pbs', + allowBlank: false, + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + inputType: 'password', + name: 'password', + value: me.isCreate ? '' : '********', + emptyText: me.isCreate ? gettext('None') : '', + fieldLabel: gettext('Password'), + allowBlank: false, + }, + ]; + + me.column2 = [ + { + xtype: 'displayfield', + name: 'content', + value: 'backup', + submitValue: true, + fieldLabel: gettext('Content'), + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'datastore', + value: '', + fieldLabel: 'Datastore', + allowBlank: false, + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'namespace', + value: '', + emptyText: gettext('Root'), + fieldLabel: gettext('Namespace'), + allowBlank: true, + }, + ]; + + me.columnB = [ + { + xtype: 'proxmoxtextfield', + name: 'fingerprint', + value: me.isCreate ? null : undefined, + fieldLabel: gettext('Fingerprint'), + emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'), + regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/, + regexText: gettext('Example') + ': AB:CD:EF:...', + deleteEmpty: !me.isCreate, + allowBlank: true, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.storage.Ceph.Model', { + extend: 'Ext.app.ViewModel', + alias: 'viewmodel.cephstorage', + + data: { + pveceph: true, + pvecephPossible: true, + namespacePresent: false, + }, +}); + +Ext.define('PVE.storage.Ceph.Controller', { + extend: 'PVE.controller.StorageEdit', + alias: 'controller.cephstorage', + + control: { + '#': { + afterrender: 'queryMonitors', + }, + 'textfield[name=username]': { + disable: 'resetField', + }, + 'displayfield[name=monhost]': { + enable: 'queryMonitors', + }, + 'textfield[name=monhost]': { + disable: 'resetField', + enable: 'resetField', + }, + 'textfield[name=namespace]': { + change: 'updateNamespaceHint', + }, + }, + resetField: function(field) { + field.reset(); + }, + updateNamespaceHint: function(field, newVal, oldVal) { + this.getViewModel().set('namespacePresent', newVal); + }, + queryMonitors: function(field, newVal, oldVal) { + // we get called with two signatures, the above one for a field + // change event and the afterrender from the view, this check only + // can be true for the field change one and omit the API request if + // pveceph got unchecked - as it's not needed there. + if (field && !newVal && oldVal) { + return; + } + var view = this.getView(); + var vm = this.getViewModel(); + if (!(view.isCreate || vm.get('pveceph'))) { + return; // only query on create or if editing a pveceph store + } + + var monhostField = this.lookupReference('monhost'); + + Proxmox.Utils.API2Request({ + url: '/api2/json/nodes/localhost/ceph/mon', + method: 'GET', + scope: this, + callback: function(options, success, response) { + var data = response.result.data; + if (response.status === 200) { + if (data.length > 0) { + var monhost = Ext.Array.pluck(data, 'name').sort().join(','); + monhostField.setValue(monhost); + monhostField.resetOriginalValue(); + if (view.isCreate) { + vm.set('pvecephPossible', true); + } + } else { + vm.set('pveceph', false); + } + } else { + vm.set('pveceph', false); + vm.set('pvecephPossible', false); + } + }, + }); + }, +}); + +Ext.define('PVE.storage.RBDInputPanel', { + extend: 'PVE.panel.StorageBase', + controller: 'cephstorage', + + onlineHelp: 'ceph_rados_block_devices', + + viewModel: { + type: 'cephstorage', + }, + + setValues: function(values) { + if (values.monhost) { + this.viewModel.set('pveceph', false); + this.lookupReference('pvecephRef').setValue(false); + this.lookupReference('pvecephRef').resetOriginalValue(); + } + if (values.namespace) { + this.getViewModel().set('namespacePresent', true); + } + this.callParent([values]); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + me.type = 'rbd'; + + me.column1 = []; + + if (me.isCreate) { + me.column1.push({ + xtype: 'pveCephPoolSelector', + nodename: me.nodename, + name: 'pool', + bind: { + disabled: '{!pveceph}', + submitValue: '{pveceph}', + hidden: '{!pveceph}', + }, + fieldLabel: gettext('Pool'), + allowBlank: false, + }, { + xtype: 'textfield', + name: 'pool', + value: 'rbd', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}', + }, + fieldLabel: gettext('Pool'), + allowBlank: false, + }); + } else { + me.column1.push({ + xtype: 'displayfield', + nodename: me.nodename, + name: 'pool', + fieldLabel: gettext('Pool'), + allowBlank: false, + }); + } + + me.column1.push( + { + xtype: 'textfield', + name: 'monhost', + vtype: 'HostList', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}', + }, + value: '', + fieldLabel: 'Monitor(s)', + allowBlank: false, + }, + { + xtype: 'displayfield', + reference: 'monhost', + bind: { + disabled: '{!pveceph}', + hidden: '{!pveceph}', + }, + value: '', + fieldLabel: 'Monitor(s)', + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + }, + value: 'admin', + fieldLabel: gettext('User name'), + allowBlank: true, + }, + ); + + me.column2 = [ + { + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images'], + multiSelect: true, + allowBlank: false, + }, + { + xtype: 'proxmoxcheckbox', + name: 'krbd', + uncheckedValue: 0, + fieldLabel: 'KRBD', + }, + ]; + + me.columnB = [ + { + xtype: me.isCreate ? 'textarea' : 'displayfield', + name: 'keyring', + fieldLabel: 'Keyring', + value: me.isCreate ? '' : '***********', + allowBlank: false, + bind: { + hidden: '{pveceph}', + disabled: '{pveceph}', + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'pveceph', + reference: 'pvecephRef', + bind: { + disabled: '{!pvecephPossible}', + value: '{pveceph}', + }, + checked: true, + uncheckedValue: 0, + submitValue: false, + hidden: !me.isCreate, + boxLabel: gettext('Use Proxmox VE managed hyper-converged ceph pool'), + }, + ]; + + me.advancedColumn1 = [ + { + xtype: 'pmxDisplayEditField', + editable: me.isCreate, + name: 'namespace', + value: '', + fieldLabel: gettext('Namespace'), + allowBlank: true, + }, + ]; + me.advancedColumn2 = [ + { + xtype: 'displayfield', + name: 'namespace-hint', + userCls: 'pmx-hint', + value: gettext('RBD namespaces must be created manually!'), + bind: { + hidden: '{!namespacePresent}', + }, + }, + ]; + + me.callParent(); + }, +}); +Ext.define('PVE.storage.StatusView', { + extend: 'Proxmox.panel.StatusView', + alias: 'widget.pveStorageStatusView', + + height: 230, + title: gettext('Status'), + + layout: { + type: 'vbox', + align: 'stretch', + }, + + defaults: { + xtype: 'pmxInfoWidget', + padding: '0 30 5 30', + }, + items: [ + { + xtype: 'box', + height: 30, + }, + { + itemId: 'enabled', + title: gettext('Enabled'), + printBar: false, + textField: 'disabled', + renderer: Proxmox.Utils.format_neg_boolean, + }, + { + itemId: 'active', + title: gettext('Active'), + printBar: false, + textField: 'active', + renderer: Proxmox.Utils.format_boolean, + }, + { + itemId: 'content', + title: gettext('Content'), + printBar: false, + textField: 'content', + renderer: PVE.Utils.format_content_types, + }, + { + itemId: 'type', + title: gettext('Type'), + printBar: false, + textField: 'type', + renderer: PVE.Utils.format_storage_type, + }, + { + xtype: 'box', + height: 10, + }, + { + itemId: 'usage', + title: gettext('Usage'), + valueField: 'used', + maxField: 'total', + renderer: (val, max) => { + if (max === undefined) { + return val; + } + return Proxmox.Utils.render_size_usage(val, max, true); + }, + }, + ], + + updateTitle: function() { + // nothing + }, +}); +Ext.define('PVE.storage.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveStorageSummary', + scrollable: true, + bodyPadding: 5, + tbar: [ + '->', + { + xtype: 'proxmoxRRDTypeSelector', + }, + ], + layout: { + type: 'column', + }, + defaults: { + padding: 5, + columnWidth: 1, + }, + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var storage = me.pveSelNode.data.storage; + if (!storage) { + throw "no storage ID specified"; + } + + var rstore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/status", + interval: 1000, + }); + + var rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/rrddata", + model: 'pve-rrd-storage', + }); + + Ext.apply(me, { + items: [ + { + xtype: 'pveStorageStatusView', + pveSelNode: me.pveSelNode, + rstore: rstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Usage'), + fields: ['total', 'used'], + fieldTitles: ['Total Size', 'Used Size'], + store: rrdstore, + }, + ], + listeners: { + activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); }, + destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); }, + }, + }); + + me.callParent(); + }, +}); +Ext.define('PVE.grid.TemplateSelector', { + extend: 'Ext.grid.GridPanel', + + alias: 'widget.pveTemplateSelector', + + stateful: true, + stateId: 'grid-template-selector', + viewConfig: { + trackOver: false, + }, + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var baseurl = "/nodes/" + me.nodename + "/aplinfo"; + var store = new Ext.data.Store({ + model: 'pve-aplinfo', + groupField: 'section', + proxy: { + type: 'proxmox', + url: '/api2/json' + baseurl, + }, + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var groupingFeature = Ext.create('Ext.grid.feature.Grouping', { + groupHeaderTpl: '{[ "Section: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})', + }); + + var reload = function() { + store.load(); + }; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + '->', + gettext('Search'), + { + xtype: 'textfield', + width: 200, + enableKeyEvents: true, + listeners: { + buffer: 500, + keyup: function(field) { + var value = field.getValue().toLowerCase(); + store.clearFilter(true); + store.filterBy(function(rec) { + return rec.data.package.toLowerCase().indexOf(value) !== -1 || + rec.data.headline.toLowerCase().indexOf(value) !== -1; + }); + }, + }, + }, + ], + features: [groupingFeature], + columns: [ + { + header: gettext('Type'), + width: 80, + dataIndex: 'type', + }, + { + header: gettext('Package'), + flex: 1, + dataIndex: 'package', + }, + { + header: gettext('Version'), + width: 80, + dataIndex: 'version', + }, + { + header: gettext('Description'), + flex: 1.5, + renderer: Ext.String.htmlEncode, + dataIndex: 'headline', + }, + ], + listeners: { + afterRender: reload, + }, + }); + + me.callParent(); + }, + +}, function() { + Ext.define('pve-aplinfo', { + extend: 'Ext.data.Model', + fields: [ + 'template', 'type', 'package', 'version', 'headline', 'infopage', + 'description', 'os', 'section', + ], + idProperty: 'template', + }); +}); + +Ext.define('PVE.storage.TemplateDownload', { + extend: 'Ext.window.Window', + alias: 'widget.pveTemplateDownload', + + modal: true, + title: gettext('Templates'), + layout: 'fit', + width: 900, + height: 600, + initComponent: function() { + var me = this; + + var grid = Ext.create('PVE.grid.TemplateSelector', { + border: false, + scrollable: true, + nodename: me.nodename, + }); + + var sm = grid.getSelectionModel(); + + var submitBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Download'), + disabled: true, + selModel: sm, + handler: function(button, event, rec) { + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/aplinfo', + params: { + storage: me.storage, + template: rec.data.template, + }, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + + Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + listeners: { + destroy: me.reloadGrid, + }, + }).show(); + + me.close(); + }, + }); + }, + }); + + Ext.apply(me, { + items: grid, + buttons: [submitBtn], + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.TemplateView', { + extend: 'PVE.storage.ContentView', + + alias: 'widget.pveStorageTemplateView', + + initComponent: function() { + var me = this; + + var nodename = me.nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var storage = me.storage = me.pveSelNode.data.storage; + if (!storage) { + throw "no storage ID specified"; + } + + me.content = 'vztmpl'; + + var reload = function() { + me.store.load(); + }; + + var templateButton = Ext.create('Proxmox.button.Button', { + itemId: 'tmpl-btn', + text: gettext('Templates'), + handler: function() { + var win = Ext.create('PVE.storage.TemplateDownload', { + nodename: nodename, + storage: storage, + reloadGrid: reload, + }); + win.show(); + }, + }); + + me.tbar = [templateButton]; + me.useUploadButton = true; + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.ZFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + viewModel: { + parent: null, + data: { + isComstar: true, + isFreeNAS: false, + isLIO: false, + isToken: false, + hasWriteCacheOption: true, + }, + formulas: { + hideUsername: function(get) { + return (!get('isFreeNAS') || !(get('isFreeNAS') && !get('isToken'))); + }, + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'field[name=iscsiprovider]': { + change: 'changeISCSIProvider', + }, + 'field[name=truenas_token_auth]': { + change: 'changeUsername', + }, + }, + changeISCSIProvider: function(f, newVal, oldVal) { + var me = this; + var vm = this.getViewModel(); + vm.set('isLIO', newVal === 'LIO'); + vm.set('isComstar', newVal === 'comstar'); + vm.set('isFreeNAS', newVal === 'freenas'); + vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'freenas' || newVal === 'istgt'); + if (newVal !== 'freenas') { + me.lookupReference('freenas_use_ssl_field').setValue(false); + me.lookupReference('truenas_token_auth_field').setValue(false); + me.lookupReference('freenas_apiv4_host_field').setValue(''); + me.lookupReference('freenas_user_field').setValue(''); + me.lookupReference('freenas_user_field').allowBlank = true; + me.lookupReference('truenas_secret_field').setValue(''); + me.lookupReference('truenas_secret_field').allowBlank = true; + me.lookupReference('truenas_confirm_secret_field').setValue(''); + me.lookupReference('truenas_confirm_secret_field').allowBlank = true; + } else { + me.lookupReference('freenas_user_field').allowBlank = false; + me.lookupReference('truenas_secret_field').allowBlank = false; + me.lookupReference('truenas_confirm_secret_field').allowBlank = false; + } + }, + changeUsername: function(f, newVal, oldVal) { + var me = this; + var vm = me.getViewModel(); + vm.set('isToken', newVal); + me.lookupReference('freenas_user_field').allowBlank = newVal; + if (newVal) { + me.lookupReference('freenas_user_field').setValue(''); + } + }, + }, + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.content = 'images'; + } + + values.nowritecache = values.writecache ? 0 : 1; + delete values.writecache; + console.warn(values.freenas_password); + if (values.freenas_password) { + values.truenas_secret = values.freenas_password; + } + console.warn(values.truenas_secret); + + return me.callParent([values]); + }, + + setValues: function(values) { + if (values.freenas_password) { + values.truenas_secret = values.freenas_password; + } + values.truenas_confirm_secret = values.truenas_secret; + values.writecache = values.nowritecache ? 0 : 1; + this.callParent([values]); + }, + + initComponent: function() { + var me = this; + + var tnsecret = Ext.create('Ext.form.TextField', { + xtype: 'proxmoxtextfield', + name: 'truenas_secret', + reference: 'truenas_secret_field', + inputType: me.isCreate ? '' : 'password', + value: '', + editable: true, + emptyText: Proxmox.Utils.noneText, + bind: { + hidden: '{!isFreeNAS}' + }, + fieldLabel: gettext('API Password'), + change: function(f, value) { + if (f.rendered) { + f.up().down('field[name=truenas_confirm_secret]').validate(); + } + }, + }); + + var tnconfirmsecret = Ext.create('Ext.form.TextField', { + xtype: 'proxmoxtextfield', + name: 'truenas_confirm_secret', + reference: 'truenas_confirm_secret_field', + inputType: me.isCreate ? '' : 'password', + value: '', + editable: true, + submitValue: false, + emptyText: Proxmox.Utils.noneText, + bind: { + hidden: '{!isFreeNAS}' + }, + fieldLabel: gettext('Confirm API Password'), + validator: function(value) { + var pw = me.up().down('field[name=truenas_secret]').getValue(); + if (pw !== value) { + return "Secrets do not match!"; + } + return true; + }, + }); + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'portal', + value: '', + fieldLabel: gettext('Portal'), + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'pool', + value: '', + fieldLabel: gettext('Pool'), + allowBlank: false, + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'blocksize', + value: '4k', + fieldLabel: gettext('ZFS Block Size'), + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'target', + value: '', + fieldLabel: gettext('Target'), + allowBlank: false, + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'comstar_tg', + value: '', + fieldLabel: gettext('Target group'), + bind: { + hidden: '{!isComstar}' + }, + allowBlank: true, + }, + { + xtype: 'proxmoxcheckbox', + name: 'freenas_use_ssl', + reference: 'freenas_use_ssl_field', + inputId: 'freenas_use_ssl_field', + checked: false, + bind: { + hidden: '{!isFreeNAS}' + }, + uncheckedValue: 0, + fieldLabel: gettext('API use SSL'), + }, + { + xtype: 'proxmoxcheckbox', + name: 'truenas_token_auth', + reference: 'truenas_token_auth_field', + inputId: 'truenas_use_token_auth_field', + checked: false, + listeners: { + change: function(field, newValue) { + if (newValue === true) { + tnsecret.labelEl.update('API Token'); + tnconfirmsecret.labelEl.update('Confirm API Token'); + me.lookupReference('freenas_user_field').setValue(''); + me.lookupReference('freenas_user_field').allowBlank = true; + } else { + tnsecret.labelEl.update('API Password'); + tnconfirmsecret.labelEl.update('Confirm API Password'); + me.lookupReference('freenas_user_field').allowBlank = false; + } + }, + }, + bind: { + hidden: '{!isFreeNAS}' + }, + uncheckedValue: 0, + fieldLabel: gettext('API Token Auth'), + }, + { + xtype: 'textfield', + name: 'freenas_user', + reference: 'freenas_user_field', + inputId: 'freenas_user_field', + value: '', + fieldLabel: gettext('API Username'), + bind: { + hidden: '{hideUsername}' + }, + }, + ]; + + me.column2 = [ + { + xtype: me.isCreate ? 'pveiScsiProviderSelector' : 'displayfield', + name: 'iscsiprovider', + value: 'comstar', + fieldLabel: gettext('iSCSI Provider'), + allowBlank: false, + }, + { + xtype: 'proxmoxcheckbox', + name: 'sparse', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Thin provision'), + }, + { + xtype: 'proxmoxcheckbox', + name: 'writecache', + checked: true, + bind: me.isCreate ? { disabled: '{!hasWriteCacheOption}' } : { hidden: '{!hasWriteCacheOption}' }, + uncheckedValue: 0, + fieldLabel: gettext('Write cache'), + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'comstar_hg', + value: '', + bind: { + hidden: '{!isComstar}' + }, + fieldLabel: gettext('Host group'), + allowBlank: true, + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'lio_tpg', + value: '', + bind: { + hidden: '{!isLIO}' + }, + fieldLabel: gettext('Target portal group'), + allowBlank: true + }, + { + xtype: 'proxmoxtextfield', + name: 'freenas_apiv4_host', + reference: 'freenas_apiv4_host_field', + value: '', + editable: true, + emptyText: Proxmox.Utils.noneText, + bind: { + hidden: '{!isFreeNAS}' + }, + fieldLabel: gettext('API IPv4 Host'), + }, + tnsecret, + tnconfirmsecret, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.ZFSPoolSelector', { + extend: 'PVE.form.ComboBoxSetStoreNode', + alias: 'widget.pveZFSPoolSelector', + valueField: 'pool', + displayField: 'pool', + queryMode: 'local', + editable: false, + allowBlank: false, + + listConfig: { + columns: [ + { + dataIndex: 'pool', + flex: 1, + }, + ], + emptyText: PVE.Utils.renderNotFound(gettext('ZFS Pool')), + }, + + config: { + apiSuffix: '/scan/zfs', + }, + + showNodeSelector: true, + + setNodeName: function(value) { + let me = this; + me.callParent([value]); + me.getStore().load(); + }, + + initComponent: function() { + let me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + let store = Ext.create('Ext.data.Store', { + autoLoad: {}, // true, + fields: ['pool', 'size', 'free'], + proxy: { + type: 'proxmox', + url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`, + }, + }); + store.sort('pool', 'ASC'); + + Ext.apply(me, { + store: store, + }); + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.ZFSPoolInputPanel', { + extend: 'PVE.panel.StorageBase', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'storage_zfspool', + + column1: [ + { + xtype: 'pmxDisplayEditField', + cbind: { + editable: '{isCreate}', + }, + + name: 'pool', + fieldLabel: gettext('ZFS Pool'), + allowBlank: false, + + editConfig: { + xtype: 'pveZFSPoolSelector', + reference: 'zfsPoolSelector', + listeners: { + nodechanged: function(value) { + this.up('inputpanel').lookup('storageNodeRestriction').setValue(value); + }, + }, + }, + }, + { + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false, + }, + ], + + column2: [ + { + xtype: 'proxmoxcheckbox', + name: 'sparse', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Thin provision'), + }, + { + xtype: 'textfield', + name: 'blocksize', + emptyText: '8k', + fieldLabel: gettext('Block Size'), + allowBlank: true, + }, + ], +}); +/* + * Workspace base class + * + * popup login window when auth fails (call onLogin handler) + * update (re-login) ticket every 15 minutes + * + */ + +Ext.define('PVE.Workspace', { + extend: 'Ext.container.Viewport', + + title: 'Proxmox Virtual Environment', + + loginData: null, // Data from last login call + + onLogin: function(loginData) { + // override me + }, + + // private + updateLoginData: function(loginData) { + let me = this; + me.loginData = loginData; + Proxmox.Utils.setAuthData(loginData); + + let rt = me.down('pveResourceTree'); + rt.setDatacenterText(loginData.clustername); + PVE.ClusterName = loginData.clustername; + + if (loginData.cap) { + Ext.state.Manager.set('GuiCap', loginData.cap); + } + me.response401count = 0; + + me.onLogin(loginData); + }, + + // private + showLogin: function() { + let me = this; + + Proxmox.Utils.authClear(); + Ext.state.Manager.clear('GuiCap'); + Proxmox.UserName = null; + me.loginData = null; + + if (!me.login) { + me.login = Ext.create('PVE.window.LoginWindow', { + handler: function(data) { + me.login = null; + me.updateLoginData(data); + Proxmox.Utils.checked_command(Ext.emptyFn); // display subscription status + }, + }); + } + me.onLogin(null); + me.login.show(); + }, + + initComponent: function() { + let me = this; + + Ext.tip.QuickTipManager.init(); + + // fixme: what about other errors + Ext.Ajax.on('requestexception', function(conn, response, options) { + if ((response.status === 401 || response.status === '401') && !PVE.Utils.silenceAuthFailures) { // auth failure + // don't immediately show as logged out to cope better with some big + // upgrades, which may temporarily produce a false positive 401 err + me.response401count++; + if (me.response401count > 5) { + me.showLogin(); + } + } + }); + + me.callParent(); + + if (!Proxmox.Utils.authOK()) { + me.showLogin(); + } else if (me.loginData) { + me.onLogin(me.loginData); + } + + Ext.TaskManager.start({ + run: function() { + let ticket = Proxmox.Utils.authOK(); + if (!ticket || !Proxmox.UserName) { + return; + } + + Ext.Ajax.request({ + params: { + username: Proxmox.UserName, + password: ticket, + }, + url: '/api2/json/access/ticket', + method: 'POST', + success: function(response, opts) { + let obj = Ext.decode(response.responseText); + me.updateLoginData(obj.data); + }, + }); + }, + interval: 15 * 60 * 1000, + }); + }, +}); + +Ext.define('PVE.StdWorkspace', { + extend: 'PVE.Workspace', + + alias: ['widget.pveStdWorkspace'], + + // private + setContent: function(comp) { + let me = this; + + let view = me.child('#content'); + let layout = view.getLayout(); + let current = layout.getActiveItem(); + + if (comp) { + Proxmox.Utils.setErrorMask(view, false); + comp.border = false; + view.add(comp); + if (current !== null && layout.getNext()) { + layout.next(); + let task = Ext.create('Ext.util.DelayedTask', function() { + view.remove(current); + }); + task.delay(10); + } + } else { + view.removeAll(); // helper for cleaning the content when logging out + } + }, + + selectById: function(nodeid) { + let me = this; + me.down('pveResourceTree').selectById(nodeid); + }, + + onLogin: function(loginData) { + let me = this; + + me.updateUserInfo(); + + if (loginData) { + PVE.data.ResourceStore.startUpdate(); + + Proxmox.Utils.API2Request({ + url: '/version', + method: 'GET', + success: function(response) { + PVE.VersionInfo = response.result.data; + me.updateVersionInfo(); + }, + }); + + PVE.UIOptions.update(); + + Proxmox.Utils.API2Request({ + url: '/cluster/sdn', + method: 'GET', + success: function(response) { + PVE.SDNInfo = response.result.data; + }, + failure: function(response) { + PVE.SDNInfo = null; + let ui = Ext.ComponentQuery.query('treelistitem[text="SDN"]')[0]; + if (ui) { + ui.addCls('x-hidden-display'); + } + }, + }); + + Proxmox.Utils.API2Request({ + url: '/access/domains', + method: 'GET', + success: function(response) { + let [_username, realm] = Proxmox.Utils.parse_userid(Proxmox.UserName); + response.result.data.forEach((domain) => { + if (domain.realm === realm) { + let schema = PVE.Utils.authSchema[domain.type]; + if (schema) { + me.query('#tfaitem')[0].setHidden(!schema.tfa); + me.query('#passworditem')[0].setHidden(!schema.pwchange); + } + } + }); + }, + }); + } + }, + + updateUserInfo: function() { + let me = this; + let ui = me.query('#userinfo')[0]; + ui.setText(Ext.String.htmlEncode(Proxmox.UserName || '')); + ui.updateLayout(); + }, + + updateVersionInfo: function() { + let me = this; + + let ui = me.query('#versioninfo')[0]; + + if (PVE.VersionInfo) { + let version = PVE.VersionInfo.version; + ui.update('Virtual Environment ' + version); + } else { + ui.update('Virtual Environment'); + } + ui.updateLayout(); + }, + + initComponent: function() { + let me = this; + + Ext.History.init(); + + let appState = Ext.create('PVE.StateProvider'); + Ext.state.Manager.setProvider(appState); + + let selview = Ext.create('PVE.form.ViewSelector', { + flex: 1, + padding: '0 5 0 0', + }); + + let rtree = Ext.createWidget('pveResourceTree', { + viewFilter: selview.getViewFilter(), + flex: 1, + selModel: { + selType: 'treemodel', + listeners: { + selectionchange: function(sm, selected) { + if (selected.length <= 0) { + return; + } + let treeNode = selected[0]; + let treeTypeToClass = { + root: 'PVE.dc.Config', + node: 'PVE.node.Config', + qemu: 'PVE.qemu.Config', + lxc: 'pveLXCConfig', + storage: 'PVE.storage.Browser', + sdn: 'PVE.sdn.Browser', + pool: 'pvePoolConfig', + }; + PVE.curSelectedNode = treeNode; + me.setContent({ + xtype: treeTypeToClass[treeNode.data.type || 'root'] || 'pvePanelConfig', + showSearch: treeNode.data.id === 'root' || Ext.isDefined(treeNode.data.groupbyid), + pveSelNode: treeNode, + workspace: me, + viewFilter: selview.getViewFilter(), + }); + }, + }, + }, + }); + + selview.on('select', function(combo, records) { + if (records) { + let view = combo.getViewFilter(); + rtree.setViewFilter(view); + } + }); + + let caps = appState.get('GuiCap'); + + let createVM = Ext.createWidget('button', { + pack: 'end', + margin: '3 5 0 0', + baseCls: 'x-btn', + iconCls: 'fa fa-desktop', + text: gettext("Create VM"), + disabled: !caps.vms['VM.Allocate'], + handler: function() { + let wiz = Ext.create('PVE.qemu.CreateWizard', {}); + wiz.show(); + }, + }); + + let createCT = Ext.createWidget('button', { + pack: 'end', + margin: '3 5 0 0', + baseCls: 'x-btn', + iconCls: 'fa fa-cube', + text: gettext("Create CT"), + disabled: !caps.vms['VM.Allocate'], + handler: function() { + let wiz = Ext.create('PVE.lxc.CreateWizard', {}); + wiz.show(); + }, + }); + + appState.on('statechange', function(sp, key, value) { + if (key === 'GuiCap' && value) { + caps = value; + createVM.setDisabled(!caps.vms['VM.Allocate']); + createCT.setDisabled(!caps.vms['VM.Allocate']); + } + }); + + Ext.apply(me, { + layout: { type: 'border' }, + border: false, + items: [ + { + region: 'north', + title: gettext('Header'), // for ARIA + header: false, // avoid rendering the title + layout: { + type: 'hbox', + align: 'middle', + }, + baseCls: 'x-plain', + defaults: { + baseCls: 'x-plain', + }, + border: false, + margin: '2 0 2 5', + items: [ + { + xtype: 'proxmoxlogo', + }, + { + minWidth: 150, + id: 'versioninfo', + html: 'Virtual Environment', + style: { + 'font-size': '14px', + 'line-height': '18px', + }, + }, + { + xtype: 'pveGlobalSearchField', + tree: rtree, + }, + { + flex: 1, + }, + { + xtype: 'proxmoxHelpButton', + hidden: false, + baseCls: 'x-btn', + iconCls: 'fa fa-book x-btn-icon-el-default-toolbar-small ', + listenToGlobalEvent: false, + onlineHelp: 'pve_documentation_index', + text: gettext('Documentation'), + margin: '0 5 0 0', + }, + createVM, + createCT, + { + pack: 'end', + margin: '0 5 0 0', + id: 'userinfo', + xtype: 'button', + baseCls: 'x-btn', + style: { + // proxmox dark grey p light grey as border + backgroundColor: '#464d4d', + borderColor: '#ABBABA', + }, + iconCls: 'fa fa-user', + menu: [ + { + iconCls: 'fa fa-gear', + text: gettext('My Settings'), + handler: function() { + var win = Ext.create('PVE.window.Settings'); + win.show(); + }, + }, + { + text: gettext('Password'), + itemId: 'passworditem', + iconCls: 'fa fa-fw fa-key', + handler: function() { + var win = Ext.create('Proxmox.window.PasswordEdit', { + userid: Proxmox.UserName, + }); + win.show(); + }, + }, + { + text: 'TFA', + itemId: 'tfaitem', + iconCls: 'fa fa-fw fa-lock', + handler: function(btn, event, rec) { + Ext.state.Manager.getProvider().set('dctab', { value: 'tfa' }, true); + me.selectById('root'); + }, + }, + { + iconCls: 'fa fa-paint-brush', + text: gettext('Color Theme'), + handler: function() { + Ext.create('Proxmox.window.ThemeEditWindow') + .show(); + }, + }, + { + iconCls: 'fa fa-language', + text: gettext('Language'), + handler: function() { + Ext.create('Proxmox.window.LanguageEditWindow') + .show(); + }, + }, + '-', + { + iconCls: 'fa fa-fw fa-sign-out', + text: gettext("Logout"), + handler: function() { + PVE.data.ResourceStore.loadData([], false); + me.showLogin(); + me.setContent(null); + var rt = me.down('pveResourceTree'); + rt.setDatacenterText(undefined); + rt.clearTree(); + + // empty the stores of the StatusPanel child items + var statusPanels = Ext.ComponentQuery.query('pveStatusPanel grid'); + Ext.Array.forEach(statusPanels, function(comp) { + if (comp.getStore()) { + comp.getStore().loadData([], false); + } + }); + }, + }, + ], + }, + ], + }, + { + region: 'center', + stateful: true, + stateId: 'pvecenter', + minWidth: 100, + minHeight: 100, + id: 'content', + xtype: 'container', + layout: { type: 'card' }, + border: false, + margin: '0 5 0 0', + items: [], + }, + { + region: 'west', + stateful: true, + stateId: 'pvewest', + itemId: 'west', + xtype: 'container', + border: false, + layout: { type: 'vbox', align: 'stretch' }, + margin: '0 0 0 5', + split: true, + width: 300, + items: [ + { + xtype: 'container', + layout: 'hbox', + padding: '0 0 5 0', + items: [ + selview, + { + xtype: 'button', + cls: 'x-btn-default-toolbar-small', + iconCls: 'fa fa-fw fa-gear x-btn-icon-el-default-toolbar-small', + handler: () => { + Ext.create('PVE.window.TreeSettingsEdit', { + autoShow: true, + apiCallDone: () => PVE.UIOptions.fireUIConfigChanged(), + }); + }, + }, + ], + }, + rtree, + ], + listeners: { + resize: function(panel, width, height) { + var viewWidth = me.getSize().width; + if (width > viewWidth - 100) { + panel.setWidth(viewWidth - 100); + } + }, + }, + }, + { + xtype: 'pveStatusPanel', + stateful: true, + stateId: 'pvesouth', + itemId: 'south', + region: 'south', + margin: '0 5 5 5', + title: gettext('Logs'), + collapsible: true, + header: false, + height: 200, + split: true, + listeners: { + resize: function(panel, width, height) { + var viewHeight = me.getSize().height; + if (height > viewHeight - 150) { + panel.setHeight(viewHeight - 150); + } + }, + }, + }, + ], + }); + + me.callParent(); + + me.updateUserInfo(); + + // on resize, center all modal windows + Ext.on('resize', function() { + let modalWindows = Ext.ComponentQuery.query('window[modal]'); + if (modalWindows.length > 0) { + modalWindows.forEach(win => win.alignTo(me, 'c-c')); + } + }); + }, +}); +