Unverified Commit 0546fe27 authored by Francesc Guasch's avatar Francesc Guasch Committed by GitHub
Browse files

Feature: cpu features (#1752)

feat(KVM): features policy in CPU
parent d1fa73a6
......@@ -1305,6 +1305,9 @@ sub _insert_display( $self, $display ) {
$used_port->{$display->{port}}++;
$display->{port} = $self->_vm->_new_free_port($used_port);
}
} elsif ($field =~ /\.id_domain_driver/) {
warn "Warning: Already added ".Dumper($display);
return;
} else {
confess "Error: I don't know how to deal with duplicated $field on ".$self->name
.Dumper($display);
......@@ -5425,11 +5428,15 @@ sub _fix_hw_booleans($data) {
next if !ref($data->{$key});
if (ref($data->{$key}) eq 'HASH') {
_fix_hw_booleans($data->{$key});
} elsif(ref($data->{$key}) eq 'ARRAY') {
for my $item (@{$data->{$key}}) {
_fix_hw_booleans($item);
}
} elsif(ref($data->{$key}) eq 'JSON::PP::Boolean') {
$data->{$key} = ''.$data->{$key};
} else {
confess "Error: expecting scalar or hash or boolean "
.Dumper($data);
.Dumper(ref($data->{$key}) ,$data->{$key});
}
}
}
......
......@@ -2881,6 +2881,8 @@ sub _change_hardware_cpu($self, $index, $data) {
$vcpu->appendText($data->{vcpu}->{_text});
}
my ($cpu) = $doc->findnodes('/domain/cpu');
my $feature = delete $data->{cpu}->{feature};
for my $field (keys %{$data->{cpu}}) {
if (ref($data->{cpu}->{$field})) {
_change_xml($cpu, $field, $data->{cpu}->{$field});
......@@ -2894,6 +2896,10 @@ sub _change_hardware_cpu($self, $index, $data) {
$changed++;
}
}
if ( $feature ) {
_change_xml_list($cpu, 'feature', $feature, 'name');
$changed++;
}
$self->reload_config($doc) if $changed;
}
......@@ -2988,10 +2994,34 @@ sub _remove_acceleration($video) {
$video->removeChild($acceleration) if $acceleration;
}
sub _change_xml_list($xml,$name, $data, $field='name') {
my %keep;
for my $entry (@$data) {
next if !defined $entry ||!$entry;
$keep{$entry->{$field}}++;
my $node;
for my $curr ($xml->findnodes($name)) {
$node = $curr if $curr->getAttribute($field) eq $entry->{$field};
}
$node = $xml->addNewChild(undef, $name) if !$node;
for my $field (keys %$entry) {
next if $field eq '$$hashKey';
$node->setAttribute($field, $entry->{$field});
}
}
for my $curr ($xml->findnodes($name)) {
my $curr_name = $curr->getAttribute($field);
$xml->removeChild($curr) if !$keep{$curr_name};
}
}
sub _change_xml($xml, $name, $data) {
confess Dumper([$name, $data])
if !ref($data) || ( ref($data) ne 'HASH' && ref($data) ne 'ARRAY');
my ($node) = $xml->findnodes($name);
$node = $xml->addNewChild(undef,$name) if !$node;
confess Dumper([$name, $data]) if !ref($data) || ref($data) ne 'HASH';
my $text = delete $data->{_text};
if ($text) {
......
......@@ -124,6 +124,17 @@ sub _get_controller_cpu($self) {
my ($xml_vcpu) = $doc->findnodes("/domain/vcpu");
_xml_elements($xml_vcpu, $item->{vcpu});
if (exists $item->{cpu}->{feature} && ref($item->{cpu}->{feature}) ne 'ARRAY') {
$item->{cpu}->{feature} = [ $item->{cpu}->{feature} ];
}
$item->{cpu}->{feature} = []
if !exists $item->{cpu}->{feature};
$item->{cpu}->{feature}
= _sort_xml_list($item->{cpu}->{feature},'name');
lock_hash(%$item);
return ($item);
}
......@@ -152,6 +163,15 @@ sub _get_controller_features($self) {
return ($item);
}
sub _sort_xml_list($list, $field) {
my @sorted = sort {
my ($name_a) = ($a->{$field} or '');
my ($name_b) = ($b->{$field} or '');
$name_a cmp $name_b
}@$list;
return \@sorted;
}
sub _xml_elements($xml, $item) {
confess if !defined $xml;
......@@ -166,7 +186,17 @@ sub _xml_elements($xml, $item) {
my $h_node = {};
_xml_elements($node, $h_node);
$h_node = 1 if !keys %$h_node;
$item->{$node->nodeName} = $h_node;
my $name = $node->nodeName;
if (!exists $item->{$name}) {
$item->{$node->nodeName} = $h_node;
} else {
my $entry = $item->{$name};
if (ref($entry) eq 'HASH') {
$item->{$name} = [ $entry , $h_node ];
} else {
push @{$item->{$name}},($h_node);
}
}
}
}
......
......@@ -1710,6 +1710,20 @@ sub requirements_done($self) {
return $ok;
}
=head2 redo
Set the request to be executed again
=cut
sub redo($self) {
my $sth = $self->_dbh->prepare(
"UPDATE requests SET status='requested',start_time=NULL "
." WHERE id=?"
);
$sth->execute($self->id);
}
sub AUTOLOAD {
my $self = shift;
......
use warnings;
use strict;
use Carp qw(confess);
use Data::Dumper;
use IPC::Run3;
use JSON::XS;
use Test::More;
use XML::LibXML;
use lib 't/lib';
use Test::Ravada;
no warnings "experimental::signatures";
use feature qw(signatures);
=pod
"<feature policy='require' name='ss'/>"
<feature policy='require' name='vmx'/>
<feature policy='require' name='pcid'/>
<feature policy='require' name='hypervisor'/>
<feature policy='require' name='arat'/>
<feature policy='require' name='tsc_adjust'/>
<feature policy='require' name='umip'/>
<feature policy='require' name='md-clear'/>
<feature policy='require' name='stibp'/>
<feature policy='require' name='arch-capabilities'/>
<feature policy='require' name='ssbd'/>
<feature policy='require' name='xsaveopt'/>
<feature policy='require' name='pdpe1gb'/>
<feature policy='require' name='ibpb'/>
<feature policy='require' name='ibrs'/>
<feature policy='require' name='amd-stibp'/>
<feature policy='require' name='amd-ssbd'/>
<feature policy='require' name='skip-l1dfl-vmentry'/>
<feature policy='require' name='pschange-mc-no'/>
=cut
sub test_feat_policy($vm) {
my $domain = create_domain_v2(
vm => $vm
,id_iso => search_id_iso('alpine%64')
);
Ravada::Request->change_hardware(
uid => user_admin->id
,id_domain => $domain->id
,hardware => 'cpu'
,data => { cpu => { 'mode' => 'host-model', check => 'partial' } }
);
wait_request();
my $req = Ravada::Request->start_domain(
uid => user_admin->id
,id_domain => $domain->id
);
wait_request( check_error => 0);
test_add_feature_policy($domain, $req->error ) if $req->error;
$domain->remove(user_admin);
}
sub _verify_features($domain, @features) {
diag("Verify features @features");
my $xml = $domain->xml_description();
my $doc = XML::LibXML->load_xml(string => $xml);
my @found = $doc->findnodes("/domain/cpu/feature");
my %missing = map { $_ => } @features;
my %dupe;
for my $feat(@found) {
my $name = $feat->getAttribute('name');
delete $missing{$name};
$dupe{$name}++;
}
ok(!keys %missing) or die "Missing features ".Dumper([keys %missing]);
for my $feat ( keys %dupe ) {
delete $dupe{$feat} if $dupe{$feat}<2;
}
ok(!keys %dupe) or die "Duplicated features ".Dumper(\%dupe);
}
sub test_feature_policy($domain, $feature, $policy='require') {
my $req_change = Ravada::Request->change_hardware(
uid => user_admin->id
,id_domain => $domain->id
,hardware => 'cpu'
,data => { cpu => { 'mode' => 'host-model', check => 'partial'
,feature => [
{ 'policy' => $policy, name => $feature}
]
}
}
);
wait_request( debug => 0);
my $domain2 = Ravada::Front::Domain->open($domain->id);
my $xml = $domain2->xml_description();
my $doc = XML::LibXML->load_xml(string => $xml);
my @features = $doc->findnodes("/domain/cpu/feature");
my @found = grep { $_->getAttribute('name') eq $feature }
$doc->findnodes("/domain/cpu/feature");
is(scalar(@found),1);
is($found[0]->getAttribute('name'), $feature) or exit;
is($found[0]->getAttribute('policy'), $policy, "Expecting feature policy=$policy name=$feature ".$found[0]->toString()) or exit;
}
sub test_remove_feature($domain, $feature) {
my $cpu = $domain->get_controller( 'cpu' );
my $old = $cpu->{cpu}->{feature};
my @keep;
for (@$old) {
push @keep,($_) if $_->{name} ne $feature;
}
my $req_change = Ravada::Request->change_hardware(
uid => user_admin->id
,id_domain => $domain->id
,hardware => 'cpu'
,data => { cpu => { 'mode' => 'host-model', check => 'partial'
,feature => \@keep
}
}
);
wait_request();
my $xml = $domain->xml_description();
my $doc = XML::LibXML->load_xml(string => $xml);
my @found = grep { $_->getAttribute('name') eq $feature }
$doc->findnodes("/domain/cpu/feature");
is(scalar(@found),0) or die Dumper([map { $_->toString } @found]);
}
sub test_add_feature_policy($domain, $error) {
my ($features) = $error =~ /required features: (.*?)($|\s)/;
my @features = split /,/,$features;
my $req_change = Ravada::Request->change_hardware(
uid => user_admin->id
,id_domain => $domain->id
,hardware => 'cpu'
,data => { cpu => { 'mode' => 'host-model', check => 'partial'
,feature => [
{ 'policy' => 'disable', name => 'svm'}
,{ 'policy' => 'require', name => 'vmx'}
]
}
}
);
wait_request();
_verify_features($domain, 'svm', 'vmx');
my $cpu = $domain->get_controller( 'cpu' );
is(scalar(@{$cpu->{cpu}->{feature}}),2);
my $req = Ravada::Request->start_domain(
uid => user_admin->id
,id_domain => $domain->id
);
wait_request();
_verify_features($domain, 'svm', 'vmx');
$domain->shutdown_now(user_admin);
$req_change->redo();
wait_request(debug => 0);
_verify_features($domain, 'svm', 'vmx');
test_feature_policy($domain, 'vmx','require');
test_feature_policy($domain, 'vmx','disable');
test_remove_feature($domain, 'vmx');
}
########################################################################
init();
clean();
for my $vm_name ( 'KVM' ) {
SKIP: {
my $vm = rvd_back->search_vm($vm_name);
my $msg = "SKIPPED test: No $vm_name VM found ";
if ($vm && $>) {
$msg = "SKIPPED: Test must run as root";
$vm = undef;
}
diag($msg) if !$vm;
skip $msg,10 if !$vm;
test_feat_policy($vm);
}
}
end();
done_testing();
......@@ -45,4 +45,58 @@
</select>
</div>
<hr>
<small><b><%=l 'CPU Features' %></b></small>
<div class="card-body ml-2" ng-show=item.cpu.feature.length">
<ul class="list-group list-group-horizontal-md"
ng-show="feature"
ng-repeat="feature in item.cpu.feature">
<li class="list-group-item list-group-item-primary col-md-3">
{{feature.name}}
</li>
<li class="list-group-item">
<select ng-model="feature.policy"
ng-options="option for option in
[ 'disable','forbid', 'force', 'optional','require']">
</select>
</li>
<li class="list-group-item">
<small>
<button type="badge" title="remove"
ng-click="feature=0;
item.cpu.feature[$index]=undefined;
form_edit.$pristine=false"
>
<span aria-hidden="true">&times;</span>
</button>
</small>
</li>
</ul>
</div>
<div class="ml-4">
<div class="row">
<div class="col-md-6">
<label for="new_feature">
<%=l 'New feature' %>
</label>
<input type="text" ng-model="new_feature" name="new_feature"/>
<button type="badge"
ng-disabled="!new_feature"
ng-click="item.cpu.feature[item.cpu.feature.length]=
{'name': new_feature, 'policy': 'require'};
form_edit.$pristine=false;
new_feature='';
">
add
</button>
</div>
</div><!-- row -->
</div>
<hr>
</div>
Supports Markdown
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