Commit b6c4b9ca authored by Dietmar Maurer's avatar Dietmar Maurer

start adding HA Config

parent f9e2a4c2
......@@ -5,9 +5,11 @@ use warnings;
use PVE::SafeSyslog;
use PVE::Tools qw(extract_param);
use PVE::INotify;
use PVE::Cluster qw(cfs_register_file cfs_lock_file cfs_read_file cfs_write_file);
use PVE::Storage;
use PVE::API2::Backup;
use PVE::API2::HAConfig;
use JSON;
use PVE::RESTHandler;
use PVE::RPCEnvironment;
......@@ -19,6 +21,11 @@ __PACKAGE__->register_method ({
path => 'backup',
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::HAConfig",
path => 'ha',
});
my $dc_schema = PVE::Cluster::get_datacenter_schema();
my $dc_properties = {
delete => {
......@@ -58,6 +65,7 @@ __PACKAGE__->register_method ({
{ name => 'resources' },
{ name => 'tasks' },
{ name => 'backup' },
{ name => 'ha' },
];
return $result;
......
package PVE::API2::HAConfig;
use strict;
use warnings;
use PVE::SafeSyslog;
use PVE::Tools;
use PVE::Cluster qw(cfs_lock_file cfs_read_file cfs_write_file);
use PVE::RESTHandler;
use PVE::RPCEnvironment;
use PVE::JSONSchema qw(get_standard_option);
use PVE::Exception qw(raise_param_exc);
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
description => "Directory index.",
permissions => {
path => '/',
privs => [ 'Sys.Audit' ],
},
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
id => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{id}" } ],
},
code => sub {
my ($param) = @_;
my $res = [
{ id => 'config' },
{ id => 'changes' },
{ id => 'groups' },
];
return $res;
}});
my $load_cluster_conf = sub {
my $oldconf;
my $newconf;
my $code = sub {
$oldconf = PVE::Cluster::cfs_read_file('cluster.conf');
$newconf = PVE::Cluster::cfs_read_file('cluster.conf.new');
};
cfs_lock_file('cluster.conf', undef, $code);
die $@ if $@;
if (!$newconf->{children}) {
return wantarray ? ($oldconf, undef) : $oldconf;
}
return $newconf if !wantarray;
# test if there is different content
my $oldstr = PVE::Cluster::write_cluster_conf("fake.cfg", $oldconf);
my $newstr = PVE::Cluster::write_cluster_conf("fake.cfg", $newconf);
return ($oldconf, undef) if $oldstr eq $newstr; # same content
# comput diff to display on GUI
my $oldfn = '/etc/pve/cluster.conf';
my $newfn = '/etc/pve/cluster.conf.new';
my $diff = PVE::INotify::ccache_compute_diff($oldfn, $newfn);
return ($newconf, $diff);
};
__PACKAGE__->register_method({
name => 'get_config',
path => 'config',
method => 'GET',
description => "Read cluster configuartion (cluster.conf). If you have any uncommitted changes in cluster.conf.new that content is returned instead.",
protected => 1,
permissions => {
path => '/',
privs => [ 'Sys.Audit' ],
},
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => "object",
properties => {},
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my ($conf, $diff) = &$load_cluster_conf();
$rpcenv->set_result_attrib('changes', $diff);
return $conf;
}});
__PACKAGE__->register_method({
name => 'get_changes',
path => 'changes',
method => 'GET',
description => "Get pending changes (unified diff between cluster.conf and cluster.conf.new",
protected => 1,
permissions => {
path => '/',
privs => [ 'Sys.Audit' ],
},
parameters => {
additionalProperties => 0,
properties => {},
},
returns => { type => "string", optional => 1 },
code => sub {
my ($param) = @_;
my ($conf, $diff) = &$load_cluster_conf();
return $diff;
}});
__PACKAGE__->register_method({
name => 'revert_changes',
path => 'changes',
method => 'DELETE',
description => "Revert pending changes (remove cluster.conf.new)",
protected => 1,
permissions => {
path => '/',
privs => [ 'Sys.Modify' ],
},
parameters => {
additionalProperties => 0,
properties => {},
},
returns => { type => "null" },
code => sub {
my ($param) = @_;
if (!unlink("/etc/pve/cluster.conf.new")) {
die "unlink failed - $!\n";
}
return;
}});
__PACKAGE__->register_method({
name => 'commit_config',
path => 'changes',
method => 'POST',
description => "Commit cluster configuartion. Pending changes from cluster.conf.new are written to cluster.conf. This triggers a CMan reload on all nodes.",
protected => 1,
permissions => {
path => '/',
privs => [ 'Sys.Modify' ],
},
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => "object",
properties => {},
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
die "not implemented";
}});
my $read_cluster_conf_new = sub {
my $conf = PVE::Cluster::cfs_read_file('cluster.conf.new');
if (!$conf->{children}) {
$conf = PVE::Cluster::cfs_read_file('cluster.conf');
}
return $conf;
};
my $update_cluster_conf_new = sub {
my ($conf) = @_;
$conf->{children}->[0]->{config_version}++;
cfs_write_file('cluster.conf.new', $conf);
};
my $lookup_cluster_sec = sub {
my ($conf) = @_;
die "no cluster defined\n" if !PVE::Cluster::cluster_conf_version($conf);
my $cluster = $conf->{children}->[0];
die "invalid root node\n" if $cluster->{text} ne 'cluster';
return $cluster;
};
my $lookup_rm_sec = sub {
my ($conf, $create, $noerr) = @_;
my $cluster = &$lookup_cluster_sec($conf);
my $rmsec;
foreach my $child (@{$cluster->{children}}) {
if ($child->{text} eq 'rm') {
$rmsec = $child;
}
}
if (!$rmsec) {
if (!$create) {
return undef if $noerr;
die "no resource manager section\n";
}
$rmsec = { text => 'rm' };
push @{$cluster->{children}}, $rmsec;
}
return $rmsec;
};
my $lookup_pvevm = sub {
my ($conf, $create, $vmid) = @_;
my $rmsec = &$lookup_rm_sec($conf, $create);
my $vmref;
foreach my $child (@{$rmsec->{children}}) {
if ($child->{text} eq 'pvevm' && $child->{vmid} eq $vmid) {
$vmref = $child;
}
}
return $vmref if !$create;
if (!$vmref) {
$vmref = { text => 'pvevm', vmid => $vmid };
push @{$rmsec->{children}}, $vmref;
} else {
die "resource group 'pvevm:$vmid' already exists\n";
}
return $vmref;
};
__PACKAGE__->register_method({
name => 'list_groups',
path => 'groups',
method => 'GET',
description => "List resource groups.",
protected => 1,
permissions => {
path => '/',
privs => [ 'Sys.Audit' ],
},
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{id}" } ],
},
code => sub {
my ($param) = @_;
my $conf = &$read_cluster_conf_new();
my $res = [];
my $rmsec = &$lookup_rm_sec($conf, 0, 1);
return $res if !$rmsec;
foreach my $child (@{$rmsec->{children}}) {
if ($child->{text} eq 'pvevm') {
push @$res, { id => "$child->{text}:$child->{vmid}" };
} elsif ($child->{text} eq 'service') {
push @$res, { id => "$child->{text}:$child->{name}" };
}
}
return $res;
}});
__PACKAGE__->register_method({
name => 'create_group',
path => 'groups',
method => 'POST',
description => "Create a new resource groups.",
protected => 1,
permissions => {
path => '/',
privs => [ 'Sys.Modify' ],
},
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid'),
autostart => {
optional => 1,
type => 'boolean',
description => "Service is started when a quorum forms.",
}
},
},
returns => { type => "null" },
code => sub {
my ($param) = @_;
my $vmlist = PVE::Cluster::get_vmlist();
raise_param_exc({ id => "no such vmid '$param->{vmid}'"})
if !($vmlist && $vmlist->{ids} && $vmlist->{ids}->{$param->{vmid}});
my $code = sub {
my $conf = &$read_cluster_conf_new();
my $pvevm = &$lookup_pvevm($conf, 1, $param->{vmid});
$pvevm->{autostart} = $param->{autostart} ? 1 : 0;
&$update_cluster_conf_new($conf);
};
cfs_lock_file('cluster.conf', undef, $code);
die $@ if $@;
return;
}});
__PACKAGE__->register_method({
name => 'update_group',
path => 'groups/{id}',
method => 'PUT',
description => "Update resource groups settings.",
protected => 1,
permissions => {
path => '/',
privs => [ 'Sys.Modify' ],
},
parameters => {
additionalProperties => 0,
properties => {
id => {
type => 'string',
description => "The resource group ID (for example 'pvevm:200').",
},
autostart => {
optional => 1,
type => 'boolean',
description => "Service is started when a quorum forms.",
}
},
},
returns => { type => "null" },
code => sub {
my ($param) = @_;
my $vmid;
if ($param->{id} =~ m/^pvevm:(\d+)$/) {
$vmid = int($1);
} else {
raise_param_exc({ id => "unsupported group type '$param->{id}'"});
}
my $code = sub {
my $conf = &$read_cluster_conf_new();
my $pvevm = &$lookup_pvevm($conf, 0, $vmid);
$pvevm->{autostart} = $param->{autostart} ? 1 : 0;
&$update_cluster_conf_new($conf);
};
cfs_lock_file('cluster.conf', undef, $code);
die $@ if $@;
return;
}});
__PACKAGE__->register_method({
name => 'read_group',
path => 'groups/{id}',
method => 'GET',
description => "List resource groups.",
protected => 1,
permissions => {
path => '/',
privs => [ 'Sys.Audit' ],
},
parameters => {
additionalProperties => 0,
properties => {
id => {
type => 'string',
description => "The resource group ID (for example 'pvevm:200').",
}
},
},
returns => {
type => "object",
properties => {},
},
code => sub {
my ($param) = @_;
my $conf = &$read_cluster_conf_new();
if (my $rmsec = &$lookup_rm_sec($conf, 0, 1)) {
foreach my $child (@{$rmsec->{children}}) {
if ($child->{text} eq 'pvevm') {
my $id = "$child->{text}:$child->{vmid}";
if ($id eq $param->{id}) {
$child->{id} = $id;
return $child;
}
} elsif ($child->{text} eq 'service') {
my $id = "$child->{text}:$child->{name}";
if ($id eq $param->{id}) {
$child->{id} = $id;
return $child;
}
}
}
}
raise_param_exc({ id => "no such group"});
}});
__PACKAGE__->register_method({
name => 'delete_group',
path => 'groups/{id}',
method => 'DELETE',
description => "Delete resource group.",
protected => 1,
permissions => {
path => '/',
privs => [ 'Sys.Modify' ],
},
parameters => {
additionalProperties => 0,
properties => {
id => {
type => 'string',
description => "The resource group ID (for example 'pvevm:200').",
}
},
},
returns => { type => "null" },
code => sub {
my ($param) = @_;
my $code = sub {
my $conf = &$read_cluster_conf_new();
my $found;
if (my $rmsec = &$lookup_rm_sec($conf, 0, 1)) {
my $oldlist = $rmsec->{children};
$rmsec->{children} = [];
foreach my $child (@$oldlist) {
if ($child->{text} eq 'pvevm') {
if ("$child->{text}:$child->{vmid}" eq $param->{id}) {
$found = 1;
next;
}
} elsif ($child->{text} eq 'service') {
if ("$child->{text}:$child->{name}" eq $param->{id}) {
$found = 1;
next;
}
}
push @{$rmsec->{children}}, $child;
}
}
raise_param_exc({ id => "no such group"}) if !$found;
&$update_cluster_conf_new($conf);
};
cfs_lock_file('cluster.conf', undef, $code);
die $@ if $@;
return;
}});
1;
......@@ -4,6 +4,7 @@ PERLSOURCE = \
VZDump.pm \
Backup.pm \
Cluster.pm \
HAConfig.pm \
Nodes.pm \
Tasks.pm \
Network.pm \
......
......@@ -121,6 +121,7 @@ JSSRC= \
dc/AuthView.js \
dc/AuthEdit.js \
dc/Backup.js \
dc/HAConfig.js \
dc/Config.js \
Workspace.js
......
......@@ -48,6 +48,11 @@ Ext.define('PVE.dc.Config', {
xtype: 'pveAuthView',
title: gettext('Authentication'),
itemId: 'domains'
},
{
xtype: 'pveDcHAConfig',
title: 'HA',
itemId: 'ha'
}
]
});
......
Ext.define('PVE.dc.vmHAServiceEdit', {
extend: 'PVE.window.Edit',
initComponent : function() {
var me = this;
me.create = me.vmid ? false : true;
if (me.vmid) {
me.create = false;
me.url = "/cluster/ha/groups/pvevm:" + me.vmid;
me.method = 'PUT';
} else {
me.create = true;
me.url = "/cluster/ha/groups";
me.method = 'POST';
}
Ext.apply(me, {
title: gettext('HA managed VM/CT'),
width: 350,
items: [
{
xtype: me.create ? 'pveVMIDSelector' : 'displayfield',
name: 'vmid',
validateExists: true,
value: me.vmid || '',
fieldLabel: "VM ID"
},
{
xtype: 'pvecheckbox',
name: 'autostart',
checked: true,
fieldLabel: 'autostart'
}
]
});
me.callParent();
if (!me.create) {
me.load()
}
}
});
Ext.define('PVE.dc.HAConfig', {
extend: 'Ext.panel.Panel',
alias: 'widget.pveDcHAConfig',
clusterInfo: {}, // reload store data here
reload: function() {
var me = this;
var getClusterInfo = function(conf) {
var info = {};
if (!(conf && conf.children && conf.children[0])) {
return info;
}
var cluster = conf.children[0];
if (cluster.text !== 'cluster' || !cluster.config_version) {
return info;
}
info.version = cluster.config_version;
Ext.Array.each(cluster.children, function(item) {
if (item.text === 'fencedevices') {
// fixme: make sure each node uses at least one fence device
info.fenceDevices = true;
} else if (item.text === 'rm') {
info.ha = true;
}
});
return info;
};
PVE.Utils.API2Request({
url: '/cluster/ha/config',
waitMsgTarget: me,
method: 'GET',
failure: function(response, opts) {
me.clusterInfo = {};
me.setLoading(response.htmlStatus);
},
success: function(response, opts) {
me.clusterInfo = getClusterInfo(response.result.data);
console.dir(me.clusterInfo);
me.setDisabled(!me.clusterInfo.version);
me.addMenu.setDisabled(!me.clusterInfo.version);
// note: this modifies response.result.data
me.treePanel.setRootNode(response.result.data);
me.treePanel.expandAll();
if (response.result.changes) {
me.commitBtn.setDisabled(false);
me.revertBtn.setDisabled(false);
me.diffPanel.setVisible(true);
me.diffPanel.update("<pre>" + Ext.htmlEncode(response.result.changes) + "</pre>");
} else {
me.commitBtn.setDisabled(true);
me.revertBtn.setDisabled(true);
me.diffPanel.setVisible(false);
me.diffPanel.update('');
}
}
});
},
initComponent: function() {
var me = this;
me.commitBtn = new PVE.button.Button({
text: gettext('Commit'),
disabled: true,
confirmMsg: function () {
return gettext('Are you sure you want to commit your changes');
},
handler: function(btn, event) {
PVE.Utils.API2Request({
url: '/cluster/ha/changes',
method: 'POST',
waitMsgTarget: me,
callback: function() {
me.reload();
},
failure: function (response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}
});
}
});
me.revertBtn = new PVE.button.Button({
text: gettext('Revert changes'),
disabled: true,
confirmMsg: function () {
return gettext('Are you sure you want to revert (undo) your changes');
},
handler: function(btn, event) {
PVE.Utils.API2Request({
url: '/cluster/ha/changes',
method: 'DELETE',
waitMsgTarget: me,
callback: function() {
me.reload();
},
failure: function (response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}
});
}
});
me.addMenu = new Ext.button.Button({
text: gettext('Add'),
disabled: true,
menu: new Ext.menu.Menu({
items: [
{
text: gettext('HA managed VM/CT'),
handler: function() {
if (!me.clusterInfo.fenceDevices) {
Ext.Msg.alert(gettext('Error'), gettext("Please configure fencing first!"));
return;
}
var win = Ext.create('PVE.dc.vmHAServiceEdit', {});
win.show();
win.on('destroy', me.reload, me);
}
},
{
text: gettext('Failover Domain'),
handler: function() {
Ext.Msg.alert(gettext('Error'), "not implemented - sorry");
}
}
]
})
});
me.treePanel = Ext.create('Ext.tree.Panel', {
rootVisible: false,
animate: false,
region: 'center',
border: false,
fields: ['text', 'id', 'vmid', 'name' ],
columns: [
{
xtype: 'treecolumn',
text: 'Tag',
dataIndex: 'text',
width: 200
},
{
text: 'Attributes',
dataIndex: 'id',
renderer: function(value, metaData, record) {
var text = '';
Ext.Object.each(record.raw, function(key, value) {
if (key === 'id' || key === 'text') {
return;
}
text += Ext.htmlEncode(key) + '="' +
Ext.htmlEncode(value) + '" ';
});
return text;
},
flex: 1
}
]
});
var run_editor = function() {
var rec = me.treePanel.selModel.getSelection()[0];
if (rec && rec.data.text === 'pvevm') {
var win = Ext.create('PVE.dc.vmHAServiceEdit', {
vmid: rec.data.vmid
});
win.show();
win.on('destroy', me.reload, me);
}
};
me.editBtn = new Ext.button.Button({
text: gettext('Edit'),
disabled: true,
handler: run_editor
});
me.removeBtn = new Ext.button.Button({
text: gettext('Remove'),
disabled: true,
handler: function() {
var rec = me.treePanel.selModel.getSelection()[0];
if (rec && rec.data.text === 'pvevm') {
var groupid = 'pvevm:' + rec.data.vmid;
PVE.Utils.API2Request({
url: '/cluster/ha/groups/' + groupid,
method: 'DELETE',
waitMsgTarget: me,
callback: function() {
me.reload();
},
failure: function (response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}
});
}
}
});
me.diffPanel = Ext.create('Ext.panel.Panel', {
border: false,
hidden: true,
region: 'south',
autoScroll: true,
itemId: 'changes',
tbar: [ gettext('Pending changes') ],
split: true,
bodyPadding: 5,
flex: 0.6
});
Ext.apply(me, {
layout: 'border',
tbar: [ me.addMenu, me.removeBtn, me.editBtn, me.revertBtn, me.commitBtn ],
items: [ me.treePanel, me.diffPanel ]
});
me.callParent();
me.on('show', me.reload);
me.treePanel.on("selectionchange", function(sm, selected) {
var rec = selected[0];
if (rec && rec.data.text === 'pvevm') {
me.editBtn.setDisabled(false);
me.removeBtn.setDisabled(false);
} else {
me.editBtn.setDisabled(true);
me.removeBtn.setDisabled(true);
}
});
me.treePanel.on("itemdblclick", function(v, record) {
run_editor()
});
}
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment