mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
fix(instances): the VM page offers only the controls the user may use
The page showed controls the backend then refused: power to global viewers, the console, virt-viewer and VDI controls and the root password and SSH key forms to viewers, resize to staff, per-vCPU hotplug, video model and guest agent to non-superusers, clone and options by the clone permission alone, snapshots without change permission. The page now asks the views' own rules (owner, is_change, clone and snapshot permissions, the template rules). OS info, opened from the header, is there for every viewer; the XML editor and tab restoring after a redirect no longer throw when a pane is absent; Settings opens on the first section the user has. The authorization matrix also covers a template VM, the template clone rule and the template snapshot rule.
This commit is contained in:
parent
443b31d86b
commit
bf1f122984
8 changed files with 294 additions and 71 deletions
|
|
@ -85,7 +85,7 @@
|
|||
{% trans "Resize" %}
|
||||
</button>
|
||||
</li>
|
||||
{% if allow_admin_or_not_template and 'instances.snapshot_instances' in perms %}
|
||||
{% if can_snapshot %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link action-button" id="snapshots-tab" aria-controls="snapshots" data-bs-toggle="pill" data-bs-target="#snapshots" type="button" role="tab" aria-selected="false">
|
||||
<span id="action-block" aria-hidden="true">{% bs_icon 'camera' size='2em'%} </span>
|
||||
|
|
@ -93,12 +93,14 @@
|
|||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if show_settings %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link action-button" id="settings-tab" aria-controls="settings" data-bs-toggle="pill" data-bs-target="#settings" type="button" role="tab" aria-selected="false">
|
||||
<span id="action-block" aria-hidden="true">{% bs_icon 'gear-wide-connected' size='2em'%} </span>
|
||||
{% trans "Settings" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link action-button" id="graphics-tab" aria-controls="graphics" data-bs-toggle="pill" data-bs-target="#graphics" type="button" role="tab" aria-selected="false">
|
||||
<span id="action-block" aria-hidden="true">{% bs_icon 'bar-chart-line' size='2em'%} </span>
|
||||
|
|
@ -117,8 +119,8 @@
|
|||
{% include 'instances/power_tab.html' %}
|
||||
{% include 'instances/access_tab.html' %}
|
||||
{% include 'instances/resize_tab.html' %}
|
||||
{% include 'instances/snapshots_tab.html' %}
|
||||
{% include 'instances/settings_tab.html' %}
|
||||
{% if can_snapshot %}{% include 'instances/snapshots_tab.html' %}{% endif %}
|
||||
{% if show_settings %}{% include 'instances/settings_tab.html' %}{% endif %}
|
||||
{% include 'instances/stats_tab.html' %}
|
||||
{% include 'instances/destroy_tab.html' %}
|
||||
</div>
|
||||
|
|
@ -156,13 +158,16 @@
|
|||
}
|
||||
</script>
|
||||
<script>
|
||||
var editor = ace.edit("editor");
|
||||
editor.getSession().setMode("ace/mode/xml");
|
||||
// The XML editor exists only for superusers
|
||||
if (document.getElementById("editor")) {
|
||||
var editor = ace.edit("editor");
|
||||
editor.getSession().setMode("ace/mode/xml");
|
||||
|
||||
var input = $('input[name="inst_xml"]');
|
||||
var input = $('input[name="inst_xml"]');
|
||||
editor.getSession().on("change", function () {
|
||||
input.val(editor.getSession().getValue());
|
||||
});
|
||||
input.val(editor.getSession().getValue());
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function random_mac(net) {
|
||||
|
|
@ -269,6 +274,7 @@
|
|||
$("#video_model_select option[value='" + video_model + "']").prop('selected', true);
|
||||
}
|
||||
});
|
||||
{% if can_open_console %}
|
||||
$(document).ready(function () {
|
||||
// set vdi url
|
||||
$.get("{% url 'vdi_url' compute.id instance.name %}", function(data) {
|
||||
|
|
@ -276,6 +282,7 @@
|
|||
$("#vdi_url").attr("href", data);
|
||||
});
|
||||
});
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser %}
|
||||
$(document).ready(function () {
|
||||
random_mac('clone-net-mac-0');
|
||||
|
|
@ -669,58 +676,41 @@
|
|||
});
|
||||
</script>
|
||||
<script>
|
||||
// Open the tab (and sub-tab) a redirect points to with #<pane id>
|
||||
function show_tab(target) {
|
||||
var button = document.querySelector('button[data-bs-target="' + target + '"]');
|
||||
if (button) {
|
||||
bootstrap.Tab.getOrCreateInstance(button).show();
|
||||
}
|
||||
}
|
||||
var hash = location.hash;
|
||||
if (~$.inArray(hash, ['#poweron', '#poweroff', '#powercycle', '#suspend', '#resume'])) {
|
||||
var btnsect = $('#navbtn>li>a');
|
||||
$(btnsect).each(function () {
|
||||
if ($(this).attr('href') === '#power') {
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
var parents = {
|
||||
'#poweron': '#power', '#poweroff': '#power', '#powercycle': '#power', '#suspend': '#power', '#resume': '#power',
|
||||
'#resizevm_cpu': '#resize', '#resizevm_mem': '#resize', '#resizevm_disk': '#resize',
|
||||
'#boot_opt': '#settings', '#disks': '#settings', '#network': '#settings', '#clone': '#settings',
|
||||
'#xmledit': '#settings', '#vncsettings': '#settings', '#migrate': '#settings', '#options': '#settings',
|
||||
'#users': '#settings',
|
||||
'#takesnapshot': '#snapshots', '#takeextsnapshot': '#snapshots', '#managesnapshot': '#snapshots'
|
||||
};
|
||||
if (hash === '#resize') {
|
||||
show_tab('#resize');
|
||||
} else if (parents[hash]) {
|
||||
show_tab(parents[hash]);
|
||||
show_tab(hash);
|
||||
}
|
||||
if (~$.inArray(hash, ['#resize', "#resizevm_cpu", "#resizevm_mem", "#resizevm_disk"])) {
|
||||
var btnsect = $('#navbtn>li>a');
|
||||
$(btnsect).each(function () {
|
||||
if ($(this).attr('href') === '#resize') {
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (~$.inArray(hash, ['#osinfo', '#boot_opt', "#disks", '#network', '#clone', '#xmledit', '#vncsettings', '#migrate', '#options', '#users'])) {
|
||||
var btnsect = $('#navbtn>li>a');
|
||||
$(btnsect).each(function () {
|
||||
if ($(this).attr('href') === '#settings') {
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
var btn = $('#settings>div>ul>li>a');
|
||||
$(btn).each(function () {
|
||||
if ($(this).attr('href') === hash) {
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (~$.inArray(hash, ['#takesnapshot', "#takeextsnapshot", "#managesnapshot"])) {
|
||||
var btnsect = $('#navbtn>li>a');
|
||||
$(btnsect).each(function () {
|
||||
if ($(this).attr('href') === '#snapshots') {
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
var btn = $('#snapshots>div>ul>li>a');
|
||||
$(btn).each(function () {
|
||||
if ($(this).attr('href') === hash) {
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
// Settings opens on its first section the user has (Boot is superuser-only)
|
||||
var settings_tabs = document.querySelector('#settings .nav-tabs');
|
||||
if (settings_tabs && !settings_tabs.querySelector('.nav-link.active')) {
|
||||
var first = settings_tabs.querySelector('.nav-link:not(#osinfo-tab)');
|
||||
if (first) {
|
||||
bootstrap.Tab.getOrCreateInstance(first).show();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function get_osinfo() {
|
||||
document.querySelector("#settings-tab").click();
|
||||
document.querySelector("#osinfo-tab").click();
|
||||
show_tab('#settings');
|
||||
show_tab('#osinfo');
|
||||
|
||||
$.getJSON("{% url 'instances:osinfo' instance.id %}", function (data) {
|
||||
$.each(data, function() {
|
||||
|
|
|
|||
|
|
@ -7,21 +7,21 @@
|
|||
{% trans "Console" %}
|
||||
</button>
|
||||
</li>
|
||||
{% if app_settings.SHOW_ACCESS_ROOT_PASSWORD == 'True' %}
|
||||
{% if app_settings.SHOW_ACCESS_ROOT_PASSWORD == 'True' and can_change %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#rootpasswd" type="button" role="tab" aria-controls="rootpasswd" aria-selected="false">
|
||||
{% trans "Root Password" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if app_settings.SHOW_ACCESS_SSH_KEYS == 'True' %}
|
||||
{% if app_settings.SHOW_ACCESS_SSH_KEYS == 'True' and can_change %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#sshkeys" type="button" role="tab" aria-controls="sshkeys" aria-selected="false">
|
||||
{% trans "SSH Keys" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if instance.status == 1 %}
|
||||
{% if instance.status == 1 and can_open_console %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#vdiconsole" type="button" role="tab" aria-controls="vdiconsole" aria-selected="false">
|
||||
{% trans "VDI" %}
|
||||
|
|
@ -34,6 +34,9 @@
|
|||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="vnconsole">
|
||||
<p>{% blocktrans with type=instance.console_type|upper %} This action opens a new window with a {{ type }} connection to the console of the instance.{% endblocktrans %}
|
||||
</p>
|
||||
{% if not can_open_console %}
|
||||
<p>{% trans "Only the owners of this instance and administrators can open its console." %}</p>
|
||||
{% else %}
|
||||
{% if instance.console_type == 'vnc' %}
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="scale"
|
||||
|
|
@ -80,9 +83,10 @@
|
|||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-end disabled">{% trans "Console" %}</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% if app_settings.SHOW_ACCESS_ROOT_PASSWORD == 'True' %}
|
||||
{% if app_settings.SHOW_ACCESS_ROOT_PASSWORD == 'True' and can_change %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="rootpasswd">
|
||||
<p>{% trans "You need shut down your instance and enter a new root password." %}</p>
|
||||
<form action="{% url 'instances:rootpasswd' instance.id %}" method="post"
|
||||
|
|
@ -105,7 +109,7 @@
|
|||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if app_settings.SHOW_ACCESS_SSH_KEYS == 'True' %}
|
||||
{% if app_settings.SHOW_ACCESS_SSH_KEYS == 'True' and can_change %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="sshkeys">
|
||||
<p>{% trans "You need shut down your instance and choose your public key." %}</p>
|
||||
<form action="{% url 'instances:add_public_key' instance.id %}"method="post"
|
||||
|
|
@ -134,7 +138,7 @@
|
|||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if instance.status == 1 %}
|
||||
{% if instance.status == 1 and can_open_console %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="vdiconsole">
|
||||
<p>{% trans "This action opens a remote viewer with a connection to the console of the instance." %}</p>
|
||||
<div class="input-group">
|
||||
|
|
@ -150,6 +154,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{% block script %}
|
||||
{% if can_open_console %}
|
||||
<script>
|
||||
function open_console(view_style) {
|
||||
const sc = $("#scale").is(':checked');
|
||||
|
|
@ -160,4 +165,5 @@
|
|||
window.open(url, '', 'width=850,height=600')
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
{% load i18n %}
|
||||
<div class="tab-pane active" id="power" role="tabpanel">
|
||||
{% if not can_power %}
|
||||
<p>{% trans "Only the owners of this instance and administrators can change its power state." %}</p>
|
||||
{% else %}
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist" aria-label="Instance power actions">
|
||||
{% if instance.status == 1 or instance.status == 2 %}
|
||||
|
|
@ -144,4 +147,5 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="resizevm_cpu">
|
||||
{% if request.user.is_superuser or request.user.is_staff or userinstance.is_change %}
|
||||
{% if can_change %}
|
||||
{% if instance.status == 5 or not instance.vcpus %}
|
||||
<form action="{% url 'instances:resizevm_cpu' instance.id %}" method="post" role="form" aria-label="Resize instance cpu form">{% csrf_token %}
|
||||
<p class="fw-bold">{% trans "Logical host CPUs" %} : {{ vcpu_host }}</p>
|
||||
|
|
@ -62,6 +62,7 @@
|
|||
<p class="fw-bold">{% trans "Logical Instance Active/Maximum CPUs" %} : {{ instance.cur_vcpu }} / {{ instance.vcpu }} </p>
|
||||
<div class="col-sm-3"></div>
|
||||
<div class="col-sm-6">
|
||||
{% if request.user.is_superuser %}
|
||||
{% for id, vcpu in instance.vcpus.items %}
|
||||
<form action="{% url 'instances:set_vcpu' instance.id %}" method="post" role="form" aria-label="Resize instance cpu form">{% csrf_token %}
|
||||
<div class="d-grid col-sm-3">
|
||||
|
|
@ -76,6 +77,7 @@
|
|||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-3"></div>
|
||||
{% endif %}
|
||||
|
|
@ -86,7 +88,7 @@
|
|||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="resizevm_mem">
|
||||
{% if request.user.is_superuser or request.user.is_staff or userinstance.is_change %}
|
||||
{% if can_change %}
|
||||
<form action="{% url 'instances:resize_memory' instance.id %}" method="post" role="form" aria-label="Resize instance memory form">
|
||||
{% csrf_token %}
|
||||
<p class="fw-bold">{% trans "Total host memory" %}: {{ memory_host|filesizeformat }}</p>
|
||||
|
|
@ -127,7 +129,7 @@
|
|||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="resizevm_disk">
|
||||
{% if request.user.is_superuser or request.user.is_staff or userinstance.is_change %}
|
||||
{% if can_change %}
|
||||
<form action="{% url 'instances:resize_disk' instance.id %}" method="post" role="form" aria-label="Resize instance disk form">
|
||||
{% csrf_token %}
|
||||
<p class="fw-bold">{% trans "Disk allocation (GB)" %}:</p>
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
<div class="tab-pane" id="settings" role="tabpanel">
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist" aria-label="Instance settings">
|
||||
{% if request.user.is_superuser %}
|
||||
{% if instance.guest_agent_ready %}
|
||||
{% if instance.guest_agent_ready %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="px-1 nav-link" data-bs-toggle="tab" id="osinfo-tab" data-bs-target="#osinfo" type="button" role="tab" aria-controls="osinfo" aria-selected="false" onclick="get_osinfo();">
|
||||
<span>{% bs_icon 'info-circle' %} </span>
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#boot_opt" type="button" role="tab" aria-controls="boot_opt" aria-selected="true">
|
||||
{% trans "Boot" %}
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.instances.clone_instances %}
|
||||
{% if can_clone %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#clone" type="button" role="tab" aria-controls="clone" aria-selected="false">
|
||||
{% trans "Clone" %}
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.instances.clone_instances %}
|
||||
{% if can_change %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#options" type="button" role="tab" aria-controls="options" aria-selected="false">
|
||||
{% trans "Options" %}
|
||||
|
|
@ -73,8 +73,10 @@
|
|||
</ul>
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
{% if request.user.is_superuser %}
|
||||
{% if instance.guest_agent_ready %}
|
||||
{% include 'instances/info_tab.html' %}
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="boot_opt">
|
||||
<p class="fw-bold">{% trans 'Autostart' %}</p>
|
||||
<div class="row">
|
||||
|
|
@ -716,7 +718,7 @@
|
|||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if perms.instances.clone_instances %}
|
||||
{% if can_clone %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="clone">
|
||||
<p class="fw-bold">{% trans "Create a clone" %}</p>
|
||||
<form class="form" action="{% url 'instances:clone' instance.id %}" method="post" role="form" aria-label="Create clone form">{% csrf_token %}
|
||||
|
|
@ -811,6 +813,8 @@
|
|||
</form>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if can_change %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="options">
|
||||
<p>{% trans "To set instance template name description, shutdown the instance." %}</p>
|
||||
<form action="{% url 'instances:change_options' instance.id %}" method="post" role="form" aria-label="Set instance template name description form">{% csrf_token %}
|
||||
|
|
@ -847,7 +851,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if request.user.is_superuser %}
|
||||
<p class="fw-bold">{% trans "To set instance video model, shutdown the instance." %}</p>
|
||||
<form action="{% url 'instances:set_video_model' instance.id %}" method="post" role="form" aria-label="Set instance video model form">{% csrf_token %}
|
||||
<div class="row">
|
||||
|
|
@ -914,6 +918,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
169
instances/test_page_controls.py
Normal file
169
instances/test_page_controls.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
"""
|
||||
The VM page renders a control only for users the backend lets use it:
|
||||
power to superusers and owners; the console, virt-viewer file and VDI URL to
|
||||
superusers and owners (can_open_console); root password, SSH key, resize and
|
||||
options to superusers and owners with is_change; per-vCPU hotplug, video
|
||||
model and guest agent to superusers; clone with clone_instances on a template
|
||||
or with is_change; snapshots with snapshot_instances and is_change, on a
|
||||
template only for staff. is_staff and the global view_instances permission
|
||||
alone are read-only.
|
||||
"""
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from lxml import html as lxml_html
|
||||
|
||||
from accounts.models import UserInstance
|
||||
from appsettings.models import AppSettings
|
||||
from computes.models import Compute
|
||||
from instances.models import Instance
|
||||
|
||||
CONSOLE = {"console", "vv file", "vdi"}
|
||||
POWER = {"power"}
|
||||
CHANGE = {"root password", "ssh key", "resize", "options"}
|
||||
SUPERUSER_ONLY = {"vcpu hotplug", "video model", "guest agent"}
|
||||
|
||||
|
||||
class PageControls:
|
||||
def setUp(self):
|
||||
AppSettings.objects.filter(
|
||||
key__in=["SHOW_ACCESS_ROOT_PASSWORD", "SHOW_ACCESS_SSH_KEYS"]
|
||||
).update(value="True")
|
||||
self.compute = Compute.objects.create(
|
||||
name="ctl-compute", hostname="127.0.0.1:1", login="root", password="", type=1
|
||||
)
|
||||
self.instance = Instance.objects.create(
|
||||
compute=self.compute, name="ctl-vm", uuid="dddddddd-eeee-ffff-0000-111111111111"
|
||||
)
|
||||
|
||||
def user(self, name, staff=False, perms=(), owner=None):
|
||||
user = get_user_model().objects.create_user(name, password="x", is_staff=staff)
|
||||
for codename in perms:
|
||||
user.user_permissions.add(Permission.objects.get(codename=codename))
|
||||
if owner is not None:
|
||||
UserInstance.objects.create(instance=self.instance, user=user, **owner)
|
||||
return user
|
||||
|
||||
def controls(self, user, status=1):
|
||||
self.client.force_login(user)
|
||||
with patch("instances.models.wvmInstance") as mock_wvm, patch.object(
|
||||
Compute, "status", new_callable=PropertyMock, return_value=True
|
||||
):
|
||||
proxy = mock_wvm.return_value
|
||||
proxy.get_memory.return_value = 1024
|
||||
proxy.get_cur_memory.return_value = 1024
|
||||
proxy.get_max_memory.return_value = 4096
|
||||
proxy.get_vcpu.return_value = 2
|
||||
proxy.get_cur_vcpu.return_value = 1
|
||||
proxy.get_max_cpus.return_value = [1, 2, 3, 4]
|
||||
# a running VM with hotpluggable vCPUs shows the per-vCPU buttons
|
||||
proxy.get_vcpus.return_value = {
|
||||
0: {"enabled": "yes", "hotpluggable": "no"},
|
||||
1: {"enabled": "no", "hotpluggable": "yes"},
|
||||
}
|
||||
for getter in ("get_networks", "get_ifaces", "get_storages", "get_disk_devices",
|
||||
"get_media_devices", "get_net_devices", "get_snapshot",
|
||||
"get_external_snapshots"):
|
||||
getattr(proxy, getter).return_value = []
|
||||
proxy.get_console_type.return_value = "vnc"
|
||||
proxy.get_status.return_value = status
|
||||
response = self.client.get(reverse("instances:instance", args=[self.instance.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
doc = lxml_html.fromstring(response.content)
|
||||
pk = self.instance.id
|
||||
|
||||
def form(name):
|
||||
return bool(doc.xpath("//form[@action=$a]", a=reverse(f"instances:{name}", args=[pk])))
|
||||
|
||||
found = {
|
||||
"console": bool(doc.xpath("//button[@id='consoleBtnGroup']")),
|
||||
"vv file": bool(doc.xpath("//a[@href=$a]", a=reverse("instances:getvvfile", args=[pk]))),
|
||||
"vdi": bool(doc.xpath("//*[@id='vdiconsole']")),
|
||||
"power": form("poweroff") or form("poweron"),
|
||||
"root password": form("rootpasswd"),
|
||||
"ssh key": form("add_public_key"),
|
||||
# a running VM with hotplug shows per-vCPU buttons instead of the CPU form
|
||||
"resize": form("resize_memory"),
|
||||
"options": form("change_options"),
|
||||
"vcpu hotplug": form("set_vcpu"),
|
||||
"video model": form("set_video_model"),
|
||||
"guest agent": form("set_guest_agent"),
|
||||
"clone": bool(doc.xpath("//*[@id='clone']")),
|
||||
"snapshots": bool(doc.xpath("//*[@id='snapshots']")),
|
||||
}
|
||||
return {name for name, present in found.items() if present}
|
||||
|
||||
|
||||
|
||||
class PageControlsTestCase(PageControls, TestCase):
|
||||
def test_superuser_sees_every_control(self):
|
||||
superuser = get_user_model().objects.create_superuser("ctl_super", "ctl_super@example.com", "x")
|
||||
self.assertEqual(
|
||||
self.controls(superuser), CONSOLE | POWER | CHANGE | SUPERUSER_ONLY | {"clone", "snapshots"}
|
||||
)
|
||||
|
||||
def test_global_viewers_see_nothing_to_act_on(self):
|
||||
perms = ("view_instances", "clone_instances", "snapshot_instances")
|
||||
for user in (self.user("ctl_viewer", perms=perms), self.user("ctl_staff", staff=True, perms=perms)):
|
||||
with self.subTest(user=user.username):
|
||||
self.assertEqual(self.controls(user), set())
|
||||
|
||||
def test_owner_without_change_powers_and_opens_the_console(self):
|
||||
owner = self.user("ctl_owner", perms=("clone_instances", "snapshot_instances"), owner={})
|
||||
self.assertEqual(self.controls(owner), CONSOLE | POWER)
|
||||
|
||||
def test_owner_with_change_gets_change_clone_and_snapshots(self):
|
||||
changer = self.user("ctl_changer", perms=("clone_instances", "snapshot_instances"),
|
||||
owner={"is_change": True})
|
||||
self.assertEqual(self.controls(changer), CONSOLE | POWER | CHANGE | {"clone", "snapshots"})
|
||||
|
||||
def test_owner_with_change_and_no_extra_permissions(self):
|
||||
changer = self.user("ctl_plain_changer", owner={"is_change": True})
|
||||
self.assertEqual(self.controls(changer), CONSOLE | POWER | CHANGE)
|
||||
|
||||
|
||||
class TemplatePageControlsTestCase(PageControls, TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.instance.is_template = True
|
||||
self.instance.save()
|
||||
|
||||
def test_a_template_is_cloned_by_viewing_it(self):
|
||||
viewer = self.user("tpl_viewer", perms=("view_instances", "clone_instances"))
|
||||
self.assertEqual(self.controls(viewer, status=5), {"clone"})
|
||||
|
||||
def test_snapshots_of_a_template_are_for_staff_owners(self):
|
||||
perms = ("clone_instances", "snapshot_instances")
|
||||
owner = self.user("tpl_changer", perms=perms, owner={"is_change": True})
|
||||
staff = self.user("tpl_staff_changer", staff=True, perms=perms, owner={"is_change": True})
|
||||
self.assertNotIn("snapshots", self.controls(owner, status=5))
|
||||
self.assertIn("snapshots", self.controls(staff, status=5))
|
||||
|
||||
|
||||
class TemplateSnapshotRuleTestCase(PageControls, TestCase):
|
||||
"""The backend side of the template rule: only staff owners (and
|
||||
superusers) take a snapshot of a template; other owners with is_change
|
||||
and snapshot_instances are turned away without a libvirt call."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.instance.is_template = True
|
||||
self.instance.save()
|
||||
|
||||
def snapshot(self, user):
|
||||
self.client.force_login(user)
|
||||
with patch("instances.models.wvmInstance") as mock_wvm:
|
||||
self.client.post(
|
||||
reverse("instances:snapshot", args=[self.instance.id]), {"name": "s1", "description": ""}
|
||||
)
|
||||
return mock_wvm.return_value.create_snapshot.called
|
||||
|
||||
def test_only_staff_owners_snapshot_a_template(self):
|
||||
perms = ("snapshot_instances",)
|
||||
owner = self.user("snap_changer", perms=perms, owner={"is_change": True})
|
||||
staff = self.user("snap_staff_changer", staff=True, perms=perms, owner={"is_change": True})
|
||||
self.assertFalse(self.snapshot(owner))
|
||||
self.assertTrue(self.snapshot(staff))
|
||||
|
|
@ -135,6 +135,21 @@ def _instance_page(request, pk, instance, compute):
|
|||
)
|
||||
except UserInstance.DoesNotExist:
|
||||
userinstance = None
|
||||
# The page offers only what the backend allows this user (get_instance
|
||||
# perm_type="change", utils.can_open_console); is_staff and the global
|
||||
# view_instances permission are read-only.
|
||||
can_open_console = utils.can_open_console(request.user, instance)
|
||||
can_power = request.user.is_superuser or userinstance is not None
|
||||
can_change = request.user.is_superuser or bool(userinstance and userinstance.is_change)
|
||||
can_clone = request.user.has_perm("instances.clone_instances") and (instance.is_template or can_change)
|
||||
can_snapshot = (
|
||||
can_change
|
||||
and request.user.has_perm("instances.snapshot_instances")
|
||||
and (request.user.is_superuser or request.user.is_staff or not instance.is_template)
|
||||
)
|
||||
show_settings = (
|
||||
request.user.is_superuser or can_manage_console or can_clone or can_change or instance.guest_agent_ready
|
||||
)
|
||||
|
||||
memory_range = [256, 512, 768, 1024, 2048, 3072, 4096, 6144, 8192, 16384]
|
||||
if instance.memory not in memory_range:
|
||||
|
|
|
|||
|
|
@ -128,6 +128,12 @@ ROLE_EXPECTATIONS = {
|
|||
}
|
||||
|
||||
|
||||
# A template VM is reached the same way: on templates, snapshot requests and
|
||||
# poweron are no-ops for these roles, and cloning (which none of them may do
|
||||
# without instances.clone_instances) has its own rule, see the clone test.
|
||||
TEMPLATE_ROLE_EXPECTATIONS = ROLE_EXPECTATIONS
|
||||
|
||||
|
||||
def iter_patterns(patterns, prefix="", params=None):
|
||||
params = params or {}
|
||||
for p in patterns:
|
||||
|
|
@ -295,6 +301,32 @@ class AuthorizationMatrixTestCase(TestCase):
|
|||
f"{role}: (unexpectedly reachable, unexpectedly denied)",
|
||||
)
|
||||
|
||||
def test_role_reachability_on_a_template_vm(self):
|
||||
self.instance.is_template = True
|
||||
self.instance.save()
|
||||
for role, (is_staff, view_instances, owner_flags, expected) in TEMPLATE_ROLE_EXPECTATIONS.items():
|
||||
with self.subTest(role=role):
|
||||
user = self._make_role_user(role, is_staff, view_instances, owner_flags)
|
||||
reachable = self._reachable(user)
|
||||
self.assertEqual(
|
||||
(sorted(reachable - expected), sorted(expected - reachable)),
|
||||
([], []),
|
||||
f"template, {role}: (unexpectedly reachable, unexpectedly denied)",
|
||||
)
|
||||
|
||||
def test_a_template_is_cloned_by_viewing_it_a_vm_needs_change(self):
|
||||
user = self._make_role_user("cloner", False, True, None)
|
||||
user.user_permissions.add(Permission.objects.get(codename="clone_instances"))
|
||||
url = reverse("instances:clone", args=[self.instance.pk])
|
||||
for is_template, reached in ((False, False), (True, True)):
|
||||
with self.subTest(is_template=is_template):
|
||||
self.instance.is_template = is_template
|
||||
self.instance.save()
|
||||
with transaction.atomic():
|
||||
response = self._request(user, url)
|
||||
transaction.set_rollback(True)
|
||||
self.assertEqual(response.status_code not in DENIED, reached, response.status_code)
|
||||
|
||||
def test_owner_power_actions_succeed_through_web_and_api_with_real_test_driver(self):
|
||||
import libvirt
|
||||
from vrtManager.instance import wvmInstance
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue