Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a12961aa02 | ||
|
|
429ce91253 | ||
|
|
0bb7be3f7f | ||
|
|
e6a220f2b1 | ||
|
|
0085410b6c | ||
|
|
8a2a44a473 | ||
|
|
11ef65c947 | ||
|
|
f1c3f66086 | ||
|
|
b72e8c92b5 | ||
|
|
2564208ca8 |
No files matched your search
+2447
File diff suppressed because it is too large.
Load diff
+5
-1
@@ -5,6 +5,7 @@ use warnings;
|
||||
use JSON;
|
||||
|
||||
use PVE::Cluster qw(cfs_lock_file cfs_read_file cfs_register_file);
|
||||
use PVE::File;
|
||||
use PVE::Job::Registry;
|
||||
use PVE::Jobs::VZDump;
|
||||
use PVE::Jobs::RealmSync;
|
||||
@@ -116,7 +117,10 @@ my $get_job_task_status = sub {
|
||||
}
|
||||
|
||||
# the task log might be gone, e.g. after a crash, do not let that block the job forever
|
||||
return "task log not found" if !-f $filename;
|
||||
if (!PVE::File::file_exists($filename)) {
|
||||
warn "missing task log for $state->{upid}\n";
|
||||
return "task log not found";
|
||||
}
|
||||
|
||||
return PVE::Tools::upid_read_status($state->{upid});
|
||||
};
|
||||
|
||||
@@ -267,6 +267,11 @@ sub test_connection {
|
||||
}
|
||||
my $response = $ua->get($url);
|
||||
|
||||
# the InfluxDB v2 listener of Telegraf before 1.36 only has the ready endpoint
|
||||
if ($response->code == 404) {
|
||||
$response = $ua->get(_get_v2url($cfg, "ready"));
|
||||
}
|
||||
|
||||
if (!$response->is_success) {
|
||||
my $err = $response->status_line;
|
||||
die "$err\n";
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Contributions to proxmox-git/pve-manager
|
||||
|
||||
|
||||
|
||||
This repository holds only my commits to the upstream project, with their original author, date and diff.
|
||||
Each of my commits is preceded by a snapshot of the upstream files it touched, so every diff shows exactly what I changed.
|
||||
+11
-1
@@ -1148,6 +1148,16 @@ Ext.define('PVE.Utils', {
|
||||
return `${per.toFixed(1)}% of ${maxcpu} ${cpu_label}`;
|
||||
},
|
||||
|
||||
// parses a size in the 'disk-size' format of the API, like '32G', into bytes
|
||||
parse_disk_size: function (value) {
|
||||
let match = /^(\d+(?:\.\d+)?)(?:([KMGT])(?:iB)?)?$/.exec(value);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
let exponent = ['', 'K', 'M', 'G', 'T'].indexOf(match[2] ?? '');
|
||||
return parseFloat(match[1]) * 1024 ** exponent;
|
||||
},
|
||||
|
||||
render_bandwidth: function (value) {
|
||||
if (!Ext.isNumeric(value)) {
|
||||
return '';
|
||||
@@ -2093,7 +2103,7 @@ Ext.define('PVE.Utils', {
|
||||
return value.replace(/(\\\\|\\n)/g, (match) => replace[match]);
|
||||
},
|
||||
|
||||
notesTemplateVars: ['cluster', 'guestname', 'node', 'pool', 'vmid'],
|
||||
notesTemplateVars: ['cluster', 'guestname', 'node', 'vmid'],
|
||||
|
||||
renderTags: function (tagstext, overrides) {
|
||||
let text = '';
|
||||
|
||||
@@ -24,6 +24,17 @@ Ext.define('PVE.window.MPResize', {
|
||||
});
|
||||
},
|
||||
|
||||
updateNewSize: function () {
|
||||
let me = this;
|
||||
let newSize = '';
|
||||
if (me.currentSize !== undefined && me.hdsizesel.isValid()) {
|
||||
newSize = Proxmox.Utils.format_size(
|
||||
me.currentSize + me.hdsizesel.getValue() * 1024 ** 3,
|
||||
);
|
||||
}
|
||||
me.down('#newSize').setValue(newSize);
|
||||
},
|
||||
|
||||
initComponent: function () {
|
||||
var me = this;
|
||||
|
||||
@@ -44,18 +55,32 @@ Ext.define('PVE.window.MPResize', {
|
||||
vtype: 'StorageId',
|
||||
allowBlank: false,
|
||||
},
|
||||
{
|
||||
xtype: 'displayfield',
|
||||
itemId: 'currentSize',
|
||||
fieldLabel: gettext('Current Size'),
|
||||
},
|
||||
];
|
||||
|
||||
me.hdsizesel = Ext.createWidget('numberfield', {
|
||||
name: 'size',
|
||||
minValue: 0,
|
||||
maxValue: 128 * 1024,
|
||||
decimalPrecision: 3,
|
||||
value: '0',
|
||||
fieldLabel: `${gettext('Size Increment')} (${gettext('GiB')})`,
|
||||
allowBlank: false,
|
||||
listeners: {
|
||||
change: () => me.updateNewSize(),
|
||||
},
|
||||
});
|
||||
|
||||
items.push(me.hdsizesel);
|
||||
items.push({
|
||||
xtype: 'displayfield',
|
||||
itemId: 'newSize',
|
||||
fieldLabel: gettext('New Size'),
|
||||
});
|
||||
|
||||
me.formPanel = Ext.create('Ext.form.Panel', {
|
||||
bodyPadding: 10,
|
||||
@@ -91,5 +116,22 @@ Ext.define('PVE.window.MPResize', {
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
|
||||
Proxmox.Utils.API2Request({
|
||||
url: `/nodes/${me.nodename}/lxc/${me.vmid}/config`,
|
||||
method: 'GET',
|
||||
waitMsgTarget: me,
|
||||
failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
||||
success: function (response) {
|
||||
let size = PVE.Parser.parseLxcMountPoint(response.result.data[me.disk])?.size;
|
||||
me.currentSize = size ? PVE.Utils.parse_disk_size(size) : undefined;
|
||||
me.down('#currentSize').setValue(
|
||||
me.currentSize !== undefined
|
||||
? Proxmox.Utils.format_size(me.currentSize)
|
||||
: Proxmox.Utils.unknownText,
|
||||
);
|
||||
me.updateNewSize();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -25,6 +25,17 @@ Ext.define('PVE.window.HDResize', {
|
||||
});
|
||||
},
|
||||
|
||||
updateNewSize: function () {
|
||||
let me = this;
|
||||
let newSize = '';
|
||||
if (me.currentSize !== undefined && me.hdsizesel.isValid()) {
|
||||
newSize = Proxmox.Utils.format_size(
|
||||
me.currentSize + me.hdsizesel.getValue() * 1024 ** 3,
|
||||
);
|
||||
}
|
||||
me.down('#newSize').setValue(newSize);
|
||||
},
|
||||
|
||||
initComponent: function () {
|
||||
var me = this;
|
||||
|
||||
@@ -45,18 +56,32 @@ Ext.define('PVE.window.HDResize', {
|
||||
vtype: 'StorageId',
|
||||
allowBlank: false,
|
||||
},
|
||||
{
|
||||
xtype: 'displayfield',
|
||||
itemId: 'currentSize',
|
||||
fieldLabel: gettext('Current Size'),
|
||||
},
|
||||
];
|
||||
|
||||
me.hdsizesel = Ext.createWidget('numberfield', {
|
||||
name: 'size',
|
||||
minValue: 0,
|
||||
maxValue: 128 * 1024,
|
||||
decimalPrecision: 3,
|
||||
value: '0',
|
||||
fieldLabel: `${gettext('Size Increment')} (${gettext('GiB')})`,
|
||||
allowBlank: false,
|
||||
listeners: {
|
||||
change: () => me.updateNewSize(),
|
||||
},
|
||||
});
|
||||
|
||||
items.push(me.hdsizesel);
|
||||
items.push({
|
||||
xtype: 'displayfield',
|
||||
itemId: 'newSize',
|
||||
fieldLabel: gettext('New Size'),
|
||||
});
|
||||
|
||||
me.formPanel = Ext.create('Ext.form.Panel', {
|
||||
bodyPadding: 10,
|
||||
@@ -86,7 +111,6 @@ Ext.define('PVE.window.HDResize', {
|
||||
Ext.apply(me, {
|
||||
modal: true,
|
||||
width: 250,
|
||||
height: 150,
|
||||
border: false,
|
||||
layout: 'fit',
|
||||
buttons: [submitBtn],
|
||||
@@ -94,5 +118,22 @@ Ext.define('PVE.window.HDResize', {
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
|
||||
Proxmox.Utils.API2Request({
|
||||
url: `/nodes/${me.nodename}/qemu/${me.vmid}/config`,
|
||||
method: 'GET',
|
||||
waitMsgTarget: me,
|
||||
failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
||||
success: function (response) {
|
||||
let size = PVE.Parser.parseQemuDrive(me.disk, response.result.data[me.disk])?.size;
|
||||
me.currentSize = size ? PVE.Utils.parse_disk_size(size) : undefined;
|
||||
me.down('#currentSize').setValue(
|
||||
me.currentSize !== undefined
|
||||
? Proxmox.Utils.format_size(me.currentSize)
|
||||
: Proxmox.Utils.unknownText,
|
||||
);
|
||||
me.updateNewSize();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,751 @@
|
||||
Ext.define('PVE.window.Migrate', {
|
||||
extend: 'Ext.window.Window',
|
||||
|
||||
vmtype: undefined,
|
||||
nodename: undefined,
|
||||
vmid: undefined,
|
||||
vmname: 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,
|
||||
withConntrackState: true,
|
||||
bothHaveDbusVmstate: 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;
|
||||
}
|
||||
},
|
||||
conntrackStateCheckboxHidden: (get) =>
|
||||
!get('running') ||
|
||||
get('vmtype') !== 'qemu' ||
|
||||
!get('migration.bothHaveDbusVmstate'),
|
||||
},
|
||||
},
|
||||
|
||||
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);
|
||||
|
||||
let title = Ext.String.format(
|
||||
'{0} {1} {2}',
|
||||
gettext('Migrate'),
|
||||
vm.get(view.vmtype).commonName,
|
||||
PVE.Utils.getFormattedGuestIdentifier(view.vmid, view.vmname),
|
||||
);
|
||||
view.setTitle(title);
|
||||
|
||||
me.lookup('proxmoxHelpButton').setHelpConfig({
|
||||
onlineHelp: vm.get(view.vmtype).onlineHelp,
|
||||
});
|
||||
me.lookup('formPanel').isValid();
|
||||
},
|
||||
|
||||
onTargetChange: function (nodeSelector) {
|
||||
// Always display the storages of the currently selected migration target
|
||||
this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value);
|
||||
this.checkMigratePreconditions(true);
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (vm.get('migration.bothHaveDbusVmstate') && vm.get('migration.withConntrackState')) {
|
||||
params['with-conntrack-state'] = 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(Proxmox.Utils.errorText, 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: async 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);
|
||||
}
|
||||
|
||||
me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')];
|
||||
|
||||
if (vm.get('vmtype') === 'qemu') {
|
||||
await me.checkQemuPreconditions(resetMigrationPossible);
|
||||
} else {
|
||||
await me.checkLxcPreconditions(resetMigrationPossible);
|
||||
}
|
||||
|
||||
// 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;
|
||||
} catch (error) {
|
||||
if (error?.result?.status !== 501) {
|
||||
Ext.Msg.alert(Proxmox.Utils.errorText, error.htmlStatus);
|
||||
}
|
||||
me.fetchingNodeMigrateInfo = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const target = me.lookup('pveNodeSelector').value;
|
||||
let targetCapabilities = {};
|
||||
|
||||
try {
|
||||
const { result } = await Proxmox.Async.api2({
|
||||
url: `/nodes/${target}/capabilities/qemu/migration`,
|
||||
method: 'GET',
|
||||
});
|
||||
targetCapabilities = result.data;
|
||||
} catch (err) {
|
||||
// Only emit a warning in the case the target node does not (yet) support the
|
||||
// `capabilites/qemu/migration` endpoint and simply treat all features as unsupported.
|
||||
console.warn(`failed to query /capabilites/qemu/migration on '${target}':`, err);
|
||||
}
|
||||
|
||||
me.fetchingNodeMigrateInfo = false;
|
||||
|
||||
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 = [];
|
||||
let disallowed = migrateStats.not_allowed_nodes?.[target] ?? {};
|
||||
|
||||
if (migrateStats.allowed_nodes && !vm.get('running')) {
|
||||
migration.allowedNodes = migrateStats.allowed_nodes;
|
||||
if (target.length && !migrateStats.allowed_nodes.includes(target)) {
|
||||
if (disallowed.unavailable_storages !== undefined) {
|
||||
let missingStorages = disallowed.unavailable_storages.join(', ');
|
||||
const text = Ext.String.format(
|
||||
gettext(
|
||||
'Storage(s) ({0}) not available on selected target. Start VM to use live storage migration or select other target node.',
|
||||
),
|
||||
missingStorages,
|
||||
);
|
||||
|
||||
migration.possible = false;
|
||||
migration.preconditions.push({ text, severity: 'error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (disallowed['unavailable-resources'] !== undefined) {
|
||||
let unavailableResources = disallowed['unavailable-resources'].join(', ');
|
||||
const text = Ext.String.format(
|
||||
gettext('Mapped Resources ({0}) not available on selected target.'),
|
||||
unavailableResources,
|
||||
);
|
||||
|
||||
migration.possible = false;
|
||||
migration.preconditions.push({ text, severity: 'error' });
|
||||
}
|
||||
|
||||
let blockingResources = [];
|
||||
let mappedResources = migrateStats['mapped-resource-info'] ?? {};
|
||||
|
||||
for (const res of migrateStats.local_resources) {
|
||||
if (!mappedResources[res]) {
|
||||
blockingResources.push(res);
|
||||
}
|
||||
}
|
||||
|
||||
if (blockingResources.length) {
|
||||
migration.hasLocalResources = true;
|
||||
if (!migration.overwriteLocalResourceCheck || vm.get('running')) {
|
||||
const text = Ext.String.format(
|
||||
gettext('Cannot migrate VM with local resources: {0}'),
|
||||
blockingResources.join(', '),
|
||||
);
|
||||
|
||||
migration.possible = false;
|
||||
migration.preconditions.push({ text, severity: 'error' });
|
||||
} else {
|
||||
const text = Ext.String.format(
|
||||
gettext(
|
||||
'Migrating VM with local resources: {0}. This might fail if the resources are not available on the target node.',
|
||||
),
|
||||
blockingResources.join(', '),
|
||||
);
|
||||
|
||||
migration.preconditions.push({ text, severity: 'warning' });
|
||||
}
|
||||
}
|
||||
|
||||
if (vm.get('running')) {
|
||||
let allowed = [];
|
||||
let notAllowed = [];
|
||||
for (const [key, resource] of Object.entries(mappedResources)) {
|
||||
if (resource['live-migration']) {
|
||||
allowed.push(key);
|
||||
} else {
|
||||
notAllowed.push(key);
|
||||
}
|
||||
}
|
||||
if (notAllowed.length > 0) {
|
||||
const text = Ext.String.format(
|
||||
gettext('Cannot migrate running VM with mapped resources: {0}'),
|
||||
notAllowed.join(', '),
|
||||
);
|
||||
|
||||
migration.possible = false;
|
||||
migration.preconditions.push({ text, severity: 'error' });
|
||||
} else if (allowed.length > 0) {
|
||||
const text = Ext.String.format(
|
||||
gettext(
|
||||
'Live-migrating running VM with mapped resources (Experimental): {0}',
|
||||
),
|
||||
allowed.join(', '),
|
||||
);
|
||||
|
||||
migration.preconditions.push({ text, 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')) {
|
||||
let text = gettext('Cannot migrate VM with local CD/DVD');
|
||||
let snapshots = Object.keys(disk.referenced_in_snapshot ?? {}).sort();
|
||||
if (!disk.is_attached && snapshots.length) {
|
||||
text = Ext.String.format(
|
||||
ngettext(
|
||||
'Cannot migrate VM with local CD/DVD ({0}) referenced in snapshot {1}. Remove the snapshot to allow migration.',
|
||||
'Cannot migrate VM with local CD/DVD ({0}) referenced in snapshots {1}. Remove the snapshots to allow migration.',
|
||||
snapshots.length,
|
||||
),
|
||||
disk.volid,
|
||||
snapshots.join(', '),
|
||||
);
|
||||
}
|
||||
|
||||
migration.possible = false;
|
||||
migration.preconditions.push({ text, severity: 'error' });
|
||||
}
|
||||
} else {
|
||||
let size = disk.size
|
||||
? '(' + Proxmox.Utils.render_size(disk.size) + ')'
|
||||
: '';
|
||||
const text = Ext.String.format(
|
||||
gettext('Migration with local disk might take long: {0} {1}'),
|
||||
disk.volid,
|
||||
size,
|
||||
);
|
||||
|
||||
migration['with-local-disks'] = 1;
|
||||
migration.preconditions.push({ text, severity: 'warning' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
migration.bothHaveDbusVmstate =
|
||||
migrateStats['has-dbus-vmstate'] && targetCapabilities['has-dbus-vmstate'];
|
||||
if (vm.get('running')) {
|
||||
if (migration.withConntrackState && !migrateStats['has-dbus-vmstate']) {
|
||||
migration.preconditions.push({
|
||||
text: gettext(
|
||||
'Cannot migrate conntrack state, source node is lacking support.',
|
||||
),
|
||||
// user cannot really do anything about this, do not bother with scaring them!
|
||||
severity: 'info',
|
||||
});
|
||||
}
|
||||
if (migration.withConntrackState && !targetCapabilities['has-dbus-vmstate']) {
|
||||
migration.preconditions.push({
|
||||
text: gettext(
|
||||
'Cannot migrate conntrack state, target node is lacking support. Active network connections might get dropped.',
|
||||
),
|
||||
severity: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
if (migration.bothHaveDbusVmstate && !migration.withConntrackState) {
|
||||
migration.preconditions.push({
|
||||
text: gettext(
|
||||
'Conntrack state migration disabled. Active network connections might get dropped.',
|
||||
),
|
||||
severity: 'warning',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let blockingHAResources = disallowed['blocking-ha-resources'] ?? [];
|
||||
if (blockingHAResources.length) {
|
||||
migration.possible = false;
|
||||
|
||||
for (const { sid, cause } of blockingHAResources) {
|
||||
let reasonText;
|
||||
if (cause === 'resource-affinity') {
|
||||
reasonText = Ext.String.format(
|
||||
gettext(
|
||||
'HA resource {0} with negative affinity to VM on selected target node',
|
||||
),
|
||||
sid,
|
||||
);
|
||||
} else if (cause === 'node-affinity') {
|
||||
reasonText = Ext.String.format(
|
||||
gettext('HA resource {0} is not allowed on the selected target node'),
|
||||
sid,
|
||||
);
|
||||
} else {
|
||||
reasonText = Ext.String.format(
|
||||
gettext('blocking HA resource {0} on selected target node'),
|
||||
sid,
|
||||
);
|
||||
}
|
||||
|
||||
migration.preconditions.push({
|
||||
severity: 'error',
|
||||
text: Ext.String.format(
|
||||
gettext('Cannot migrate VM, because {0}.'),
|
||||
reasonText,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let dependentHAResources = migrateStats['dependent-ha-resources'];
|
||||
if (dependentHAResources !== undefined) {
|
||||
for (const sid of dependentHAResources) {
|
||||
const text = Ext.String.format(
|
||||
gettext(
|
||||
'HA resource {0} with positive affinity to VM is also migrated to selected target node.',
|
||||
),
|
||||
sid,
|
||||
);
|
||||
|
||||
migration.preconditions.push({ text, severity: 'warning' });
|
||||
}
|
||||
}
|
||||
|
||||
vm.set('migration', migration);
|
||||
},
|
||||
checkLxcPreconditions: async function (resetMigrationPossible) {
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
let migrateStats;
|
||||
|
||||
if (vm.get('running')) {
|
||||
vm.set('migration.mode', 'restart');
|
||||
}
|
||||
|
||||
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) {
|
||||
if (error?.result?.status !== 501) {
|
||||
Ext.Msg.alert(Proxmox.Utils.errorText, error.htmlStatus);
|
||||
}
|
||||
me.fetchingNodeMigrateInfo = false;
|
||||
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 = [];
|
||||
let targetNode = me.lookup('pveNodeSelector').value;
|
||||
let disallowed = migrateStats['not-allowed-nodes']?.[targetNode] ?? {};
|
||||
|
||||
let blockingHAResources = disallowed['blocking-ha-resources'] ?? [];
|
||||
if (blockingHAResources.length) {
|
||||
migration.possible = false;
|
||||
|
||||
for (const { sid, cause } of blockingHAResources) {
|
||||
let reasonText;
|
||||
if (cause === 'resource-affinity') {
|
||||
reasonText = Ext.String.format(
|
||||
gettext(
|
||||
'HA resource {0} with negative affinity to container on selected target node',
|
||||
),
|
||||
sid,
|
||||
);
|
||||
} else if (cause === 'node-affinity') {
|
||||
reasonText = Ext.String.format(
|
||||
gettext('HA resource {0} is not allowed on the selected target node'),
|
||||
sid,
|
||||
);
|
||||
} else {
|
||||
reasonText = Ext.String.format(
|
||||
gettext('blocking HA resource {0} on selected target node'),
|
||||
sid,
|
||||
);
|
||||
}
|
||||
|
||||
migration.preconditions.push({
|
||||
severity: 'error',
|
||||
text: Ext.String.format(
|
||||
gettext('Cannot migrate container, because {0}.'),
|
||||
reasonText,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let dependentHAResources = migrateStats['dependent-ha-resources'];
|
||||
if (dependentHAResources !== undefined) {
|
||||
for (const sid of dependentHAResources) {
|
||||
const text = Ext.String.format(
|
||||
gettext(
|
||||
'HA resource {0} with positive affinity to container is also migrated to selected target node.',
|
||||
),
|
||||
sid,
|
||||
);
|
||||
|
||||
migration.preconditions.push({ text, severity: 'warning' });
|
||||
}
|
||||
}
|
||||
|
||||
vm.set('migration', migration);
|
||||
},
|
||||
},
|
||||
|
||||
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': gettext('Overwrite local resources unavailable check'),
|
||||
},
|
||||
bind: {
|
||||
hidden: '{setLocalResourceCheckboxHidden}',
|
||||
value: '{migration.overwriteLocalResourceCheck}',
|
||||
},
|
||||
listeners: {
|
||||
change: {
|
||||
fn: 'checkMigratePreconditions',
|
||||
extraArg: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxcheckbox',
|
||||
name: 'withConntrackState',
|
||||
// TRANSLATORS: See https://www.kernel.org/doc/html/next/networking/netlink_spec/conntrack.html
|
||||
fieldLabel: gettext('Conntrack state'),
|
||||
autoEl: {
|
||||
tag: 'div',
|
||||
'data-qtip': gettext(
|
||||
'Enables live migration of conntrack entries for this VM.',
|
||||
),
|
||||
},
|
||||
bind: {
|
||||
hidden: '{conntrackStateCheckboxHidden}',
|
||||
value: '{migration.withConntrackState}',
|
||||
},
|
||||
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 '<i class="fa fa-exclamation-triangle warning"></i> ';
|
||||
case 'error':
|
||||
return '<i class="fa fa-times critical"></i>';
|
||||
case 'info':
|
||||
return '<i class="fa fa-info-circle info-blue"></i>';
|
||||
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}',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in new issue
Block a user