Develop/v3 (#27)
Some checks failed
Java CI with Gradle / build (push) Has been cancelled

Prepare release.
This commit is contained in:
Michael N. Lipp 2024-06-09 22:54:42 +02:00 committed by GitHub
parent 659463b3b4
commit 65a5cfd286
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 500 additions and 132 deletions

View file

@ -18,10 +18,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
ref: main
- name: Install graphviz - name: Install graphviz
run: sudo apt-get install graphviz run: sudo apt-get install graphviz
- name: Install podman - name: Install podman

View file

@ -1,4 +1,4 @@
[![Java CI with Gradle](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml/badge.svg)](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml) [![Java CI with Gradle](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml/badge.svg)](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/2277842dac894de4b663c6aa2779077e)](https://app.codacy.com/gh/mnlipp/VM-Operator/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/2277842dac894de4b663c6aa2779077e)](https://app.codacy.com/gh/mnlipp/VM-Operator/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
![Latest Manager](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=manager*&label=latest) ![Latest Manager](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=manager*&label=latest)
![Latest Runner](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=runner-qemu*&label=latest) ![Latest Runner](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=runner-qemu*&label=latest)
@ -6,8 +6,7 @@
# Run Qemu in Kubernetes Pods # Run Qemu in Kubernetes Pods
The goal of this project is to provide the means for running Qemu The goal of this project is to provide the means for running Qemu
based VMs in Kubernetes pods. based VMs in Kubernetes pods.
See the [project's home page](https://mnlipp.github.io/VM-Operator/) See the [project's home page](https://mnlipp.github.io/VM-Operator/)
for details. for details.

View file

@ -5,6 +5,11 @@
*/ */
plugins { plugins {
// Apply the common versioning conventions.
// Put this at the start, because accessing project.version before
// this is applied makes things fail.
id 'org.jdrupes.vmoperator.versioning-conventions'
// Apply the java Plugin to add support for Java. // Apply the java Plugin to add support for Java.
id 'java' id 'java'
@ -13,9 +18,6 @@ plugins {
// Access to git information // Access to git information
id 'org.ajoberstar.grgit' id 'org.ajoberstar.grgit'
// Apply the common versioning conventions.
id 'org.jdrupes.vmoperator.versioning-conventions'
} }
repositories { repositories {

View file

@ -21,11 +21,13 @@ scmVersion {
} }
var p = shortened.replace('.', '-') + "-" var p = shortened.replace('.', '-') + "-"
if (grgit.branch.current.name != "main" if (grgit.branch.current.name != "main"
&& !grgit.branch.current.name.startsWith("release")) { && grgit.branch.current.name != "HEAD"
&& !grgit.branch.current.name.startsWith("release")
&& !grgit.branch.current.name.startsWith("develop")) {
p = p + grgit.branch.current.name.replace('/', '-') + "-" p = p + grgit.branch.current.name.replace('/', '-') + "-"
} }
prefix = p prefix = p
} }
} }
version = scmVersion.version project.version = scmVersion.version
ext.isSnapshot = version.endsWith('-SNAPSHOT') ext.isSnapshot = version.endsWith('-SNAPSHOT')

View file

@ -1012,7 +1012,12 @@ spec:
type: array type: array
items: items:
type: string type: string
enum: ["start", "stop", "accessConsole", "*"] enum:
- start
- stop
- reset
- accessConsole
- "*"
default: [] default: []
vm: vm:
type: object type: object

View file

@ -1,16 +1,16 @@
# Example setup for development # Example setup for development
The CRD must be deployed independently. Apart from that, the The CRD must be deployed independently. Apart from that, the
`kustomize.yaml` `kustomize.yaml`
* creates a small cdrom image repository and * creates a small cdrom image repository and
* deploys the operator in namespace `vmop-dev` with a replica of 0. * deploys the operator in namespace `vmop-dev` with a replica of 0.
This allows you to run the manager in your IDE. This allows you to run the manager in your IDE.
The `kustomize.yaml` also changes the container image repository for The `kustomize.yaml` also changes the container image repository for
the operator to a private repository for development. You have to the operator to a private repository for development. You have to
adapt this to your own repository if you also want to test your adapt this to your own repository if you also want to test your
development version in a container. development version in a container.

View file

@ -1,17 +1,17 @@
# Example setup # Example setup
The CRD must be deployed independently. The CRD must be deployed independently.
```sh ```sh
kubectl apply -f https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml kubectl apply -f https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml
``` ```
Apart from that, the `kustomize.yaml` defines a namespace for the manager Apart from that, the `kustomize.yaml` defines a namespace for the manager
(and the VMs managed by it) and patches the repository PVC to create (and the VMs managed by it) and patches the repository PVC to create
a small volume using local-path. a small volume using local-path.
A second patch provides a new configuration file for the manager A second patch provides a new configuration file for the manager
that makes it use the local-path storage class when creating the that makes it use the local-path storage class when creating the
small volume for a runner's data. small volume for a runner's data.
The `kustomize.yaml` does not include the test VM. Before creating The `kustomize.yaml` does not include the test VM. Before creating

View file

@ -1,12 +1,12 @@
# Example setup # Example setup
The CRD must be deployed independently. The CRD must be deployed independently.
```sh ```sh
kubectl apply -f https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml kubectl apply -f https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml
``` ```
Apart from that, the `kustomize.yaml` defines a namespace for the manager Apart from that, the `kustomize.yaml` defines a namespace for the manager
(and the VMs managed by it) and applies patches to use `rook-cephfs` as (and the VMs managed by it) and applies patches to use `rook-cephfs` as
storage class (instead of the default storage class). storage class (instead of the default storage class).

View file

@ -45,7 +45,7 @@
body { body {
background-color:#ffffff; background-color:#ffffff;
color:#353833; color:#353833;
font: normal 16px/1.5 "DejaVu Serif", serif; font: normal 16px/1.5 "DejaVu Sans", Arial, Helvetica, sans-serif;
margin:0; margin:0;
padding:0; padding:0;
height:100%; height:100%;
@ -71,37 +71,33 @@ a[name] {
color:#353833; color:#353833;
} }
pre { pre {
font-family: "DejaVu Sans Mono", monospace; font-family:'DejaVu Sans Mono', monospace;
} }
h1 { h1 {
font-family: "DejaVu Sans", sans;
font-size:20px; font-size:20px;
} }
h2 { h2 {
font-family: "DejaVu Sans", sans;
font-size:18px; font-size:18px;
} }
h3 { h3 {
font-family: "DejaVu Sans", sans; font-size:17px;
font-size:16px;
} }
h4 { h4 {
font-family: "DejaVu Sans", sans; font-size:16px;
font-size:15px; margin-top: 1rem;
margin-bottom: 1rem;
} }
h5 { h5 {
font-family: "DejaVu Sans", sans;
font-size:14px; font-size:14px;
} }
h6 { h6 {
font-family: "DejaVu Sans", sans;
font-size:13px; font-size:13px;
} }
ul { ul {
list-style-type:disc; list-style-type:disc;
} }
code, tt { code, tt {
font-family: "DejaVu Sans Mono", monospace; font-family:'DejaVu Sans Mono', monospace;
} }
:not(h1, h2, h3, h4, h5, h6) > code, :not(h1, h2, h3, h4, h5, h6) > code,
:not(h1, h2, h3, h4, h5, h6) > tt { :not(h1, h2, h3, h4, h5, h6) > tt {
@ -111,12 +107,12 @@ code, tt {
line-height:1.4em; line-height:1.4em;
} }
dt code { dt code {
font-family: "DejaVu Sans Mono", monospace; font-family:'DejaVu Sans Mono', monospace;
font-size:14px; font-size:14px;
padding-top:4px; padding-top:4px;
} }
.summary-table dt code { .summary-table dt code {
font-family: "DejaVu Sans Mono", monospace; font-family:'DejaVu Sans Mono', monospace;
font-size:14px; font-size:14px;
vertical-align:top; vertical-align:top;
padding-top:4px; padding-top:4px;
@ -124,7 +120,9 @@ dt code {
sup { sup {
font-size:8px; font-size:8px;
} }
button {
font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif;
}
/* /*
* Styles for HTML generated by javadoc. * Styles for HTML generated by javadoc.
* *
@ -185,7 +183,6 @@ sup {
min-height:2.8em; min-height:2.8em;
padding-top:10px; padding-top:10px;
overflow:hidden; overflow:hidden;
font-family: "DejaVu Sans", sans;
font-size:80%; font-size:80%;
} }
.sub-nav { .sub-nav {
@ -193,7 +190,6 @@ sup {
float:left; float:left;
width:100%; width:100%;
overflow:hidden; overflow:hidden;
font-family: "DejaVu Sans", sans;
font-size:80%; font-size:80%;
} }
.sub-nav div { .sub-nav div {
@ -311,13 +307,16 @@ main {
position:relative; position:relative;
} }
dl.notes > dt { dl.notes > dt {
font-family: "DejaVu Sans", sans; font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif;
/* font-size:12px; */
font-weight:bold; font-weight:bold;
margin:10px 0 0 0; margin:10px 0 0 0;
color:#4E4E4E; color:#4E4E4E;
} }
dl.notes > dd { dl.notes > dd {
margin:5px 10px 10px 0; margin:5px 10px 0 0;
/* font-size:14px; */
font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
} }
dl.name-value > dt { dl.name-value > dt {
margin-left:1px; margin-left:1px;
@ -389,6 +388,11 @@ ul.see-list-long li:not(:last-child):after {
border-bottom:1px solid #EEE; border-bottom:1px solid #EEE;
padding:0; padding:0;
} }
.summary-table .col-first {
font-family: "DejaVu Sans Mono", monospace;
}
.caption { .caption {
position:relative; position:relative;
text-align:left; text-align:left;
@ -402,7 +406,6 @@ ul.see-list-long li:not(:last-child):after {
padding-left:1px; padding-left:1px;
margin:0; margin:0;
white-space:pre; white-space:pre;
font-family: 'DejaVu Sans';
} }
.caption a:link, .caption a:visited { .caption a:link, .caption a:visited {
color:#1f389c; color:#1f389c;
@ -450,9 +453,6 @@ div.table-tabs > button.table-tab {
display: grid; display: grid;
grid-template-columns: minmax(10%, max-content) minmax(15%, max-content) minmax(15%, auto); grid-template-columns: minmax(10%, max-content) minmax(15%, max-content) minmax(15%, auto);
} }
#method-summary-table .three-column-summary {
grid-template-columns: minmax(10%, 20%) minmax(15%, max-content) minmax(15%, auto);
}
.four-column-summary { .four-column-summary {
display: grid; display: grid;
grid-template-columns: minmax(10%, max-content) minmax(10%, max-content) minmax(10%, max-content) minmax(10%, auto); grid-template-columns: minmax(10%, max-content) minmax(10%, max-content) minmax(10%, max-content) minmax(10%, auto);
@ -490,7 +490,6 @@ div.table-tabs > button.table-tab {
} }
.table-header { .table-header {
background:#dee3e9; background:#dee3e9;
font-family: 'DejaVu Sans';
font-weight: bold; font-weight: bold;
} }
/* /*
@ -508,7 +507,6 @@ div.table-tabs > button.table-tab {
.col-last { .col-last {
white-space:normal; white-space:normal;
} }
/*
.col-first a:link, .col-first a:visited, .col-first a:link, .col-first a:visited,
.col-second a:link, .col-second a:visited, .col-second a:link, .col-second a:visited,
.col-first a:link, .col-first a:visited, .col-first a:link, .col-first a:visited,
@ -520,7 +518,6 @@ div.table-tabs > button.table-tab {
.all-packages-container a:link, .all-packages-container a:visited { .all-packages-container a:link, .all-packages-container a:visited {
font-weight:bold; font-weight:bold;
} }
*/
.table-sub-heading-color { .table-sub-heading-color {
background-color:#EEEEFF; background-color:#EEEEFF;
} }
@ -537,12 +534,9 @@ div.table-tabs > button.table-tab {
margin:0; margin:0;
padding:10px 0; padding:10px 0;
} }
/*
div.block { div.block {
font-size:14px;
font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif; font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
} }
*/
.col-last div { .col-last div {
padding-top:0; padding-top:0;
} }
@ -553,8 +547,7 @@ div.block {
.package-signature, .package-signature,
.type-signature, .type-signature,
.member-signature { .member-signature {
font-family: "DejaVu Sans Mono", monospace; font-family:'DejaVu Sans Mono', monospace;
/* font-size:14px; */
margin:14px 0; margin:14px 0;
white-space: pre-wrap; white-space: pre-wrap;
} }
@ -593,13 +586,8 @@ h1.hidden {
.deprecated-label, .descfrm-type-label, .implementation-label, .member-name-label, .member-name-link, .deprecated-label, .descfrm-type-label, .implementation-label, .member-name-label, .member-name-link,
.module-label-in-package, .module-label-in-type, .override-specify-label, .package-label-in-type, .module-label-in-package, .module-label-in-type, .override-specify-label, .package-label-in-type,
.package-hierarchy-label, .type-name-label, .type-name-link, .search-tag-link, .preview-label { .package-hierarchy-label, .type-name-label, .type-name-link, .search-tag-link, .preview-label {
font-family: "DejaVu Sans", sans;
font-weight:bold; font-weight:bold;
} }
.sub-title, .inheritance, .all-packages-table-tab1.col-first,
.summary-table .col-first {
font-family: "DejaVu Sans", sans;
}
.deprecation-comment, .help-footnote, .preview-comment { .deprecation-comment, .help-footnote, .preview-comment {
font-style:italic; font-style:italic;
} }
@ -658,6 +646,7 @@ main, nav, header, footer, section {
ul.ui-autocomplete { ul.ui-autocomplete {
position:fixed; position:fixed;
z-index:999999; z-index:999999;
background-color: #FFFFFF;
} }
ul.ui-autocomplete li { ul.ui-autocomplete li {
float:left; float:left;
@ -667,6 +656,9 @@ ul.ui-autocomplete li {
.result-highlight { .result-highlight {
font-weight:bold; font-weight:bold;
} }
.ui-autocomplete .result-item {
font-size: inherit;
}
#search-input { #search-input {
background-image:url('resources/glass.png'); background-image:url('resources/glass.png');
background-size:13px; background-size:13px;

View file

@ -41,7 +41,8 @@ public class VmDefinitionModel extends K8sDynamicModel {
* Permissions for accessing and manipulating the VM. * Permissions for accessing and manipulating the VM.
*/ */
public enum Permission { public enum Permission {
START("start"), STOP("stop"), ACCESS_CONSOLE("accessConsole"); START("start"), STOP("stop"), RESET("reset"),
ACCESS_CONSOLE("accessConsole");
@SuppressWarnings("PMD.UseConcurrentHashMap") @SuppressWarnings("PMD.UseConcurrentHashMap")
private static Map<String, Permission> reprs = new HashMap<>(); private static Map<String, Permission> reprs = new HashMap<>();

View file

@ -0,0 +1,48 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager.events;
import org.jgrapes.core.Event;
/**
* Triggers a reset of the VM.
*/
@SuppressWarnings("PMD.DataClass")
public class ResetVm extends Event<String> {
private final String vmName;
/**
* Instantiates a new event.
*
* @param vmName the vm name
*/
public ResetVm(String vmName) {
this.vmName = vmName;
}
/**
* Gets the vm name.
*
* @return the vm name
*/
public String vmName() {
return vmName;
}
}

View file

@ -18,10 +18,10 @@ dependencies {
implementation 'org.jgrapes:org.jgrapes.http:[3.1.0,4)' implementation 'org.jgrapes:org.jgrapes.http:[3.1.0,4)'
implementation 'org.jgrapes:org.jgrapes.util:[1.34.0,2)' implementation 'org.jgrapes:org.jgrapes.util:[1.34.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.5.0,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.7.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.5.0,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.5.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.3.0,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.3.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.3.0,2)' implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.4.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconlet.markdowndisplay:[1.2.0,2)' implementation 'org.jgrapes:org.jgrapes.webconlet.markdowndisplay:[1.2.0,2)'
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.4.0,2)' runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.4.0,2)'

View file

@ -1,5 +1,5 @@
You can use the "puzzle piece" icon on the top right corner of the You can use the "puzzle piece" icon on the top right corner of the
page to add display widgets (conlets) to the overview tab. page to add display widgets (conlets) to the overview tab.
Use the "full screen" icon on the top right corner of any Use the "full screen" icon on the top right corner of any
conlet (if available) to get a detailed view. conlet (if available) to get a detailed view.

View file

@ -1,4 +1,4 @@
Verwenden Sie das "Puzzle"-Icon auf der rechten oberen Ecke Verwenden Sie das "Puzzle"-Icon auf der rechten oberen Ecke
der Seite, um Anzeige-Widgets (Conlets) hinzuzufügen. der Seite, um Anzeige-Widgets (Conlets) hinzuzufügen.
Wenn sich in der rechten oberen Ecke eines Conlets ein Vollbild-Icon Wenn sich in der rechten oberen Ecke eines Conlets ein Vollbild-Icon

View file

@ -48,6 +48,12 @@ data:
# Whether a shutdown initiated by the guest stops the pod deployment # Whether a shutdown initiated by the guest stops the pod deployment
guestShutdownStops: ${ cr.spec.guestShutdownStops!false?c } guestShutdownStops: ${ cr.spec.guestShutdownStops!false?c }
# When incremented, the VM is reset. The value has no default value,
# i.e. if you start the VM without a value for this property, and
# decide to trigger a reset later, you have to first set the value
# and then inrement it.
resetCounter: ${ cr.resetCount }
# Forward the cloud-init data if provided # Forward the cloud-init data if provided
<#if cr.spec.cloudInit??> <#if cr.spec.cloudInit??>
cloudInit: cloudInit:

View file

@ -36,7 +36,6 @@ import org.jdrupes.vmoperator.common.K8s;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.constructor.SafeConstructor;
@ -62,7 +61,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Reconcile. * Reconcile.
* *
* @param event the event
* @param model the model * @param model the model
* @param channel the channel * @param channel the channel
* @return the dynamic kubernetes object * @return the dynamic kubernetes object
@ -70,8 +68,8 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
* @throws TemplateException the template exception * @throws TemplateException the template exception
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public DynamicKubernetesObject reconcile(VmDefChanged event, public DynamicKubernetesObject reconcile(Map<String, Object> model,
Map<String, Object> model, VmChannel channel) VmChannel channel)
throws IOException, TemplateException, ApiException { throws IOException, TemplateException, ApiException {
// Get API // Get API
DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1",

View file

@ -181,13 +181,12 @@ public class Controller extends Component {
@Handler @Handler
public void onModifyVm(ModifyVm event, VmChannel channel) public void onModifyVm(ModifyVm event, VmChannel channel)
throws ApiException, IOException { throws ApiException, IOException {
patchVmSpec(channel.client(), event.name(), event.path(), patchVmDef(channel.client(), event.name(), "spec/vm/" + event.path(),
event.value()); event.value());
} }
private void patchVmSpec(K8sClient client, String name, String path, private void patchVmDef(K8sClient client, String name, String path,
Object value) Object value) throws ApiException, IOException {
throws ApiException, IOException {
var vmStub = K8sDynamicStub.get(client, var vmStub = K8sDynamicStub.get(client,
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), namespace, new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), namespace,
name); name);
@ -197,7 +196,7 @@ public class Controller extends Component {
? "\"" + value + "\"" ? "\"" + value + "\""
: value.toString(); : value.toString();
var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/" new V1Patch("[{\"op\": \"replace\", \"path\": \"/"
+ path + "\", \"value\": " + valueAsText + "}]"), + path + "\", \"value\": " + valueAsText + "}]"),
client.defaultPatchOptions()); client.defaultPatchOptions());
if (!res.isPresent()) { if (!res.isPresent()) {

View file

@ -33,6 +33,7 @@ import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Scanner; import java.util.Scanner;
import java.util.logging.Level; import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
@ -180,7 +181,8 @@ public class DisplaySecretMonitor
// Check validity // Check validity
var model = stub.model().get(); var model = stub.model().get();
@SuppressWarnings("PMD.StringInstantiation") @SuppressWarnings("PMD.StringInstantiation")
var expiry = new String(model.getData().get(DATA_PASSWORD_EXPIRY)); var expiry = Optional.ofNullable(model.getData()
.get(DATA_PASSWORD_EXPIRY)).map(b -> new String(b)).orElse(null);
if (model.getData().get(DATA_DISPLAY_PASSWORD) != null if (model.getData().get(DATA_DISPLAY_PASSWORD) != null
&& stillValid(expiry)) { && stillValid(expiry)) {
event.setResult( event.setResult(

View file

@ -64,7 +64,7 @@ import org.jose4j.base64url.Base64;
var display = GsonPtr.to(event.vmDefinition().data()).to("spec", "vm", var display = GsonPtr.to(event.vmDefinition().data()).to("spec", "vm",
"display"); "display");
if (!display.get(JsonPrimitive.class, "spice", "generateSecret") if (!display.get(JsonPrimitive.class, "spice", "generateSecret")
.map(JsonPrimitive::getAsBoolean).orElse(false)) { .map(JsonPrimitive::getAsBoolean).orElse(true)) {
return; return;
} }

View file

@ -51,6 +51,7 @@ import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.K8sV1SecretStub; import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET; import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
import org.jdrupes.vmoperator.manager.events.ResetVm;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper; import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
@ -209,13 +210,35 @@ public class Reconciler extends Component {
// Reconcile, use "augmented" vm definition for model // Reconcile, use "augmented" vm definition for model
Map<String, Object> model Map<String, Object> model
= prepareModel(channel.client(), patchCr(event.vmDefinition())); = prepareModel(channel.client(), patchCr(event.vmDefinition()));
var configMap = cmReconciler.reconcile(event, model, channel); var configMap = cmReconciler.reconcile(model, channel);
model.put("cm", configMap.getRaw()); model.put("cm", configMap.getRaw());
dsReconciler.reconcile(event, model, channel); dsReconciler.reconcile(event, model, channel);
stsReconciler.reconcile(event, model, channel); stsReconciler.reconcile(event, model, channel);
lbReconciler.reconcile(event, model, channel); lbReconciler.reconcile(event, model, channel);
} }
/**
* Reset the VM by incrementing the reset count and doing a
* partial reconcile (configmap only).
*
* @param event the event
* @param channel the channel
* @throws IOException
* @throws ApiException
* @throws TemplateException
*/
@Handler
public void onResetVm(ResetVm event, VmChannel channel)
throws ApiException, IOException, TemplateException {
var defRoot
= GsonPtr.to(channel.vmDefinition().data()).get(JsonObject.class);
defRoot.addProperty("resetCount",
defRoot.get("resetCount").getAsLong() + 1);
Map<String, Object> model
= prepareModel(channel.client(), patchCr(channel.vmDefinition()));
cmReconciler.reconcile(model, channel);
}
private DynamicKubernetesObject patchCr(K8sDynamicModel vmDef) { private DynamicKubernetesObject patchCr(K8sDynamicModel vmDef) {
var json = vmDef.data().deepCopy(); var json = vmDef.data().deepCopy();
// Adjust cdromImage path // Adjust cdromImage path

View file

@ -25,13 +25,13 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Watch; import io.kubernetes.client.util.Watch;
import io.kubernetes.client.util.generic.options.ListOptions; import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sDynamicStub; import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub; import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
@ -121,7 +121,7 @@ public class VmMonitor extends
} }
if (vmDef.data() != null) { if (vmDef.data() != null) {
// New data, augment and save // New data, augment and save
addDynamicData(channel.client(), vmDef); addDynamicData(channel.client(), vmDef, channel.vmDefinition());
channel.setVmDefinition(vmDef); channel.setVmDefinition(vmDef);
} else { } else {
// Reuse cached // Reuse cached
@ -151,8 +151,16 @@ public class VmMonitor extends
} }
} }
private void addDynamicData(K8sClient client, K8sDynamicModel vmState) { private void addDynamicData(K8sClient client, VmDefinitionModel vmState,
VmDefinitionModel prevState) {
var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class); var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class);
// Maintain (or initialize) the resetCount
rootNode.addProperty("resetCount", Optional.ofNullable(prevState)
.map(ps -> GsonPtr.to(ps.data()))
.flatMap(d -> d.getAsLong("resetCount")).orElse(0L));
// Add defaults in case the VM is not running
rootNode.addProperty("nodeName", ""); rootNode.addProperty("nodeName", "");
rootNode.addProperty("nodeAddress", ""); rootNode.addProperty("nodeAddress", "");

View file

@ -57,7 +57,7 @@
* ``` * ```
* *
* Developers may also be interested in the usage of channels * Developers may also be interested in the usage of channels
* by the application's component: * by the application's components:
* *
* ![Main channels](app-channels.svg) * ![Main channels](app-channels.svg)
* *
@ -74,6 +74,8 @@
* *
* Component NioDispatcher as NioDispatcher <<internal>> * Component NioDispatcher as NioDispatcher <<internal>>
* [Manager] *-up- [NioDispatcher] * [Manager] *-up- [NioDispatcher]
* Component HttpConnector as HttpConnector <<internal>>
* [Manager] *-up- [HttpConnector]
* Component FileSystemWatcher as FileSystemWatcher <<internal>> * Component FileSystemWatcher as FileSystemWatcher <<internal>>
* [Manager] *-up- [FileSystemWatcher] * [Manager] *-up- [FileSystemWatcher]
* Component YamlConfigurationStore as YamlConfigurationStore <<internal>> * Component YamlConfigurationStore as YamlConfigurationStore <<internal>>
@ -119,6 +121,7 @@
* [WebConsole] *-- [RoleConfigurator] * [WebConsole] *-- [RoleConfigurator]
* [WebConsole] *-- [RoleConletFilter] * [WebConsole] *-- [RoleConletFilter]
* [WebConsole] *-left- [LoginConlet] * [WebConsole] *-left- [LoginConlet]
* [WebConsole] *-right- [OidcClient]
* *
* Component "ComponentCollector\nfor page resources" as cpr <<internal>> * Component "ComponentCollector\nfor page resources" as cpr <<internal>>
* [WebConsole] *-- [cpr] * [WebConsole] *-- [cpr]
@ -147,21 +150,35 @@
* () "guiTransport" as hT * () "guiTransport" as hT
* hT .up. [GuiSocketServer:8080] * hT .up. [GuiSocketServer:8080]
* hT .down. [GuiHttpServer] * hT .down. [GuiHttpServer]
* hT .right[hidden]. [HttpConnector]
* *
* [YamlConfigurationStore] -right[hidden]- hT * [YamlConfigurationStore] -right[hidden]- hT
* *
* () "guiHttp" as http * () "guiHttp" as http
* http .up. [GuiHttpServer] * http .up. [GuiHttpServer]
* http .up. [HttpConnector]
* note top of [HttpConnector]: transport layer com-\nponents omitted
* *
* [PreferencesStore] .right. http * [PreferencesStore] .. http
* [OidcClient] .up. http
* [LanguageSelector] .left. http
* [InMemorySessionManager] .up. http * [InMemorySessionManager] .up. http
* [LanguageSelector] .up. http
* *
* package "Conceptual WebConsole" { * package "Conceptual WebConsole" {
* [ConsoleWeblet] .left. http * [ConsoleWeblet] .right. http
* [ConsoleWeblet] *-down- [WebConsole] * [ConsoleWeblet] *-down- [WebConsole]
* } * }
* *
* [Controller] .down[hidden]. [ConsoleWeblet]
*
* () "console" as console
* console .. WebConsole
*
* [OidcClient] .. console
* [LoginConlet] .right. console
*
* note right of console: More conlets\nconnect here
*
* @enduml * @enduml
*/ */
package org.jdrupes.vmoperator.manager; package org.jdrupes.vmoperator.manager;

View file

@ -45,6 +45,12 @@
# property in the CRD. # property in the CRD.
# "guestShutdownStops": # "guestShutdownStops":
# false # false
# When incremented, the VM is reset. The value has no default value,
# i.e. if you start the VM without a value for this property, and
# decide to trigger a reset later, you have to first set the value
# and then inrement it.
# "resetCounter": 1
# Define the VM (required) # Define the VM (required)
"vm": "vm":

View file

@ -82,6 +82,9 @@ public class Configuration implements Dto {
/** If guest shutdown changes CRD .vm.state to "Stopped". */ /** If guest shutdown changes CRD .vm.state to "Stopped". */
public boolean guestShutdownStops; public boolean guestShutdownStops;
/** Increments of the reset counter trigger a reset of the VM. */
public Integer resetCounter;
/** The vm. */ /** The vm. */
@SuppressWarnings("PMD.ShortVariable") @SuppressWarnings("PMD.ShortVariable")
public Vm vm; public Vm vm;

View file

@ -116,8 +116,9 @@ public class DisplayController extends Component {
} }
if (Objects.equals(this.currentPassword, password)) { if (Objects.equals(this.currentPassword, password)) {
return false; return true;
} }
this.currentPassword = password;
logger.fine(() -> "Updating display password"); logger.fine(() -> "Updating display password");
fire(new MonitorCommand(new QmpSetDisplayPassword(protocol, password))); fire(new MonitorCommand(new QmpSetDisplayPassword(protocol, password)));
return true; return true;

View file

@ -55,6 +55,7 @@ import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options; import org.apache.commons.cli.Options;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCont; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCont;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpReset;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.Exit; import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
@ -215,6 +216,7 @@ public class Runner extends Component {
private CommandDefinition cloudInitImgDefinition; private CommandDefinition cloudInitImgDefinition;
private CommandDefinition qemuDefinition; private CommandDefinition qemuDefinition;
private final QemuMonitor qemuMonitor; private final QemuMonitor qemuMonitor;
private Integer resetCounter;
private State state = State.INITIALIZING; private State state = State.INITIALIZING;
/** Preparatory actions for QEMU start */ /** Preparatory actions for QEMU start */
@ -615,7 +617,7 @@ public class Runner extends Component {
* @param event the event * @param event the event
*/ */
@Handler(priority = -1000) @Handler(priority = -1000)
public void onConfigureQemu(ConfigureQemu event) { public void onConfigureQemuFinal(ConfigureQemu event) {
if (state == State.STARTING) { if (state == State.STARTING) {
fire(new MonitorCommand(new QmpCont())); fire(new MonitorCommand(new QmpCont()));
state = State.RUNNING; state = State.RUNNING;
@ -624,6 +626,23 @@ public class Runner extends Component {
} }
} }
/**
* On configure qemu.
*
* @param event the event
*/
@Handler
public void onConfigureQemu(ConfigureQemu event) {
if (state == State.RUNNING) {
if (resetCounter != null
&& event.configuration().resetCounter != null
&& event.configuration().resetCounter > resetCounter) {
fire(new MonitorCommand(new QmpReset()));
}
resetCounter = event.configuration().resetCounter;
}
}
/** /**
* On process exited. * On process exited.
* *

View file

@ -0,0 +1,43 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.runner.qemu.commands;
import com.fasterxml.jackson.databind.JsonNode;
/**
* A {@link QmpCommand} that send a system_reset to the VM.
*/
public class QmpReset extends QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
private static final JsonNode jsonTemplate
= parseJson("{ \"execute\": \"system_reset\" }");
@Override
public JsonNode toJson() {
return jsonTemplate.deepCopy();
}
@Override
public String toString() {
return "QmpReset()";
}
}

View file

@ -265,6 +265,18 @@ public class GsonPtr {
return set(selector, new JsonPrimitive(value)); return set(selector, new JsonPrimitive(value));
} }
/**
* Short for `set(selector, new JsonPrimitive(value))`.
*
* @param selector the selector
* @param value the value
* @return the gson ptr
* @see #set(Object, JsonElement)
*/
public GsonPtr set(Object selector, Long value) {
return set(selector, new JsonPrimitive(value));
}
/** /**
* Short for `set(selector, new JsonPrimitive(value))`. * Short for `set(selector, new JsonPrimitive(value))`.
* *

View file

@ -52,12 +52,14 @@
v-html="controller.breakBeforeDots(entry[key])"></span> v-html="controller.breakBeforeDots(entry[key])"></span>
</td> </td>
<td class="jdrupes-vmoperator-vmconlet-view-action-list"> <td class="jdrupes-vmoperator-vmconlet-view-action-list">
<span role="button" v-if="entry.spec.vm.state != 'Running'" <span role="button"
v-if="entry.spec.vm.state != 'Running' && !entry['running']"
tabindex="0" class="fa fa-play" :title="localize('Start VM')" tabindex="0" class="fa fa-play" :title="localize('Start VM')"
v-on:click="vmAction(entry.name, 'start')"></span> v-on:click="vmAction(entry.name, 'start')"></span>
<span role="button" v-else class="fa fa-play" <span role="button" v-else class="fa fa-play"
aria-disabled="true" :title="localize('Start VM')"></span> aria-disabled="true" :title="localize('Start VM')"></span>
<span role="button" v-if="entry.spec.vm.state != 'Stopped'" <span role="button"
v-if="entry.spec.vm.state != 'Stopped' && entry['running']"
tabindex="0" class="fa fa-stop" :title="localize('Stop VM')" tabindex="0" class="fa fa-stop" :title="localize('Stop VM')"
v-on:click="vmAction(entry.name, 'stop')"></span> v-on:click="vmAction(entry.name, 'stop')"></span>
<span role="button" v-else class="fa fa-stop" <span role="button" v-else class="fa fa-stop"

View file

@ -5,7 +5,7 @@ plugins {
dependencies { dependencies {
implementation project(':org.jdrupes.vmoperator.manager.events') implementation project(':org.jdrupes.vmoperator.manager.events')
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.3.0,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.7.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)'

View file

@ -0,0 +1,13 @@
<div
class="jdrupes-vmoperator-vmviewer jdrupes-vmoperator-vmviewer-confirm-reset">
<p>${_("confirmResetMsg")}</p>
<p>
<span role="button" tabindex="0" class="svg-icon"
onclick="orgJDrupesVmOperatorVmViewer.confirmReset('${conletType}', '${conletId}')">
<svg viewBox="0 0 1541.33 1535.5083">
<path d="m 0,127.9968 v 448 c 0,35 29,64 64,64 h 448 c 35,0 64,-29 64,-64 0,-17 -6.92831,-33.07213 -19,-45 C 264.23058,241.7154 337.19508,314.89599 109,82.996795 c -11.999999,-12 -28,-19 -45,-19 -35,0 -64,29 -64,64.000005 z" />
<path d="m 772.97656,1535.5046 c 117.57061,0.3623 236.06134,-26.2848 345.77544,-81.4687 292.5708,-147.1572 459.8088,-465.37411 415.5214,-790.12504 C 1489.9861,339.15993 1243.597,77.463924 922.29883,14.342498 601.00067,-48.778928 274.05699,100.37563 110.62891,384.39133 c -34.855139,60.57216 -14.006492,137.9313 46.5664,172.78516 60.57172,34.85381 137.92941,14.00532 172.78321,-46.56641 109.97944,-191.12927 327.69604,-290.34657 543.53515,-247.94336 215.83913,42.40321 380.18953,216.77543 410.00973,435.44141 29.8203,218.66598 -81.8657,430.94957 -278.4863,529.84567 -196.6206,98.8962 -432.84043,61.8202 -589.90233,-92.6777 -24.91016,-24.5038 -85.48587,-83.3326 -119.02246,-52.9832 -24.01114,21.7292 -35.41741,29.5454 -59.9209,54.4559 -24.50381,24.9102 -35.33636,36.9034 -57.54543,60.4713 -38.1335,40.4667 34.10761,93.9685 59.01808,118.472 145.96311,143.5803 339.36149,219.2087 535.3125,219.8125 z"/>
</svg>
</span>
</p>
</div>

View file

@ -1,7 +1,8 @@
<div title="${_("conletName")}" class="jdrupes-vmoperator-vmviewer-edit" <div title="${_("conletName")}"
data-jgwc-on-load="orgJDrupesVmOperatorVmViewer.initEdit" class="jdrupes-vmoperator-vmviewer jdrupes-vmoperator-vmviewer-edit"
data-jgwc-on-action="orgJDrupesVmOperatorVmViewer.applyEdit" data-jgwc-on-load="orgJDrupesVmOperatorVmViewer.initEdit"
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps"> data-jgwc-on-action="orgJDrupesVmOperatorVmViewer.applyEdit"
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps">
<form :id="formId" ref="formDom" onsubmit="return false;"> <form :id="formId" ref="formDom" onsubmit="return false;">
<section> <section>
<span>{{ localize("Select VM") }}</span> <span>{{ localize("Select VM") }}</span>

View file

@ -1,4 +1,5 @@
<div class="jdrupes-vmoperator-vmviewer jdrupes-vmoperator-vmviewer-preview" <div
class="jdrupes-vmoperator-vmviewer jdrupes-vmoperator-vmviewer-preview"
data-conlet-grid-rows="2" data-conlet-grid-columns="2" data-conlet-grid-rows="2" data-conlet-grid-columns="2"
data-jgwc-on-load="orgJDrupesVmOperatorVmViewer.initPreview" data-jgwc-on-load="orgJDrupesVmOperatorVmViewer.initPreview"
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps" data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps"

View file

@ -1,3 +1,7 @@
conletName = VM Console conletName = VM Console
okayLabel = Apply and Close okayLabel = Apply and Close
confirmResetTitle = Confirm reset
confirmResetMsg = Resetting the VM may cause loss of data. \
Please confirm to continue.

View file

@ -5,4 +5,9 @@ Select\ VM = VM ausw
Start\ VM = VM starten Start\ VM = VM starten
Stop\ VM = VM anhalten Stop\ VM = VM anhalten
Reset\ VM = VM zurücksetzen
Open\ console = Konsole anzeigen Open\ console = Konsole anzeigen
confirmResetTitle = Zurücksetzen bestätigen
confirmResetMsg = Zurücksetzen der VM kann zu Datenverlust führen. \
Bitte bestätigen um fortzufahren.

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1541.33"
height="1535.5083"
version="1.1"
id="svg1"
sodipodi:docname="reset-icon2.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.34987054"
inkscape:cx="704.54631"
inkscape:cy="711.69181"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 0,127.9968 v 448 c 0,35 29,64 64,64 h 448 c 35,0 64,-29 64,-64 0,-17 -6.92831,-33.07213 -19,-45 C 264.23058,241.7154 337.19508,314.89599 109,82.996795 c -11.999999,-12 -28,-19 -45,-19 -35,0 -64,29 -64,64.000005 z"
id="path1"
sodipodi:nodetypes="sssssscss" />
<path
style="color:#000000;fill:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 772.97656,1535.5046 c 117.57061,0.3623 236.06134,-26.2848 345.77544,-81.4687 292.5708,-147.1572 459.8088,-465.37411 415.5214,-790.12504 C 1489.9861,339.15993 1243.597,77.463924 922.29883,14.342498 601.00067,-48.778928 274.05699,100.37563 110.62891,384.39133 c -34.855139,60.57216 -14.006492,137.9313 46.5664,172.78516 60.57172,34.85381 137.92941,14.00532 172.78321,-46.56641 109.97944,-191.12927 327.69604,-290.34657 543.53515,-247.94336 215.83913,42.40321 380.18953,216.77543 410.00973,435.44141 29.8203,218.66598 -81.8657,430.94957 -278.4863,529.84567 -196.6206,98.8962 -432.84043,61.8202 -589.90233,-92.6777 -24.91016,-24.5038 -85.48587,-83.3326 -119.02246,-52.9832 -24.01114,21.7292 -35.41741,29.5454 -59.9209,54.4559 -24.50381,24.9102 -35.33636,36.9034 -57.54543,60.4713 -38.1335,40.4667 34.10761,93.9685 59.01808,118.472 145.96311,143.5803 339.36149,219.2087 535.3125,219.8125 z"
id="path2"
sodipodi:nodetypes="sssscccssscscscs" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -20,6 +20,7 @@ package org.jdrupes.vmoperator.vmviewer;
import com.fasterxml.jackson.annotation.JsonGetter; import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
@ -54,6 +55,7 @@ import org.jdrupes.vmoperator.common.VmDefinitionModel.Permission;
import org.jdrupes.vmoperator.manager.events.ChannelCache; import org.jdrupes.vmoperator.manager.events.ChannelCache;
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword; import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
import org.jdrupes.vmoperator.manager.events.ModifyVm; import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.ResetVm;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.GsonPtr; import org.jdrupes.vmoperator.util.GsonPtr;
@ -90,10 +92,10 @@ import org.jgrapes.webconsole.base.events.UpdateConletType;
import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
/** /**
* The Class VmConlet. * The Class VmViewer.
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports", @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
"PMD.CouplingBetweenObjects", "PMD.GodClass" }) "PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods" })
public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> { public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
private static final String VM_NAME_PROPERTY = "vmName"; private static final String VM_NAME_PROPERTY = "vmName";
@ -465,12 +467,19 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
@Override @Override
@SuppressWarnings({ "PMD.AvoidDecimalLiteralsInBigDecimalConstructor", @SuppressWarnings({ "PMD.AvoidDecimalLiteralsInBigDecimalConstructor",
"PMD.ConfusingArgumentToVarargsMethod" }) "PMD.ConfusingArgumentToVarargsMethod", "PMD.NcssCount",
"PMD.AvoidLiteralsInIfCondition" })
protected void doUpdateConletState(NotifyConletModel event, protected void doUpdateConletState(NotifyConletModel event,
ConsoleConnection channel, ViewerModel model) ConsoleConnection channel, ViewerModel model)
throws Exception { throws Exception {
event.stop(); event.stop();
var both = Optional.ofNullable(event.params().asString(0)) if ("selectedVm".equals(event.method())) {
selectVm(event, channel, model);
return;
}
// Handle command for selected VM
var both = Optional.ofNullable(model.vmName())
.flatMap(vm -> channelManager.both(vm)); .flatMap(vm -> channelManager.both(vm));
if (both.isEmpty()) { if (both.isEmpty()) {
return; return;
@ -479,14 +488,8 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
var vmDef = both.get().associated; var vmDef = both.get().associated;
var vmName = vmDef.metadata().getName(); var vmName = vmDef.metadata().getName();
var perms = permissions(vmDef, channel.session()); var perms = permissions(vmDef, channel.session());
var resourceBundle = resourceBundle(channel.locale());
switch (event.method()) { switch (event.method()) {
case "selectedVm":
model.setVmName(event.params().asString(0));
String jsonState = objectMapper.writeValueAsString(model);
channel.respond(new KeyValueStoreUpdate().update(storagePath(
channel.session(), model.getConletId()), jsonState));
updateConfig(channel, model);
break;
case "start": case "start":
if (perms.contains(Permission.START)) { if (perms.contains(Permission.START)) {
fire(new ModifyVm(vmName, "state", "Running", vmChannel)); fire(new ModifyVm(vmName, "state", "Running", vmChannel));
@ -497,6 +500,16 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel)); fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
} }
break; break;
case "reset":
if (perms.contains(Permission.RESET)) {
confirmReset(event, channel, model, resourceBundle);
}
break;
case "resetConfirmed":
if (perms.contains(Permission.RESET)) {
fire(new ResetVm(vmName), vmChannel);
}
break;
case "openConsole": case "openConsole":
if (perms.contains(Permission.ACCESS_CONSOLE)) { if (perms.contains(Permission.ACCESS_CONSOLE)) {
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef), var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef),
@ -510,6 +523,15 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
} }
} }
private void selectVm(NotifyConletModel event, ConsoleConnection channel,
ViewerModel model) throws JsonProcessingException {
model.setVmName(event.params().asString(0));
String jsonState = objectMapper.writeValueAsString(model);
channel.respond(new KeyValueStoreUpdate().update(storagePath(
channel.session(), model.getConletId()), jsonState));
updateConfig(channel, model);
}
private void openConsole(String vmName, ConsoleConnection connection, private void openConsole(String vmName, ConsoleConnection connection,
ViewerModel model, String password) { ViewerModel model, String password) {
var vmDef = channelManager.associated(vmName).orElse(null); var vmDef = channelManager.associated(vmName).orElse(null);
@ -577,6 +599,20 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
.findFirst().or(() -> addrs.stream().findFirst()); .findFirst().or(() -> addrs.stream().findFirst());
} }
private void confirmReset(NotifyConletModel event,
ConsoleConnection channel, ViewerModel model,
ResourceBundle resourceBundle) throws TemplateNotFoundException,
MalformedTemplateNameException, ParseException, IOException {
Template tpl = freemarkerConfig()
.getTemplate("VmViewer-confirmReset.ftl.html");
channel.respond(new OpenModalDialog(type(), model.getConletId(),
processTemplate(event, tpl,
fmModel(event, channel, model.getConletId(), model)))
.addOption("cancelable", true).addOption("closeLabel", "")
.addOption("title",
resourceBundle.getString("confirmResetTitle")));
}
@Override @Override
protected boolean doSetLocale(SetLocale event, ConsoleConnection channel, protected boolean doSetLocale(SetLocale event, ConsoleConnection channel,
String conletId) throws Exception { String conletId) throws Exception {

View file

@ -31,8 +31,9 @@ declare global {
interface Window { interface Window {
orgJDrupesVmOperatorVmViewer: { orgJDrupesVmOperatorVmViewer: {
initPreview?: (previewDom: HTMLElement, isUpdate: boolean) => void, initPreview?: (previewDom: HTMLElement, isUpdate: boolean) => void,
initEdit?: (viewDom: HTMLElement, isUpdate: boolean) => void initEdit?: (viewDom: HTMLElement, isUpdate: boolean) => void,
applyEdit?: (viewDom: HTMLElement, apply: boolean) => void applyEdit?: (viewDom: HTMLElement, apply: boolean) => void,
confirmReset?: (conletType: string, conletId: string) => void
} }
} }
} }
@ -63,7 +64,16 @@ window.orgJDrupesVmOperatorVmViewer.initPreview = (previewDom: HTMLElement,
vmName: "", vmName: "",
vmDefinition: {} vmDefinition: {}
}); });
const vmDef = computed(() => previewApi.vmDefinition); const configured = computed(() => previewApi.vmDefinition.spec);
const startable = computed(() => previewApi.vmDefinition.spec &&
previewApi.vmDefinition.spec.vm.state !== 'Running'
&& !previewApi.vmDefinition.running);
const stoppable = computed(() => previewApi.vmDefinition.spec &&
previewApi.vmDefinition.spec.vm.state !== 'Stopped'
&& previewApi.vmDefinition.running);
const running = computed(() => previewApi.vmDefinition.running);
const permissions = computed(() => previewApi.vmDefinition.spec
? previewApi.vmDefinition.userPermissions : []);
watch(() => previewApi.vmName, (name: string) => { watch(() => previewApi.vmName, (name: string) => {
if (name !== "") { if (name !== "") {
@ -73,41 +83,51 @@ window.orgJDrupesVmOperatorVmViewer.initPreview = (previewDom: HTMLElement,
provideApi(previewDom, previewApi); provideApi(previewDom, previewApi);
const vmAction = (vmName: string, action: string) => { const vmAction = (action: string) => {
JGConsole.notifyConletModel(conletId, action, vmName); JGConsole.notifyConletModel(conletId, action);
}; };
return { localize, resourceBase, vmDef, vmAction }; return { localize, resourceBase, vmAction, configured,
startable, stoppable, running, permissions };
}, },
template: ` template: `
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td><img role=button <td rowspan="2" style="position: relative"><span
:aria-disabled="!vmDef.running || !vmDef.userPermissions style="position: absolute;"
|| !vmDef.userPermissions.includes('accessConsole')" :class="{ busy: configured && !startable && !stoppable }"
v-on:click="vmAction(vmDef.name, 'openConsole')" ><img role=button :aria-disabled="!running
:src="resourceBase + (vmDef.running || !permissions.includes('accessConsole')"
? 'computer.svg' : 'computer-off.svg')" v-on:click="vmAction('openConsole')"
:title="localize('Open console')"></td> :src="resourceBase + (running
<td v-if="vmDef.spec" ? 'computer.svg' : 'computer-off.svg')"
class="jdrupes-vmoperator-vmviewer-preview-action-list"> :title="localize('Open console')"></span><span
<span role="button" v-if="vmDef.spec.vm.state != 'Running'" style="visibility: hidden;"><img
:aria-disabled="!vmDef.userPermissions.includes('start')" :src="resourceBase + 'computer.svg'"></span></td>
<td class="jdrupes-vmoperator-vmviewer-preview-action-list">
<span role="button"
:aria-disabled="!startable || !permissions.includes('start')"
tabindex="0" class="fa fa-play" :title="localize('Start VM')" tabindex="0" class="fa fa-play" :title="localize('Start VM')"
v-on:click="vmAction(vmDef.name, 'start')"></span> v-on:click="vmAction('start')"></span>
<span role="button" v-else class="fa fa-play" <span role="button"
aria-disabled="true" :title="localize('Start VM')"></span> :aria-disabled="!stoppable || !permissions.includes('stop')"
<span role="button" v-if="vmDef.spec.vm.state != 'Stopped'"
:aria-disabled="!vmDef.userPermissions.includes('stop')"
tabindex="0" class="fa fa-stop" :title="localize('Stop VM')" tabindex="0" class="fa fa-stop" :title="localize('Stop VM')"
v-on:click="vmAction(vmDef.name, 'stop')"></span> v-on:click="vmAction('stop')"></span>
<span role="button" v-else class="fa fa-stop" <span role="button"
aria-disabled="true" :title="localize('Stop VM')"></span> :aria-disabled="!running || !permissions.includes('reset')"
</td> tabindex="0" class="svg-icon" :title="localize('Reset VM')"
<td v-else> v-on:click="vmAction('reset')">
<svg viewBox="0 0 1541.33 1535.5083">
<path d="m 0,127.9968 v 448 c 0,35 29,64 64,64 h 448 c 35,0 64,-29 64,-64 0,-17 -6.92831,-33.07213 -19,-45 C 264.23058,241.7154 337.19508,314.89599 109,82.996795 c -11.999999,-12 -28,-19 -45,-19 -35,0 -64,29 -64,64.000005 z" />
<path d="m 772.97656,1535.5046 c 117.57061,0.3623 236.06134,-26.2848 345.77544,-81.4687 292.5708,-147.1572 459.8088,-465.37411 415.5214,-790.12504 C 1489.9861,339.15993 1243.597,77.463924 922.29883,14.342498 601.00067,-48.778928 274.05699,100.37563 110.62891,384.39133 c -34.855139,60.57216 -14.006492,137.9313 46.5664,172.78516 60.57172,34.85381 137.92941,14.00532 172.78321,-46.56641 109.97944,-191.12927 327.69604,-290.34657 543.53515,-247.94336 215.83913,42.40321 380.18953,216.77543 410.00973,435.44141 29.8203,218.66598 -81.8657,430.94957 -278.4863,529.84567 -196.6206,98.8962 -432.84043,61.8202 -589.90233,-92.6777 -24.91016,-24.5038 -85.48587,-83.3326 -119.02246,-52.9832 -24.01114,21.7292 -35.41741,29.5454 -59.9209,54.4559 -24.50381,24.9102 -35.33636,36.9034 -57.54543,60.4713 -38.1335,40.4667 34.10761,93.9685 59.01808,118.472 145.96311,143.5803 339.36149,219.2087 535.3125,219.8125 z"/>
</svg>
</span>
</td> </td>
</tr> </tr>
<tr>
<td></td>
</tr>
</tbody> </tbody>
</table>` </table>`
}); });
@ -209,3 +229,9 @@ window.orgJDrupesVmOperatorVmViewer.applyEdit =
const vmName = getApi<ref<string>>(dialogDom!)!.value; const vmName = getApi<ref<string>>(dialogDom!)!.value;
JGConsole.notifyConletModel(conletId, "selectedVm", vmName); JGConsole.notifyConletModel(conletId, "selectedVm", vmName);
} }
window.orgJDrupesVmOperatorVmViewer.confirmReset =
(conletType: string, conletId: string) => {
JGConsole.instance.closeModalDialog(conletType, conletId);
JGConsole.notifyConletModel(conletId, "resetConfirmed");
}

View file

@ -19,7 +19,24 @@
/* /*
* Conlet specific styles. * Conlet specific styles.
*/ */
.jdrupes-vmoperator-vmviewer-preview { .jdrupes-vmoperator-vmviewer {
span[role="button"].svg-icon {
display: inline-block;
line-height: 1;
/* Align with forkawesome */
font-size: 14px;
fill: var(--primary);
&[aria-disabled="true"], &[aria-disabled=""] {
fill: var(--disabled);
}
svg {
height: 2ex;
width: 1em;
}
}
[role=button] { [role=button] {
padding: 0.25rem; padding: 0.25rem;
@ -28,7 +45,10 @@
box-shadow: var(--darkening); box-shadow: var(--darkening);
} }
} }
}
.jdrupes-vmoperator-vmviewer.jdrupes-vmoperator-vmviewer-preview {
img { img {
height: 3em; height: 3em;
padding: 0.25rem; padding: 0.25rem;
@ -37,9 +57,42 @@
opacity: 0.4; opacity: 0.4;
} }
} }
.jdrupes-vmoperator-vmviewer-preview-action-list {
white-space: nowrap;
}
span.busy::before {
font: normal normal normal 14px/1 ForkAwesome;
font-size: 1.125em;
content: "\f1ce";
left: 1.45em;
top: 0.7em;
color: var(--info);
position: absolute;
animation: spin 2s linear infinite;
z-index: 100;
}
} }
.jdrupes-vmoperator-vmviewer-preview-action-list { .jdrupes-vmoperator-vmviewer.jdrupes-vmoperator-vmviewer-edit {
white-space: nowrap; select {
width: 15em;
}
} }
.jdrupes-vmoperator-vmviewer.jdrupes-vmoperator-vmviewer-confirm-reset {
p {
text-align: center;
}
span[role="button"].svg-icon {
fill: var(--danger);
svg {
width: 2.5em;
height: 2.5em;
}
}
}