Merge branch 'main' into testing

This commit is contained in:
Michael Lipp 2025-03-31 22:57:49 +02:00
commit 8bf6692b8d
12 changed files with 178 additions and 44 deletions

View file

@ -1,6 +1,6 @@
/* /*
* VM-Operator * VM-Operator
* Copyright (C) 2023 Michael N. Lipp * Copyright (C) 2023,2025 Michael N. Lipp
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -83,8 +83,18 @@
* [YamlConfigurationStore] *-right[hidden]- [Controller] * [YamlConfigurationStore] *-right[hidden]- [Controller]
* *
* [Manager] *-- [Controller] * [Manager] *-- [Controller]
* [Controller] *-- [VmWatcher] * Component VmMonitor as VmMonitor <<internal>>
* [Controller] *-- [Reconciler] * [Controller] *-- [VmMonitor]
* [VmMonitor] -right[hidden]- [PoolMonitor]
* Component PoolMonitor as PoolMonitor <<internal>>
* [Controller] *-- [PoolMonitor]
* Component PodMonitor as PodMonitor <<internal>>
* [Controller] *-- [PodMonitor]
* [PodMonitor] -up[hidden]- VmMonitor
* Component DisplaySecretMonitor as DisplaySecretMonitor <<internal>>
* [Controller] *-- [DisplaySecretMonitor]
* [DisplaySecretMonitor] -up[hidden]- VmMonitor
* [Controller] *-left- [Reconciler]
* [Controller] -right[hidden]- [GuiHttpServer] * [Controller] -right[hidden]- [GuiHttpServer]
* *
* [Manager] *-down- [GuiSocketServer:8080] * [Manager] *-down- [GuiSocketServer:8080]

View file

@ -1,8 +1,8 @@
apiVersion: "vmoperator.jdrupes.org/v1" apiVersion: "vmoperator.jdrupes.org/v1"
kind: VirtualMachine kind: VirtualMachine
metadata: metadata:
namespace: vmop-dev namespace: vmop-test
name: unittest-vm name: test-vm
spec: spec:
image: image:
repository: docker-registry.lan.mnl.de repository: docker-registry.lan.mnl.de

View file

@ -0,0 +1,111 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../deploy
namespace: vmop-test
patches:
- patch: |-
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: vmop-image-repository
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: local-path
- patch: |-
kind: ConfigMap
apiVersion: v1
metadata:
name: vm-operator
data:
# Keep in sync with config.yaml
config.yaml: |
"/Manager":
# clusterName: "test"
"/Controller":
"/Reconciler":
runnerData:
storageClassName: null
loadBalancerService:
labels:
label1: label1
label2: toBeReplaced
annotations:
metallb.universe.tf/loadBalancerIPs: 192.168.168.1
metallb.universe.tf/ip-allocated-from-pool: single-common
metallb.universe.tf/allow-shared-ip: single-common
"/GuiSocketServer":
port: 8888
"/GuiHttpServer":
# This configures the GUI
"/ConsoleWeblet":
"/WebConsole":
"/LoginConlet":
users:
- name: admin
fullName: Administrator
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
- name: test1
fullName: Test Account
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
- name: test2
fullName: Test Account
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
- name: test3
fullName: Test Account
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
"/RoleConfigurator":
rolesByUser:
# User admin has role admin
admin:
- admin
test1:
- user
test2:
- user
test3:
- user
# All users have role other
"*":
- other
replace: false
"/RoleConletFilter":
conletTypesByRole:
# Admins can use all conlets
admin:
- "*"
user:
- org.jdrupes.vmoperator.vmviewer.VmViewer
# Others cannot use any conlet (except login conlet to log out)
other:
- org.jgrapes.webconlet.locallogin.LoginConlet
"/ComponentCollector":
"/VmAccess":
displayResource:
preferredIpVersion: ipv4
syncPreviewsFor:
- role: user
- target:
group: apps
version: v1
kind: Deployment
name: vm-operator
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.manager:test
- op: replace
path: /spec/template/spec/containers/0/imagePullPolicy
value: Always
- op: replace
path: /spec/replicas
value: 0

View file

@ -41,7 +41,7 @@ class BasicTests {
private static APIResource vmsContext; private static APIResource vmsContext;
private static K8sV1DeploymentStub mgrDeployment; private static K8sV1DeploymentStub mgrDeployment;
private static K8sDynamicStub vmStub; private static K8sDynamicStub vmStub;
private static final String VM_NAME = "unittest-vm"; private static final String VM_NAME = "test-vm";
private static final Object EXISTS = new Object(); private static final Object EXISTS = new Object();
@BeforeAll @BeforeAll
@ -54,7 +54,7 @@ class BasicTests {
// Update manager pod by scaling deployment // Update manager pod by scaling deployment
mgrDeployment mgrDeployment
= K8sV1DeploymentStub.get(client, "vmop-dev", "vm-operator"); = K8sV1DeploymentStub.get(client, "vmop-test", "vm-operator");
mgrDeployment.scale(0); mgrDeployment.scale(0);
mgrDeployment.scale(1); mgrDeployment.scale(1);
waitForManager(); waitForManager();
@ -65,13 +65,13 @@ class BasicTests {
vmsContext = apiRes.get(); vmsContext = apiRes.get();
// Cleanup existing VM // Cleanup existing VM
K8sDynamicStub.get(client, vmsContext, "vmop-dev", VM_NAME) K8sDynamicStub.get(client, vmsContext, "vmop-test", VM_NAME)
.delete(); .delete();
ListOptions listOpts = new ListOptions(); ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + VM_NAME + "," + "app.kubernetes.io/instance=" + VM_NAME + ","
+ "app.kubernetes.io/component=" + DisplaySecret.NAME); + "app.kubernetes.io/component=" + DisplaySecret.NAME);
var secrets = K8sV1SecretStub.list(client, "vmop-dev", listOpts); var secrets = K8sV1SecretStub.list(client, "vmop-test", listOpts);
for (var secret : secrets) { for (var secret : secrets) {
secret.delete(); secret.delete();
} }
@ -103,7 +103,7 @@ class BasicTests {
"app.kubernetes.io/managed-by=" + VM_OP_NAME + "," "app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME + "," + "app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + VM_NAME); + "app.kubernetes.io/instance=" + VM_NAME);
var knownPvcs = K8sV1PvcStub.list(client, "vmop-dev", listOpts); var knownPvcs = K8sV1PvcStub.list(client, "vmop-test", listOpts);
for (var pvc : knownPvcs) { for (var pvc : knownPvcs) {
pvc.delete(); pvc.delete();
} }
@ -112,7 +112,7 @@ class BasicTests {
@AfterAll @AfterAll
static void tearDownAfterClass() throws Exception { static void tearDownAfterClass() throws Exception {
// Cleanup // Cleanup
K8sDynamicStub.get(client, vmsContext, "vmop-dev", VM_NAME) K8sDynamicStub.get(client, vmsContext, "vmop-test", VM_NAME)
.delete(); .delete();
deletePvcs(); deletePvcs();
@ -124,7 +124,7 @@ class BasicTests {
void testConfigMap() void testConfigMap()
throws IOException, InterruptedException, ApiException { throws IOException, InterruptedException, ApiException {
K8sV1ConfigMapStub stub K8sV1ConfigMapStub stub
= K8sV1ConfigMapStub.get(client, "vmop-dev", VM_NAME); = K8sV1ConfigMapStub.get(client, "vmop-test", VM_NAME);
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
if (stub.model().isPresent()) { if (stub.model().isPresent()) {
break; break;
@ -134,7 +134,7 @@ class BasicTests {
// Check config map // Check config map
var config = stub.model().get(); var config = stub.model().get();
Map<List<? extends Object>, Object> toCheck = Map.of( Map<List<? extends Object>, Object> toCheck = Map.of(
List.of("namespace"), "vmop-dev", List.of("namespace"), "vmop-test",
List.of("name"), VM_NAME, List.of("name"), VM_NAME,
List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME,
List.of("labels", "app.kubernetes.io/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
@ -191,7 +191,7 @@ class BasicTests {
+ "app.kubernetes.io/component=" + DisplaySecret.NAME); + "app.kubernetes.io/component=" + DisplaySecret.NAME);
Collection<K8sV1SecretStub> secrets = null; Collection<K8sV1SecretStub> secrets = null;
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
secrets = K8sV1SecretStub.list(client, "vmop-dev", listOpts); secrets = K8sV1SecretStub.list(client, "vmop-test", listOpts);
if (secrets.size() > 0) { if (secrets.size() > 0) {
break; break;
} }
@ -207,7 +207,7 @@ class BasicTests {
@Test @Test
void testRunnerPvc() throws ApiException, InterruptedException { void testRunnerPvc() throws ApiException, InterruptedException {
var stub var stub
= K8sV1PvcStub.get(client, "vmop-dev", VM_NAME + "-runner-data"); = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-runner-data");
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
if (stub.model().isPresent()) { if (stub.model().isPresent()) {
break; break;
@ -227,7 +227,7 @@ class BasicTests {
@Test @Test
void testSystemDiskPvc() throws ApiException, InterruptedException { void testSystemDiskPvc() throws ApiException, InterruptedException {
var stub var stub
= K8sV1PvcStub.get(client, "vmop-dev", VM_NAME + "-system-disk"); = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-system-disk");
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
if (stub.model().isPresent()) { if (stub.model().isPresent()) {
break; break;
@ -248,7 +248,7 @@ class BasicTests {
@Test @Test
void testDisk1Pvc() throws ApiException, InterruptedException { void testDisk1Pvc() throws ApiException, InterruptedException {
var stub var stub
= K8sV1PvcStub.get(client, "vmop-dev", VM_NAME + "-disk-1"); = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-disk-1");
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
if (stub.model().isPresent()) { if (stub.model().isPresent()) {
break; break;
@ -274,7 +274,7 @@ class BasicTests {
new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state" new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state"
+ "\", \"value\": \"Running\"}]"), + "\", \"value\": \"Running\"}]"),
client.defaultPatchOptions()).isPresent()); client.defaultPatchOptions()).isPresent());
var stub = K8sV1PodStub.get(client, "vmop-dev", VM_NAME); var stub = K8sV1PodStub.get(client, "vmop-test", VM_NAME);
for (int i = 0; i < 20; i++) { for (int i = 0; i < 20; i++) {
if (stub.model().isPresent()) { if (stub.model().isPresent()) {
break; break;
@ -303,7 +303,7 @@ class BasicTests {
@Test @Test
public void testLoadBalancer() throws ApiException, InterruptedException { public void testLoadBalancer() throws ApiException, InterruptedException {
var stub = K8sV1ServiceStub.get(client, "vmop-dev", VM_NAME); var stub = K8sV1ServiceStub.get(client, "vmop-test", VM_NAME);
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
if (stub.model().isPresent()) { if (stub.model().isPresent()) {
break; break;

View file

@ -157,6 +157,15 @@ import org.jgrapes.util.events.WatchFile;
* *
* success --> Running * success --> Running
* *
* state Running {
* state Booting
* state Booted
*
* [*] -right-> Booting
* Booting -down-> Booting: VserportChanged[guest agent connected]/fire GetOsinfo
* Booting --> Booted: Osinfo
* }
*
* state Terminating { * state Terminating {
* state terminate <<entryPoint>> * state terminate <<entryPoint>>
* state qemuRunning <<choice>> * state qemuRunning <<choice>>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -12,10 +12,10 @@ layout: vm-operator
An overview display shows the current CPU and RAM usage and a graph An overview display shows the current CPU and RAM usage and a graph
with recent changes. with recent changes.
![VM-Operator GUI](VM-Operator-GUI-preview.png) ![VM-Operator admin GUI preview](VM-Operator-GUI-preview.png)
The detail display lists all VMs. From here you can start and stop The detail display lists all VMs. From here you can start and stop
the VMs and adjust the CPU and RAM usages (modifies the definition the VMs and adjust the CPU and RAM usages (modifies the definition
in kubernetes). in kubernetes).
![VM-Operator GUI](VM-Operator-GUI-view.png) ![VM-Operator admin GUI view](VM-Operator-GUI-view.png)

View file

@ -9,7 +9,7 @@ layout: vm-operator
When users log into the web GUI, they have already authenticated with the When users log into the web GUI, they have already authenticated with the
VM-Operator. In some environments, requiring an additional login on the VM-Operator. In some environments, requiring an additional login on the
guest OS can be cumbersome. To enhance the user experience, the VM-Operator guest OS can be annoying. To enhance the user experience, the VM-Operator
supports automatic login on the guest operating system, thus eliminating supports automatic login on the guest operating system, thus eliminating
the need for multiple logins. However, this feature requires specific the need for multiple logins. However, this feature requires specific
support from the guest OS. support from the guest OS.
@ -18,9 +18,9 @@ support from the guest OS.
Automatic login requires an agent running inside the guest OS. Similar Automatic login requires an agent running inside the guest OS. Similar
to QEMU's standard guest agent, the VM-Operator agent communicates with to QEMU's standard guest agent, the VM-Operator agent communicates with
the host via a tty device (`/dev/virtio-ports/org.jdrupes.vmop_agent.0`). On the host via a tty device (provided in the guest as
modern Linux systems, `udev` can detect this device and trigger the start `/dev/virtio-ports/org.jdrupes.vmop_agent.0`). On modern Linux systems, `udev` can
of an associated systemd service. detect this device and trigger the start of an associated systemd service.
Sample configuration files for a VM-Operator agent are available Sample configuration files for a VM-Operator agent are available
[here](https://github.com/mnlipp/VM-Operator/tree/main/dev-example/vmop-agent). [here](https://github.com/mnlipp/VM-Operator/tree/main/dev-example/vmop-agent).

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 588 KiB

After

Width:  |  Height:  |  Size: 591 KiB

Before After
Before After

View file

@ -9,7 +9,7 @@ layout: vm-operator
# Welcome to VM-Operator # Welcome to VM-Operator
![Overview picture](index-pic.svg) ![VM-Operator summary picture](index-pic.svg)
This project provides an easy to use and flexible solution for This project provides an easy to use and flexible solution for
running QEMU/KVM based virtual machines (VMs) in Kubernetes pods. running QEMU/KVM based virtual machines (VMs) in Kubernetes pods.

View file

@ -46,16 +46,20 @@ spec:
The `retention` specifies how long the assignment of a VM from the pool to The `retention` specifies how long the assignment of a VM from the pool to
a user remains valid after the user closes the console. This ensures that a user remains valid after the user closes the console. This ensures that
a user can resume work within this timeframe without the risk of another a user can resume work within this timeframe without the risk of another
user taking over the VM. The time is specified as user taking over the VM. The time is specified as an
[ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations). [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations).
Specifying an ISO 8601 time is also supported, but if you consider
using an absolute time, check again whether a dedicated VM for the user
isn't the more appropriate choice.
Setting `loginOnAssignment` to `true` triggers automatic login of the Setting `loginOnAssignment` to `true` (defaults to `false`) triggers automatic
user (as described in [section auto login](auto-login.html)) when login of the user (as described in [section auto login](auto-login.html))
the VM is assigned. The `permissions` property specifies the actions when the VM is assigned. The `permissions` property specifies the actions
that users or roles can perform on assigned VMs. that users or roles can perform on assigned VMs. The `may` property defaults
to `[accessConsole]` if not specified.
VMs become members of one (or more) pools by adding the pool name to VMs become members of one (or more) pools by adding the pool name to
the `spec.pools` array, as shown below: the `spec.pools` array in the VM definition, as shown below:
```yaml ```yaml
apiVersion: "vmoperator.jdrupes.org/v1" apiVersion: "vmoperator.jdrupes.org/v1"