Merge branch 'release/v3.1.x' into release/v3.x

This commit is contained in:
Michael Lipp 2024-06-22 15:28:22 +02:00
commit d285c17268
62 changed files with 3653 additions and 2747 deletions

View file

@ -22,10 +22,10 @@ jobs:
fetch-depth: 0
- name: Install graphviz
run: sudo apt-get install graphviz
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'
- name: Build with Gradle
run: ./gradlew -Prepo.access.token=${{ secrets.REPO_ACCESS_TOKEN }} stage
run: ./gradlew -Pwebsite.push.token=${{ secrets.WEBSITE_PUSH_TOKEN }} stage

View file

@ -31,10 +31,10 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'
- name: Push with Gradle
run: ./gradlew -Prepo.access.token=${{ secrets.REPO_ACCESS_TOKEN }} -Pdocker.registry=ghcr.io/${{ github.actor }} stage pushImages
run: ./gradlew -Pwebsite.push.token=${{ secrets.WEBSITE_PUSH_TOKEN }} -Pdocker.registry=ghcr.io/${{ github.actor }} stage pushImages

View file

@ -1,28 +1,76 @@
default:
# Template project: https://gitlab.com/pages/jekyll
# Docs: https://docs.gitlab.com/ee/pages/
image: ruby:3.2
stages:
- build
- test
- publish
- deploy
.any-job:
rules:
- if: $CI_SERVER_HOST == "gitlab.mnl.de"
.gradle-job:
extends: .any-job
image: registry.mnl.de/org/jgrapes/jdk21-builder:v2
cache:
- key: dependencies
policy: pull-push
paths:
- .gradle
- node_modules
- key: "$CI_COMMIT_SHA"
policy: pull-push
paths:
- build
- "*/build"
before_script:
- echo -n $CI_REGISTRY_PASSWORD | podman login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
- git switch $CI_COMMIT_REF_NAME
- git pull
- git reset --hard $CI_COMMIT_SHA
build-jars:
stage: build
extends: .gradle-job
script:
- ./gradlew -Pdocker.registry=$CI_REGISTRY_IMAGE build apidocs
publish-images:
stage: publish
extends: .gradle-job
script:
- ./gradlew -Pdocker.registry=$CI_REGISTRY_IMAGE pushImage
.pages-job:
extends: .any-job
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/ruby:3.2
variables:
JEKYLL_ENV: production
LC_ALL: C.UTF-8
before_script:
- git fetch origin gh-pages
- git checkout gh-pages
- gem install bundler
- bundle install
variables:
JEKYLL_ENV: production
LC_ALL: C.UTF-8
test:
test-pages:
stage: test
extends: .pages-job
rules:
- if: $CI_COMMIT_BRANCH == "gh-pages"
script:
- bundle exec jekyll build -d test
artifacts:
paths:
- test
pages:
stage: deploy
script:
- bundle exec jekyll build -d public
artifacts:
paths:
- public
environment: production
#publish-pages:
# stage: publish
# extends: .pages-job
# rules:
# - if: $CI_COMMIT_BRANCH == "gh-pages"
# script:
# - bundle exec jekyll build -d public
# artifacts:
# paths:
# - public
# environment: production

View file

@ -8,5 +8,5 @@
The goal of this project is to provide the means for running Qemu
based VMs in Kubernetes pods.
See the [project's home page](https://mnlipp.github.io/VM-Operator/)
See the [project's home page](https://jdrupes.org/vm-operator/)
for details.

View file

@ -27,7 +27,7 @@ task stage {
tc -> tc.findByName("build") }.flatten()
}
if (JavaVersion.current() == JavaVersion.VERSION_17) {
if (JavaVersion.current() == JavaVersion.VERSION_21) {
// Publish JavaDoc
dependsOn gitPublishPush
}

View file

@ -55,7 +55,7 @@ sourceSets {
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
languageVersion = JavaLanguageVersion.of(21)
}
}

View file

@ -22,31 +22,28 @@ configurations {
}
dependencies {
markdownDoclet "org.jdrupes.mdoclet:doclet:3.1.0"
javadocTaglets "org.jdrupes.taglets:plantuml-taglet:2.1.0"
}
task javadocResources(type: Copy) {
into file(docDestinationDir)
from ("${rootProject.rootDir}/misc") {
include '*.woff2'
}
markdownDoclet "org.jdrupes.mdoclet:doclet:4.0.0"
javadocTaglets "org.jdrupes.taglets:plantuml-taglet:3.0.0"
}
task apidocs (type: JavaExec) {
// Does not work on JitPack, no /usr/bin/dot
enabled = JavaVersion.current() == JavaVersion.VERSION_17
dependsOn javadocResources
enabled = JavaVersion.current() == JavaVersion.VERSION_21
outputs.dir(docDestinationDir)
inputs.file rootProject.file('overview.md')
inputs.file "${rootProject.rootDir}/misc/stylesheet.css"
inputs.file "${rootProject.rootDir}/misc/javadoc-overwrites.css"
jvmArgs = ['--add-exports=jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED']
main = 'jdk.javadoc.internal.tool.Main'
jvmArgs = ['--add-exports=jdk.compiler/com.sun.tools.doclint=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
'--add-exports=jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED',
'--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit=ALL-UNNAMED',
'--add-opens=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit.resources.releases=ALL-UNNAMED',
'-Duser.language=en', '-Duser.region=US']
mainClass = 'jdk.javadoc.internal.tool.Main'
gradle.projectsEvaluated {
// Make sure that other projects' compileClasspaths are resolved
@ -69,8 +66,8 @@ task apidocs (type: JavaExec) {
'-package',
'-use',
'-linksource',
'-link', 'https://docs.oracle.com/en/java/javase/17/docs/api/',
'-link', 'https://mnlipp.github.io/jgrapes/latest-release/javadoc/',
'-link', 'https://docs.oracle.com/en/java/javase/21/docs/api/',
'-link', 'https://jgrapes.org/latest-release/javadoc/',
'-link', 'https://freemarker.apache.org/docs/api/',
'--add-exports', 'jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED',
'--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
@ -88,7 +85,7 @@ task apidocs (type: JavaExec) {
'-bottom', rootProject.file("misc/javadoc.bottom.txt").text,
'--allow-script-in-comments',
'-Xdoclint:-html',
'--main-stylesheet', "${rootProject.rootDir}/misc/stylesheet.css",
'--add-stylesheet', "${rootProject.rootDir}/misc/javadoc-overwrites.css",
'--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.formats.html=ALL-UNNAMED',
'-quiet'
]
@ -97,23 +94,46 @@ task apidocs (type: JavaExec) {
ignoreExitValue true
}
task testJavadoc(type: Javadoc) {
enabled = JavaVersion.current() == JavaVersion.VERSION_21
source = fileTree(dir: 'testfiles', include: '**/*.java')
destinationDir = project.file("build/testfiles-gradle")
options.docletpath = configurations.markdownDoclet.files.asType(List)
options.doclet = 'org.jdrupes.mdoclet.MDoclet'
options.overview = 'testfiles/overview.md'
options.addStringOption('Xdoclint:-html', '-quiet')
options.setJFlags([
'--add-exports=jdk.compiler/com.sun.tools.doclint=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
'--add-exports=jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED',
'--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit=ALL-UNNAMED',
'--add-opens=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit.resources.releases=ALL-UNNAMED'])
}
// Prepare github authentication for plugins
if (System.properties['org.ajoberstar.grgit.auth.username'] == null) {
System.setProperty('org.ajoberstar.grgit.auth.username',
project.rootProject.properties['repo.access.token'] ?: "nouser")
project.rootProject.properties['website.push.token'] ?: "nouser")
}
gitPublish {
repoUri = 'https://github.com/mnlipp/VM-Operator.git'
branch = 'gh-pages'
repoUri = 'https://github.com/mnlipp/jdrupes.org.git'
branch = 'main'
contents {
from("${rootProject.projectDir}/webpages") {
include '_layouts/vm-operator.html'
include 'vm-operator/**'
}
from("${rootProject.buildDir}/javadoc") {
into 'javadoc'
into 'vm-operator/javadoc'
}
if (!findProject(':org.jdrupes.vmoperator.runner.qemu').isSnapshot
&& !findProject(':org.jdrupes.vmoperator.manager').isSnapshot) {
from("${rootProject.buildDir}/javadoc") {
into 'latest-release/javadoc'
into 'vm-operator/latest-release/javadoc'
}
}
}

View file

@ -20,7 +20,7 @@ spec:
containers:
- name: vm-operator
image: >-
ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:3.0.0
ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:3.1.1
volumeMounts:
- name: config
mountPath: /etc/opt/vmoperator

1
gradle.properties Normal file
View file

@ -0,0 +1 @@
org.gradle.parallel=true

Binary file not shown.

View file

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

31
gradlew vendored
View file

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -83,10 +83,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -133,18 +131,21 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

20
gradlew.bat vendored
View file

@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,2 @@
:root { --body-font-size: 16px;}
:root { --code-font-size: 16px;}

View file

@ -4,26 +4,30 @@
<a href="https://github.com/site/terms" target="_top">Terms</a>
&mdash; <a href="https://github.com/site/privacy" target="_top">Privacy</a></p>
<script type="text/javascript">
if (location.hostname.indexOf("github") !== -1) {
if (location.hostname.indexOf("github") !== -1 || location.hostname.indexOf("jdrupes.org") !== -1) {
document.getElementById("githubfooter").style.visibility="visible";
var _paq = _paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
_paq.push(["setCookieDomain", "*.mnlipp.github.io"]);
_paq.push(["setDomains", ["*.mnlipp.github.io"]]);
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//piwik.mnl.de/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '14']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
}
</script>
<noscript>
<div>JavaScript is disabled on your browser, terms and privacy links may not be shown correctly.</div>
</noscript>
<!-- Matomo anonymous, no cookies (https://matomo.org/blog/2018/04/how-to-not-process-any-personal-data-with-matomo-and-what-it-means-for-you/) -->
<script>
var _paq = _paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
_paq.push(["setCookieDomain", "*.jdrupes.org"]);
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//jdrupes.org/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '15']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img referrerpolicy="no-referrer-when-downgrade" src="//piwik.mnl.de/matomo.php?idsite=15&amp;rec=1" style="border:0;" alt="" /></p></noscript>
<!-- End Matomo Code -->
</div>

View file

@ -1,904 +0,0 @@
/*
* Javadoc style sheet
*/
@font-face {
font-family: 'DejaVu Serif';
src: local('DejaVu Serif'), url('DejaVuSerif.woff2');
}
@font-face {
font-family: 'DejaVu Serif';
font-weight: bold;
src: local('DejaVu Serif Bold'), url('DejaVuSerif-Bold.woff2');
}
@font-face {
font-family: 'DejaVu Sans';
src: local('DejaVu Sans'), url('DejaVuSans.woff2');
}
@font-face {
font-family: 'DejaVu Sans';
font-weight: bold;
src: local('DejaVu Sans Bold'), url('DejaVuSans-Bold.woff2');
}
@font-face {
font-family: 'DejaVu Sans Mono';
src: local('DejaVu Sans Mono'), url('DejaVuSansMono.woff2');
}
@font-face {
font-family: 'DejaVu Sans Mono';
font-weight: bold;
src: local('DejaVu Sans Mono Bold'), url('DejaVuSansMono-Bold.woff2');
}
/*
* Styles for individual HTML elements.
*
* These are styles that are specific to individual HTML elements. Changing them affects the style of a particular
* HTML element throughout the page.
*/
body {
background-color:#ffffff;
color:#353833;
font: normal 16px/1.5 "DejaVu Sans", Arial, Helvetica, sans-serif;
margin:0;
padding:0;
height:100%;
width:100%;
}
iframe {
margin:0;
padding:0;
height:100%;
width:100%;
overflow-y:scroll;
border:none;
}
a:link, a:visited {
text-decoration:none;
color:#4A6782;
}
a[href]:hover, a[href]:focus {
text-decoration:none;
color:#bb7a2a;
}
a[name] {
color:#353833;
}
pre {
font-family:'DejaVu Sans Mono', monospace;
}
h1 {
font-size:20px;
}
h2 {
font-size:18px;
}
h3 {
font-size:17px;
}
h4 {
font-size:16px;
margin-top: 1rem;
margin-bottom: 1rem;
}
h5 {
font-size:14px;
}
h6 {
font-size:13px;
}
ul {
list-style-type:disc;
}
code, tt {
font-family:'DejaVu Sans Mono', monospace;
}
:not(h1, h2, h3, h4, h5, h6) > code,
:not(h1, h2, h3, h4, h5, h6) > tt {
/* font-size:14px; */
padding-top:4px;
margin-top:8px;
line-height:1.4em;
}
dt code {
font-family:'DejaVu Sans Mono', monospace;
font-size:14px;
padding-top:4px;
}
.summary-table dt code {
font-family:'DejaVu Sans Mono', monospace;
font-size:14px;
vertical-align:top;
padding-top:4px;
}
sup {
font-size:8px;
}
button {
font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif;
}
/*
* Styles for HTML generated by javadoc.
*
* These are style classes that are used by the standard doclet to generate HTML documentation.
*/
/*
* Styles for document title and copyright.
*/
.clear {
clear:both;
height:0;
overflow:hidden;
}
.about-language {
float:right;
padding:0 21px 8px 8px;
font-size:11px;
margin-top:-9px;
height:2.9em;
}
.legal-copy {
margin-left:.5em;
}
.tab {
background-color:#0066FF;
color:#ffffff;
padding:8px;
width:5em;
font-weight:bold;
}
/*
* Styles for navigation bar.
*/
@media screen {
.flex-box {
position:fixed;
display:flex;
flex-direction:column;
height: 100%;
width: 100%;
}
.flex-header {
flex: 0 0 auto;
}
.flex-content {
flex: 1 1 auto;
overflow-y: auto;
}
}
.top-nav {
background-color:#4D7A97;
color:#FFFFFF;
float:left;
padding:0;
width:100%;
clear:right;
min-height:2.8em;
padding-top:10px;
overflow:hidden;
font-size:80%;
}
.sub-nav {
background-color:#dee3e9;
float:left;
width:100%;
overflow:hidden;
font-size:80%;
}
.sub-nav div {
clear:left;
float:left;
padding:0 0 5px 6px;
text-transform:uppercase;
}
.sub-nav .nav-list {
padding-top:5px;
}
ul.nav-list {
display:block;
margin:0 25px 0 0;
padding:0;
}
ul.sub-nav-list {
float:left;
margin:0 25px 0 0;
padding:0;
}
ul.nav-list li {
list-style:none;
float:left;
padding: 5px 6px;
text-transform:uppercase;
}
.sub-nav .nav-list-search {
float:right;
margin:0 0 0 0;
padding:5px 6px;
clear:none;
}
.nav-list-search label {
position:relative;
right:-16px;
}
ul.sub-nav-list li {
list-style:none;
float:left;
padding-top:10px;
}
.top-nav a:link, .top-nav a:active, .top-nav a:visited {
color:#FFFFFF;
text-decoration:none;
text-transform:uppercase;
}
.top-nav a:hover {
text-decoration:none;
color:#bb7a2a;
text-transform:uppercase;
}
.nav-bar-cell1-rev {
background-color:#F8981D;
color:#253441;
margin: auto 5px;
}
.skip-nav {
position:absolute;
top:auto;
left:-9999px;
overflow:hidden;
}
/*
* Hide navigation links and search box in print layout
*/
@media print {
ul.nav-list, div.sub-nav {
display:none;
}
}
/*
* Styles for page header and footer.
*/
.title {
color:#2c4557;
margin:10px 0;
}
.sub-title {
margin:5px 0 0 0;
}
.header ul {
margin:0 0 15px 0;
padding:0;
}
.header ul li, .footer ul li {
list-style:none;
font-size:80%;
}
/*
* Styles for headings.
*/
body.class-declaration-page .summary h2,
body.class-declaration-page .details h2,
body.class-use-page h2,
body.module-declaration-page .block-list h2 {
font-style: italic;
padding:0;
margin:15px 0;
}
body.class-declaration-page .summary h3,
body.class-declaration-page .details h3,
body.class-declaration-page .summary .inherited-list h2 {
background-color:#dee3e9;
border:1px solid #d0d9e0;
margin:0 0 6px -8px;
padding:7px 5px;
}
/*
* Styles for page layout containers.
*/
main {
clear:both;
padding:10px 20px;
position:relative;
}
dl.notes > dt {
font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif;
/* font-size:12px; */
font-weight:bold;
margin:10px 0 0 0;
color:#4E4E4E;
}
dl.notes > dd {
margin:5px 10px 0 0;
/* font-size:14px; */
font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
}
dl.name-value > dt {
margin-left:1px;
/* font-size:1.1em; */
display:inline;
font-weight:bold;
}
dl.name-value > dd {
margin:0 0 0 1px;
/* font-size:1.1em; */
display:inline;
}
/*
* Styles for lists.
*/
li.circle {
list-style:circle;
}
ul.horizontal li {
display:inline;
/* font-size:0.9em; */
}
div.inheritance {
margin:0;
padding:0;
}
div.inheritance div.inheritance {
margin-left:2em;
}
ul.block-list,
ul.details-list,
ul.member-list,
ul.summary-list {
margin:10px 0 10px 0;
padding:0;
}
ul.block-list > li,
ul.details-list > li,
ul.member-list > li,
ul.summary-list > li {
list-style:none;
margin-bottom:15px;
line-height:1.4;
}
.summary-table dl, .summary-table dl dt, .summary-table dl dd {
margin-top:0;
margin-bottom:1px;
}
ul.see-list, ul.see-list-long {
padding-left: 0;
list-style: none;
}
ul.see-list li {
display: inline;
}
ul.see-list li:not(:last-child):after,
ul.see-list-long li:not(:last-child):after {
content: ", ";
white-space: pre-wrap;
}
/*
* Styles for tables.
*/
.summary-table, .details-table {
width:100%;
border-spacing:0;
border-left:1px solid #EEE;
border-right:1px solid #EEE;
border-bottom:1px solid #EEE;
padding:0;
}
.summary-table .col-first {
font-family: "DejaVu Sans Mono", monospace;
}
.caption {
position:relative;
text-align:left;
background-repeat:no-repeat;
color:#253441;
font-weight:bold;
clear:none;
overflow:hidden;
padding:0;
padding-top:10px;
padding-left:1px;
margin:0;
white-space:pre;
}
.caption a:link, .caption a:visited {
color:#1f389c;
}
.caption a:hover,
.caption a:active {
color:#FFFFFF;
}
.caption span {
white-space:nowrap;
padding-top:5px;
padding-left:12px;
padding-right:12px;
padding-bottom:7px;
display:inline-block;
float:left;
background-color:#F8981D;
border: none;
height:16px;
}
div.table-tabs {
padding:10px 0 0 1px;
margin:0;
}
div.table-tabs > button {
border: none;
cursor: pointer;
padding: 5px 12px 7px 12px;
font-weight: bold;
margin-right: 3px;
}
div.table-tabs > button.active-table-tab {
background: #F8981D;
color: #253441;
}
div.table-tabs > button.table-tab {
background: #4D7A97;
color: #FFFFFF;
}
.two-column-summary {
display: grid;
grid-template-columns: minmax(15%, max-content) minmax(15%, auto);
}
.three-column-summary {
display: grid;
grid-template-columns: minmax(10%, max-content) minmax(15%, max-content) minmax(15%, auto);
}
.four-column-summary {
display: grid;
grid-template-columns: minmax(10%, max-content) minmax(10%, max-content) minmax(10%, max-content) minmax(10%, auto);
}
@media screen and (max-width: 600px) {
.two-column-summary {
display: grid;
grid-template-columns: 1fr;
}
}
@media screen and (max-width: 800px) {
.three-column-summary {
display: grid;
grid-template-columns: minmax(10%, max-content) minmax(25%, auto);
}
.three-column-summary .col-last {
grid-column-end: span 2;
}
}
@media screen and (max-width: 1000px) {
.four-column-summary {
display: grid;
grid-template-columns: minmax(15%, max-content) minmax(15%, auto);
}
}
.summary-table > div, .details-table > div {
text-align:left;
padding: 8px 3px 3px 7px;
}
.col-first, .col-second, .col-last, .col-constructor-name, .col-summary-item-name {
vertical-align:top;
padding-right:0;
padding-top:8px;
padding-bottom:3px;
}
.table-header {
background:#dee3e9;
font-weight: bold;
}
/*
.col-first, .col-first {
font-size:13px;
}
.col-second, .col-second, .col-last, .col-constructor-name, .col-summary-item-name, .col-last {
font-size:13px;
}
*/
.col-first, .col-second, .col-constructor-name {
vertical-align:top;
overflow: auto;
}
.col-last {
white-space:normal;
}
.col-first a:link, .col-first a:visited,
.col-second a:link, .col-second a:visited,
.col-first a:link, .col-first a:visited,
.col-second a:link, .col-second a:visited,
.col-constructor-name a:link, .col-constructor-name a:visited,
.col-summary-item-name a:link, .col-summary-item-name a:visited,
.constant-values-container a:link, .constant-values-container a:visited,
.all-classes-container a:link, .all-classes-container a:visited,
.all-packages-container a:link, .all-packages-container a:visited {
font-weight:bold;
}
.table-sub-heading-color {
background-color:#EEEEFF;
}
.even-row-color, .even-row-color .table-header {
background-color:#FFFFFF;
}
.odd-row-color, .odd-row-color .table-header {
background-color:#EEEEEF;
}
/*
* Styles for contents.
*/
.deprecated-content {
margin:0;
padding:10px 0;
}
div.block {
font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
}
.col-last div {
padding-top:0;
}
.col-last a {
padding-bottom:3px;
}
.module-signature,
.package-signature,
.type-signature,
.member-signature {
font-family:'DejaVu Sans Mono', monospace;
margin:14px 0;
white-space: pre-wrap;
}
.module-signature,
.package-signature,
.type-signature {
margin-top: 0;
}
.member-signature .type-parameters-long,
.member-signature .parameters,
.member-signature .exceptions {
display: inline-block;
vertical-align: top;
white-space: pre;
}
.member-signature .type-parameters {
white-space: normal;
}
/*
* Styles for formatting effect.
*/
.source-line-no {
color:green;
padding:0 30px 0 0;
}
h1.hidden {
visibility:hidden;
overflow:hidden;
/* font-size:10px; */
}
.block {
display:block;
margin:0 10px 5px 0;
color:#474747;
}
.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,
.package-hierarchy-label, .type-name-label, .type-name-link, .search-tag-link, .preview-label {
font-weight:bold;
}
.deprecation-comment, .help-footnote, .preview-comment {
font-style:italic;
}
.deprecation-block {
/* font-size:14px; */
font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
border-style:solid;
border-width:thin;
border-radius:10px;
padding:10px;
margin-bottom:10px;
margin-right:10px;
display:inline-block;
}
.preview-block {
/* font-size:14px; */
font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
border-style:solid;
border-width:thin;
border-radius:10px;
padding:10px;
margin-bottom:10px;
margin-right:10px;
display:inline-block;
}
div.block div.deprecation-comment {
font-style:normal;
}
/*
* Styles specific to HTML5 elements.
*/
main, nav, header, footer, section {
display:block;
}
/*
* Styles for javadoc search.
*/
.ui-autocomplete-category {
font-weight:bold;
/* font-size:15px; */
padding:7px 0 7px 3px;
background-color:#4D7A97;
color:#FFFFFF;
}
.result-item {
/* font-size:13px; */
}
.ui-autocomplete {
max-height:85%;
max-width:65%;
overflow-y:scroll;
overflow-x:scroll;
white-space:nowrap;
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
ul.ui-autocomplete {
position:fixed;
z-index:999999;
background-color: #FFFFFF;
}
ul.ui-autocomplete li {
float:left;
clear:both;
width:100%;
}
.result-highlight {
font-weight:bold;
}
.ui-autocomplete .result-item {
font-size: inherit;
}
#search-input {
background-image:url('resources/glass.png');
background-size:13px;
background-repeat:no-repeat;
background-position:2px 3px;
padding-left:20px;
position:relative;
right:-18px;
width:400px;
}
#reset-button {
background-color: rgb(255,255,255);
background-image:url('resources/x.png');
background-position:center;
background-repeat:no-repeat;
background-size:12px;
border:0 none;
width:16px;
height:16px;
position:relative;
left:-4px;
top:-4px;
font-size:0px;
}
.watermark {
color:#545454;
}
.search-tag-desc-result {
font-style:italic;
/* font-size:11px; */
}
.search-tag-holder-result {
font-style:italic;
/* font-size:12px; */
}
.search-tag-result:target {
background-color:yellow;
}
.module-graph span {
display:none;
position:absolute;
}
.module-graph:hover span {
display:block;
margin: -100px 0 0 100px;
z-index: 1;
}
.inherited-list {
margin: 10px 0 10px 0;
}
section.class-description {
line-height: 1.4;
}
.summary section[class$="-summary"], .details section[class$="-details"],
.class-uses .detail, .serialized-class-details {
padding: 0px 20px 5px 10px;
border: 1px solid #ededed;
background-color: #f8f8f8;
}
.inherited-list, section[class$="-details"] .detail {
padding:0 0 5px 8px;
background-color:#ffffff;
border:none;
}
.vertical-separator {
padding: 0 5px;
}
ul.help-section-list {
margin: 0;
}
ul.help-subtoc > li {
display: inline-block;
padding-right: 5px;
/* font-size: smaller; */
}
ul.help-subtoc > li::before {
content: "\2022" ;
padding-right:2px;
}
span.help-note {
font-style: italic;
}
/*
* Indicator icon for external links.
*/
main a[href*="://"]::after {
content:"";
display:inline-block;
background-image:url('data:image/svg+xml; utf8, \
<svg xmlns="http://www.w3.org/2000/svg" width="768" height="768">\
<path d="M584 664H104V184h216V80H0v688h688V448H584zM384 0l132 \
132-240 240 120 120 240-240 132 132V0z" fill="%234a6782"/>\
</svg>');
background-size:100% 100%;
width:7px;
height:7px;
margin-left:2px;
margin-bottom:4px;
}
main a[href*="://"]:hover::after,
main a[href*="://"]:focus::after {
background-image:url('data:image/svg+xml; utf8, \
<svg xmlns="http://www.w3.org/2000/svg" width="768" height="768">\
<path d="M584 664H104V184h216V80H0v688h688V448H584zM384 0l132 \
132-240 240 120 120 240-240 132 132V0z" fill="%23bb7a2a"/>\
</svg>');
}
/*
* Styles for user-provided tables.
*
* borderless:
* No borders, vertical margins, styled caption.
* This style is provided for use with existing doc comments.
* In general, borderless tables should not be used for layout purposes.
*
* plain:
* Plain borders around table and cells, vertical margins, styled caption.
* Best for small tables or for complex tables for tables with cells that span
* rows and columns, when the "striped" style does not work well.
*
* striped:
* Borders around the table and vertical borders between cells, striped rows,
* vertical margins, styled caption.
* Best for tables that have a header row, and a body containing a series of simple rows.
*/
table.borderless,
table.plain,
table.striped {
margin-top: 10px;
margin-bottom: 10px;
}
table.borderless > caption,
table.plain > caption,
table.striped > caption {
font-weight: bold;
/* font-size: smaller; */
}
table.borderless th, table.borderless td,
table.plain th, table.plain td,
table.striped th, table.striped td {
padding: 2px 5px;
}
table.borderless,
table.borderless > thead > tr > th, table.borderless > tbody > tr > th, table.borderless > tr > th,
table.borderless > thead > tr > td, table.borderless > tbody > tr > td, table.borderless > tr > td {
border: none;
}
table.borderless > thead > tr, table.borderless > tbody > tr, table.borderless > tr {
background-color: transparent;
}
table.plain {
border-collapse: collapse;
border: 1px solid black;
}
table.plain > thead > tr, table.plain > tbody tr, table.plain > tr {
background-color: transparent;
}
table.plain > thead > tr > th, table.plain > tbody > tr > th, table.plain > tr > th,
table.plain > thead > tr > td, table.plain > tbody > tr > td, table.plain > tr > td {
border: 1px solid black;
}
table.striped {
border-collapse: collapse;
border: 1px solid black;
}
table.striped > thead {
background-color: #E3E3E3;
}
table.striped > thead > tr > th, table.striped > thead > tr > td {
border: 1px solid black;
}
table.striped > tbody > tr:nth-child(even) {
background-color: #EEE
}
table.striped > tbody > tr:nth-child(odd) {
background-color: #FFF
}
table.striped > tbody > tr > th, table.striped > tbody > tr > td {
border-left: 1px solid black;
border-right: 1px solid black;
}
table.striped > tbody > tr > th {
font-weight: normal;
}
/**
* Tweak font sizes and paddings for small screens.
*/
@media screen and (max-width: 1050px) {
#search-input {
width: 300px;
}
}
@media screen and (max-width: 800px) {
#search-input {
width: 200px;
}
.top-nav,
.bottom-nav {
font-size: 80%;
padding-top: 6px;
}
.sub-nav {
font-size: 80%;
}
.about-language {
padding-right: 16px;
}
ul.nav-list li,
.sub-nav .nav-list-search {
padding: 6px;
}
ul.sub-nav-list li {
padding-top: 5px;
}
main {
padding: 10px;
}
.summary section[class$="-summary"], .details section[class$="-details"],
.class-uses .detail, .serialized-class-details {
padding: 0 8px 5px 8px;
}
body {
-webkit-text-size-adjust: none;
}
}
@media screen and (max-width: 500px) {
#search-input {
width: 150px;
}
.top-nav,
.bottom-nav {
font-size: 80%;
}
.sub-nav {
font-size: 80%;
}
.about-language {
font-size: 80%;
padding-right: 12px;
}
}

View file

@ -45,45 +45,36 @@ application {
mainClass = 'org.jdrupes.vmoperator.manager.Manager'
}
project.ext.gitBranch = grgit.branch.current.name.replace('/', '-')
task buildImage(type: Exec) {
dependsOn installDist
inputs.files 'src/org/jdrupes/vmoperator/manager/Containerfile'
commandLine 'podman', 'build', '--pull',
'-t', "${project.name}:${project.version}",\
'-t', "${project.name}:${project.gitBranch}",\
'-f', 'src/org/jdrupes/vmoperator/manager/Containerfile', '.'
}
task tagLatestImage(type: Exec) {
dependsOn buildImage
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
&& !project.version.contains("beta") \
|| project.rootProject.properties['docker.testRegistry'] \
&& project.rootProject.properties['docker.registry'] \
== project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'tag', "${project.name}:${project.version}",\
"${project.name}:latest"
}
task buildLatestImage {
dependsOn buildImage
dependsOn tagLatestImage
}
task pushImage(type: Exec) {
dependsOn buildImage
// Don't push without testing first
dependsOn test
def registry = "${project.rootProject.properties['docker.registry']}"
commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}:${project.version}", \
"${project.rootProject.properties['docker.registry']}" \
+ "/${project.name}:${project.version}"
"localhost/${project.name}:${project.gitBranch}", \
"${registry}/${project.name}:${project.gitBranch}"
if (!project.version.contains("SNAPSHOT")) {
commandLine 'podman', 'tag', \
"${registry}/${project.name}:${project.gitBranch}",\
"${registry}/${project.name}:${project.version}"
}
}
task pushLatestImage(type: Exec) {
dependsOn buildLatestImage
task tagAsLatest(type: Exec) {
dependsOn pushImage
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
@ -92,28 +83,21 @@ task pushLatestImage(type: Exec) {
&& project.rootProject.properties['docker.registry'] \
== project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}:${project.version}", \
"${project.rootProject.properties['docker.registry']}" \
+ "/${project.name}:latest"
def registry = "${project.rootProject.properties['docker.registry']}"
commandLine 'podman', 'tag', \
"${registry}/${project.name}:${project.version}",\
"${registry}/${project.name}:latest"
}
task pushForTest(type: Exec) {
dependsOn buildImage
commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}:${project.version}", \
"${project.rootProject.properties['docker.registry']}" \
"localhost/${project.name}:${project.gitBranch}", \
"${project.rootProject.properties['docker.testRegistry']}" \
+ "/${project.name}:test"
}
task pushImages {
// Don't push without testing first
dependsOn test
dependsOn pushImage
dependsOn pushLatestImage
}
test {
enabled = project.hasProperty("k8s.testCluster")

View file

@ -1,4 +1,4 @@
FROM docker.io/eclipse-temurin:17-jre-alpine
FROM docker.io/eclipse-temurin:21-jre-alpine
COPY build/install/vm-manager /opt/vmmanager

View file

@ -184,12 +184,8 @@ public class Reconciler extends Component {
* @param event the event
* @param channel the channel
* @throws ApiException the api exception
* @throws IOException
* @throws ParseException
* @throws MalformedTemplateNameException
* @throws TemplateNotFoundException
* @throws TemplateException
* @throws KubectlException
* @throws TemplateException the template exception
* @throws IOException Signals that an I/O exception has occurred.
*/
@Handler
@SuppressWarnings("PMD.ConfusingTernary")

View file

@ -31,45 +31,34 @@ application {
mainClass = 'org.jdrupes.vmoperator.runner.qemu.Runner'
}
project.ext.gitBranch = grgit.branch.current.name.replace('/', '-')
task buildArchImage(type: Exec) {
dependsOn installDist
inputs.files 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch'
commandLine 'podman', 'build', '--pull',
'-t', "${project.name}-arch:${project.version}",\
'-t', "${project.name}-arch:${project.gitBranch}",\
'-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch', '.'
}
task tagLatestArchImage(type: Exec) {
dependsOn buildArchImage
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
&& !project.version.contains("beta") \
|| project.rootProject.properties['docker.testRegistry'] \
&& project.rootProject.properties['docker.registry'] \
== project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'tag', "${project.name}-arch:${project.version}",\
"${project.name}-arch:latest"
}
task buildLatestArchImage {
dependsOn buildArchImage
dependsOn tagLatestArchImage
}
task pushArchImage(type: Exec) {
dependsOn buildArchImage
def registry = "${project.rootProject.properties['docker.registry']}"
commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}-arch:${project.version}", \
"${project.rootProject.properties['docker.registry']}" \
+ "/${project.name}-arch:${project.version}"
"localhost/${project.name}-arch:${project.gitBranch}", \
"${registry}/${project.name}-arch:${project.gitBranch}"
if (!project.version.contains("SNAPSHOT")) {
commandLine 'podman', 'tag', \
"${registry}/${project.name}-arch:${project.gitBranch}",\
"${registry}/${project.name}-arch:${project.version}"
}
}
task pushArchLatestImage(type: Exec) {
dependsOn buildLatestArchImage
task tagAsLatestArch(type: Exec) {
dependsOn pushArchImage
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
@ -78,10 +67,10 @@ task pushArchLatestImage(type: Exec) {
&& project.rootProject.properties['docker.registry'] \
== project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}-arch:${project.version}", \
"${project.rootProject.properties['docker.registry']}" \
+ "/${project.name}-arch:latest"
def registry = "${project.rootProject.properties['docker.registry']}"
commandLine 'podman', 'tag', \
"${registry}/${project.name}-arch:${project.version}",\
"${registry}/${project.name}-arch:latest"
}
task buildAlpineImage(type: Exec) {
@ -89,40 +78,27 @@ task buildAlpineImage(type: Exec) {
inputs.files 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine'
commandLine 'podman', 'build', '--pull',
'-t', "${project.name}-alpine:${project.version}",\
'-t', "${project.name}-alpine:${project.gitBranch}",\
'-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine', '.'
}
task tagLatestAlpineImage(type: Exec) {
dependsOn buildAlpineImage
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
&& !project.version.contains("beta") \
|| project.rootProject.properties['docker.testRegistry'] \
&& project.rootProject.properties['docker.registry'] \
== project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'tag', "${project.name}-alpine:${project.version}",\
"${project.name}-alpine:latest"
}
task buildLatestAlpineImage {
dependsOn buildAlpineImage
dependsOn tagLatestAlpineImage
}
task pushAlpineImage(type: Exec) {
dependsOn buildAlpineImage
def registry = "${project.rootProject.properties['docker.registry']}"
commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}-alpine:${project.version}", \
"${project.rootProject.properties['docker.registry']}" \
+ "/${project.name}-alpine:${project.version}"
"localhost/${project.name}-alpine:${project.gitBranch}", \
"${registry}/${project.name}-alpine:${project.gitBranch}"
if (!project.version.contains("SNAPSHOT")) {
commandLine 'podman', 'tag', \
"${registry}/${project.name}-alpine:${project.gitBranch}",\
"${registry}/${project.name}-alpine:${project.version}"
}
}
task pushAlpineLatestImage(type: Exec) {
dependsOn buildLatestAlpineImage
task tagAsLatestAlpine(type: Exec) {
dependsOn pushAlpineImage
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
@ -131,16 +107,19 @@ task pushAlpineLatestImage(type: Exec) {
&& project.rootProject.properties['docker.registry'] \
== project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}-alpine:${project.version}", \
"${project.rootProject.properties['docker.registry']}" \
+ "/${project.name}-alpine:latest"
def registry = "${project.rootProject.properties['docker.registry']}"
commandLine 'podman', 'tag', \
"${registry}/${project.name}-alpine:${project.version}",\
"${registry}/${project.name}-alpine:latest"
}
task pushImages {
task pushImage {
dependsOn pushArchImage
dependsOn pushArchLatestImage
dependsOn pushAlpineImage
dependsOn pushAlpineLatestImage
}
task tagAsLatest {
dependsOn tagAsLatestArch
dependsOn tagAsLatestAlpine
}

View file

@ -27,7 +27,7 @@ import org.jdrupes.vmoperator.runner.qemu.commands.QmpOpenTray;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpRemoveMedium;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
import org.jdrupes.vmoperator.runner.qemu.events.TrayMovedEvent;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
@ -69,7 +69,7 @@ public class CdMediaController extends Component {
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
"PMD.AvoidInstantiatingObjectsInLoops" })
public void onConfigureQemu(ConfigureQemu event) {
if (event.state() == State.TERMINATING) {
if (event.runState() == RunState.TERMINATING) {
return;
}
@ -82,7 +82,7 @@ public class CdMediaController extends Component {
}
var driveId = "cd" + cdCounter++;
var newFile = Optional.ofNullable(drives[i].file).orElse("");
if (event.state() == State.STARTING) {
if (event.runState() == RunState.STARTING) {
current.put(driveId, newFile);
continue;
}
@ -116,8 +116,8 @@ public class CdMediaController extends Component {
*/
@Handler
public void onTrayMovedEvent(TrayMovedEvent event) {
trayState.put(event.driveId(), event.state());
if (event.state() == TrayState.OPEN
trayState.put(event.driveId(), event.trayState());
if (event.trayState() == TrayState.OPEN
&& pending.containsKey(event.driveId())) {
changeMedium(event.driveId());
}

View file

@ -2,7 +2,7 @@ FROM docker.io/alpine
RUN apk update
RUN apk add qemu-system-x86_64 qemu-modules ovmf swtpm openjdk17 mtools
RUN apk add qemu-system-x86_64 qemu-modules ovmf swtpm openjdk21 mtools
RUN mkdir -p /etc/qemu && echo "allow all" > /etc/qemu/bridge.conf

View file

@ -1,11 +1,11 @@
FROM archlinux/archlinux:latest
FROM docker.io/archlinux/archlinux:latest
RUN systemd-firstboot
RUN pacman-key --init \
&& pacman -Sy --noconfirm archlinux-keyring && pacman -Su --noconfirm \
&& pacman -S --noconfirm which qemu-full virtiofsd \
edk2-ovmf swtpm iproute2 bridge-utils jre17-openjdk-headless \
edk2-ovmf swtpm iproute2 bridge-utils jre21-openjdk-headless \
mtools \
&& pacman -Scc --noconfirm

View file

@ -33,7 +33,7 @@ import org.jdrupes.vmoperator.runner.qemu.events.CpuAdded;
import org.jdrupes.vmoperator.runner.qemu.events.CpuDeleted;
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
@ -64,7 +64,7 @@ public class CpuController extends Component {
*/
@Handler
public void onConfigureQemu(ConfigureQemu event) {
if (event.state() == State.TERMINATING) {
if (event.runState() == RunState.TERMINATING) {
return;
}
Optional.ofNullable(event.configuration().vm.currentCpus)

View file

@ -27,7 +27,7 @@ import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetPasswordExpiry;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
@ -67,7 +67,7 @@ public class DisplayController extends Component {
*/
@Handler
public void onConfigureQemu(ConfigureQemu event) {
if (event.state() == State.TERMINATING) {
if (event.runState() == RunState.TERMINATING) {
return;
}
protocol

View file

@ -61,7 +61,7 @@ import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.QmpConfigured;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
import org.jdrupes.vmoperator.util.FsdUtils;
import org.jgrapes.core.Channel;
@ -217,7 +217,7 @@ public class Runner extends Component {
private CommandDefinition qemuDefinition;
private final QemuMonitor qemuMonitor;
private Integer resetCounter;
private State state = State.INITIALIZING;
private RunState state = RunState.INITIALIZING;
/** Preparatory actions for QEMU start */
@SuppressWarnings("PMD.FieldNamingConventions")
@ -467,7 +467,7 @@ public class Runner extends Component {
*/
@Handler
public void onStarted(Started event) {
state = State.STARTING;
state = RunState.STARTING;
rep.fire(new RunnerStateChange(state, "RunnerStarted",
"Runner has been started"));
// Start first process(es)
@ -618,9 +618,9 @@ public class Runner extends Component {
*/
@Handler(priority = -1000)
public void onConfigureQemuFinal(ConfigureQemu event) {
if (state == State.STARTING) {
if (state == RunState.STARTING) {
fire(new MonitorCommand(new QmpCont()));
state = State.RUNNING;
state = RunState.RUNNING;
rep.fire(new RunnerStateChange(state, "VmStarted",
"Qemu has been configured and is continuing"));
}
@ -633,7 +633,7 @@ public class Runner extends Component {
*/
@Handler
public void onConfigureQemu(ConfigureQemu event) {
if (state == State.RUNNING) {
if (state == RunState.RUNNING) {
if (resetCounter != null
&& event.configuration().resetCounter != null
&& event.configuration().resetCounter > resetCounter) {
@ -659,14 +659,14 @@ public class Runner extends Component {
return;
}
// No other process(es) may exit during startup
if (state == State.STARTING) {
if (state == RunState.STARTING) {
logger.severe(() -> "Process " + procDef.name
+ " has exited with value " + event.exitValue()
+ " during startup.");
rep.fire(new Stop());
return;
}
if (procDef.equals(qemuDefinition) && state == State.RUNNING) {
if (procDef.equals(qemuDefinition) && state == RunState.RUNNING) {
rep.fire(new Exit(event.exitValue()));
}
logger.info(() -> "Process " + procDef.name
@ -693,7 +693,7 @@ public class Runner extends Component {
*/
@Handler(priority = 10_000)
public void onStopFirst(Stop event) {
state = State.TERMINATING;
state = RunState.TERMINATING;
rep.fire(new RunnerStateChange(state, "VmTerminating",
"The VM is being shut down", exitStatus != 0));
}
@ -705,14 +705,14 @@ public class Runner extends Component {
*/
@Handler(priority = -10_000)
public void onStopLast(Stop event) {
state = State.STOPPED;
state = RunState.STOPPED;
rep.fire(new RunnerStateChange(state, "VmStopped",
"The VM has been shut down"));
}
@SuppressWarnings("PMD.ConfusingArgumentToVarargsMethod")
private void shutdown() {
if (!Set.of(State.TERMINATING, State.STOPPED).contains(state)) {
if (!Set.of(RunState.TERMINATING, RunState.STOPPED).contains(state)) {
fire(new Stop());
}
try {

View file

@ -48,7 +48,7 @@ import org.jdrupes.vmoperator.runner.qemu.events.DisplayPasswordChanged;
import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
@ -65,8 +65,8 @@ import org.jgrapes.util.events.InitialConfiguration;
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class StatusUpdater extends Component {
private static final Set<State> RUNNING_STATES
= Set.of(State.RUNNING, State.TERMINATING);
private static final Set<RunState> RUNNING_STATES
= Set.of(RunState.RUNNING, RunState.TERMINATING);
private String namespace;
private String vmName;
@ -240,11 +240,11 @@ public class StatusUpdater extends Component {
updateRunningCondition(event, from, cond);
}
});
if (event.state() == State.STARTING) {
if (event.runState() == RunState.STARTING) {
status.addProperty("ram", GsonPtr.to(from.data())
.getAsString("spec", "vm", "maximumRam").orElse("0"));
status.addProperty("cpus", 1);
} else if (event.state() == State.STOPPED) {
} else if (event.runState() == RunState.STOPPED) {
status.addProperty("ram", "0");
status.addProperty("cpus", 0);
}
@ -252,7 +252,7 @@ public class StatusUpdater extends Component {
});
// Maybe stop VM
if (event.state() == State.TERMINATING && !event.failed()
if (event.runState() == RunState.TERMINATING && !event.failed()
&& guestShutdownStops && shutdownByGuest) {
logger.info(() -> "Stopping VM because of shutdown by guest.");
var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
@ -277,13 +277,13 @@ public class StatusUpdater extends Component {
K8sDynamicModel from, JsonObject cond) {
boolean reportedRunning
= "True".equals(cond.get("status").getAsString());
if (RUNNING_STATES.contains(event.state())
if (RUNNING_STATES.contains(event.runState())
&& !reportedRunning) {
cond.addProperty("status", "True");
cond.addProperty("lastTransitionTime",
Instant.now().toString());
}
if (!RUNNING_STATES.contains(event.state())
if (!RUNNING_STATES.contains(event.runState())
&& reportedRunning) {
cond.addProperty("status", "False");
cond.addProperty("lastTransitionTime",

View file

@ -19,7 +19,7 @@
package org.jdrupes.vmoperator.runner.qemu.events;
import org.jdrupes.vmoperator.runner.qemu.Configuration;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Event;
@ -34,14 +34,14 @@ import org.jgrapes.core.Event;
public class ConfigureQemu extends Event<Void> {
private final Configuration configuration;
private final State state;
private final RunState state;
/**
* Instantiates a new configuration event.
*
* @param channels the channels
*/
public ConfigureQemu(Configuration configuration, State state,
public ConfigureQemu(Configuration configuration, RunState state,
Channel... channels) {
super(channels);
this.state = state;
@ -62,7 +62,7 @@ public class ConfigureQemu extends Event<Void> {
*
* @return the state
*/
public State state() {
public RunState runState() {
return state;
}
}

View file

@ -31,11 +31,11 @@ public class RunnerStateChange extends Event<Void> {
/**
* The state.
*/
public enum State {
public enum RunState {
INITIALIZING, STARTING, RUNNING, TERMINATING, STOPPED
}
private final State state;
private final RunState state;
private final String reason;
private final String message;
private final boolean failed;
@ -48,7 +48,7 @@ public class RunnerStateChange extends Event<Void> {
* @param message the message
* @param channels the channels
*/
public RunnerStateChange(State state, String reason, String message,
public RunnerStateChange(RunState state, String reason, String message,
Channel... channels) {
this(state, reason, message, false, channels);
}
@ -62,7 +62,7 @@ public class RunnerStateChange extends Event<Void> {
* @param failed the failed
* @param channels the channels
*/
public RunnerStateChange(State state, String reason, String message,
public RunnerStateChange(RunState state, String reason, String message,
boolean failed, Channel... channels) {
super(channels);
this.state = state;
@ -76,7 +76,7 @@ public class RunnerStateChange extends Event<Void> {
*
* @return the state
*/
public State state() {
public RunState runState() {
return state;
}

View file

@ -50,7 +50,7 @@ public class TrayMovedEvent extends MonitorEvent {
*
* @return the tray state
*/
public TrayState state() {
public TrayState trayState() {
return data().get("tray-open").asBoolean()
? TrayState.OPEN
: TrayState.CLOSED;

3158
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,10 @@
"typedoc-plugin-missing-exports": "^2.1.0",
"typescript": "^5.2.2"
},
"overrides": {
"node-gyp": "^10.1.0",
"glob": "^9.0.0"
},
"eslintConfig": {
"root": true,
"extends": "eslint:recommended",

View file

@ -1,4 +1,4 @@
FROM alpine:3.19
FROM docker.io/alpine:3.19
RUN apk update &&\
apk add --no-cache inotify-tools &&\

4
webpages/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
_site
Gemfile.lock
.bundle
.jekyll-cache

5
webpages/Gemfile Normal file
View file

@ -0,0 +1,5 @@
source 'https://rubygems.org'
# gem 'github-pages', group: :jekyll_plugins
gem "jekyll", "~> 4.0"
gem "jekyll-seo-tag"
gem 'webrick', '~> 1.3', '>= 1.3.1'

10
webpages/_config.yml Normal file
View file

@ -0,0 +1,10 @@
plugins:
- jekyll-seo-tag
author: Michael N. Lipp
logo: VM-Operator.svg
tagline: VM-Operator by mnlipp
description: A Kubernetes operator for running virtual machines (notably Qemu VMs) as pods.

View file

@ -0,0 +1,23 @@
<!-- Matomo anonymous, no cookies (https://matomo.org/blog/2018/04/how-to-not-process-any-personal-data-with-matomo-and-what-it-means-for-you/) -->
<script type="text/javascript">
var _paq = _paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
_paq.push(["setCookieDomain", "*.mnlipp.github.io"]);
_paq.push(["setDomains", ["*.mnlipp.github.io"]]);
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//piwik.mnl.de/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '14']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript>
<img src="https://piwik.mnl.de/piwik.php?idsite=14&amp;rec=1&amp;action_name=JGrapes" style="border:0" alt="" />
</noscript>
<!-- End Matomo Code -->

View file

@ -0,0 +1,96 @@
{% capture tocWorkspace %}
{% comment %}
Version 1.0.10
https://github.com/allejo/jekyll-toc
"...like all things liquid - where there's a will, and ~36 hours to spare, there's usually a/some way" ~jaybe
Usage:
{% include toc.html html=content sanitize=true class="inline_toc" id="my_toc" h_min=2 h_max=3 %}
Parameters:
* html (string) - the HTML of compiled markdown generated by kramdown in Jekyll
Optional Parameters:
* sanitize (bool) : false - when set to true, the headers will be stripped of any HTML in the TOC
* class (string) : '' - a CSS class assigned to the TOC
* id (string) : '' - an ID to assigned to the TOC
* h_min (int) : 1 - the minimum TOC header level to use; any header lower than this value will be ignored
* h_max (int) : 6 - the maximum TOC header level to use; any header greater than this value will be ignored
* ordered (bool) : false - when set to true, an ordered list will be outputted instead of an unordered list
* item_class (string) : '' - add custom class(es) for each list item; has support for '%level%' placeholder, which is the current heading level
* baseurl (string) : '' - add a base url to the TOC links for when your TOC is on another page than the actual content
* anchor_class (string) : '' - add custom class(es) for each anchor element
Output:
An ordered or unordered list representing the table of contents of a markdown block. This snippet will only
generate the table of contents and will NOT output the markdown given to it
{% endcomment %}
{% capture my_toc %}{% endcapture %}
{% assign orderedList = include.ordered | default: false %}
{% assign minHeader = include.h_min | default: 1 %}
{% assign maxHeader = include.h_max | default: 6 %}
{% assign nodes = include.html | split: '<h' %}
{% assign firstHeader = true %}
{% capture listModifier %}{% if orderedList %}1.{% else %}-{% endif %}{% endcapture %}
{% for node in nodes %}
{% if node == "" %}
{% continue %}
{% endif %}
{% assign headerLevel = node | replace: '"', '' | slice: 0, 1 | times: 1 %}
{% if headerLevel < minHeader or headerLevel > maxHeader %}
{% continue %}
{% endif %}
{% if firstHeader %}
{% assign firstHeader = false %}
{% assign minHeader = headerLevel %}
{% endif %}
{% assign indentAmount = headerLevel | minus: minHeader %}
{% assign _workspace = node | split: '</h' %}
{% assign _idWorkspace = _workspace[0] | split: 'id="' %}
{% assign _idWorkspace = _idWorkspace[1] | split: '"' %}
{% assign html_id = _idWorkspace[0] %}
{% assign _classWorkspace = _workspace[0] | split: 'class="' %}
{% assign _classWorkspace = _classWorkspace[1] | split: '"' %}
{% assign html_class = _classWorkspace[0] %}
{% if html_class contains "no_toc" %}
{% continue %}
{% endif %}
{% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %}
{% assign header = _workspace[0] | replace: _hAttrToStrip, '' %}
{% assign space = '' %}
{% for i in (1..indentAmount) %}
{% assign space = space | prepend: ' ' %}
{% endfor %}
{% if include.item_class and include.item_class != blank %}
{% capture listItemClass %}{:.{{ include.item_class | replace: '%level%', headerLevel }}}{% endcapture %}
{% endif %}
{% capture heading_body %}{% if include.sanitize %}{{ header | strip_html }}{% else %}{{ header }}{% endif %}{% endcapture %}
{% capture my_toc %}{{ my_toc }}
{{ space }}{{ listModifier }} {{ listItemClass }} [{{ heading_body | replace: "|", "\|" }}]({% if include.baseurl %}{{ include.baseurl }}{% endif %}#{{ html_id }}){% if include.anchor_class %}{:.{{ include.anchor_class }}}{% endif %}{% endcapture %}
{% endfor %}
{% if include.class and include.class != blank %}
{% capture my_toc %}{:.{{ include.class }}}
{{ my_toc | lstrip }}{% endcapture %}
{% endif %}
{% if include.id %}
{% capture my_toc %}{: #{{ include.id }}}
{{ my_toc | lstrip }}{% endcapture %}
{% endif %}
{% endcapture %}{% assign tocWorkspace = '' %}{{ my_toc | markdownify | strip }}

View file

@ -0,0 +1,71 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="referrer" content="no-referrer">
<link rel="icon" type="image/svg+xml" href="favicon.svg" sizes="any">
<link rel="stylesheet" href="../stylesheets/styles.css">
<link rel="stylesheet" href="../stylesheets/pygment_trac.css">
<meta name="viewport" content="width=device-width">
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
{% seo %}
</head>
<body>
<div class="wrapper">
<header>
<div>
<div style="float: left;">
<h1><a style="color: #222;" href="http://vm-operator.jdrupes.org">VM-Operator</a></h1>
<h3>By <a href="https://github.com/mnlipp">Michael N. Lipp</a></h3>
<p><a rel="me" href="https://fosstodon.org/@mnl"><img alt="Mastodon Follow"
src="https://img.shields.io/mastodon/follow/108843609567976408?domain=https%3A%2F%2Ffosstodon.org&style=social"></a></p>
</div>
<div style="float: right; width: 7em;">
<img alt="VM-Operator Logo" src="VM-Operator.svg">
</div>
<div style="clear:both;"></div>
</div>
<p></p>
<p class="view"><a href="https://github.com/mnlipp/VM-Operator">View GitHub Project</a></p>
<p></p>
<p class="part-entry"><a href="index.html">Overview</a></p>
<p class="part-entry"><a href="runner.html">The Runner</a></p>
<p class="part-list-title"><a href="manager.html">The Manager</a></p>
<ul style="margin-bottom: 0;" class="no-bullets">
<li><p class="part-entry"><a href="controller.html">The Controller</a></p></li>
</ul>
<p class="part-list-title"><a href="webgui.html">The Web-GUI</a></p>
<ul style="margin-bottom: 0;" class="no-bullets">
<li><p class="part-entry"><a href="admin-gui.html">For Admins</a></p></li>
<li><p class="part-entry"><a href="user-gui.html">For Users</a></p></li>
</ulstyle="margin-bottom: 0;">
<p class="part-list-title"><a href="upgrading.html">Upgrading</a></p>
<p class="part-list-title"><a href="latest-release/javadoc/index.html">Javadoc</a></p>
</header>
<section>
<div class="post-date"><span class="post-meta">{{ page.date | date: "%b %-d, %Y" }}</span></div>
{% if page.tocTitle %}
<h1>{{ page.tocTitle }}</h1>
{% include toc.html html=content %}
{% endif %}
{{ content }}
</section>
<footer>
<p><small>Hosted on GitHub Pages &mdash; <a href="https://github.com/site/terms">Terms</a>
&mdash; <a href="https://github.com/site/privacy">Privacy</a>
&mdash; Theme derived from <a href="https://github.com/orderedlist/minimal">minimal</a></small></p>
</footer>
</div>
{% include matomo.html %}
</body>
</html>

View file

@ -0,0 +1,69 @@
.highlight { background: #ffffff; }
.highlight .c { color: #999988; font-style: italic } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { font-weight: bold } /* Keyword */
.highlight .o { font-weight: bold } /* Operator */
.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */
.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */
.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #999999 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { font-weight: bold } /* Keyword.Constant */
.highlight .kd { font-weight: bold } /* Keyword.Declaration */
.highlight .kn { font-weight: bold } /* Keyword.Namespace */
.highlight .kp { font-weight: bold } /* Keyword.Pseudo */
.highlight .kr { font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #009999 } /* Literal.Number */
.highlight .s { color: #d14 } /* Literal.String */
.highlight .na { color: #008080 } /* Name.Attribute */
.highlight .nb { color: #0086B3 } /* Name.Builtin */
.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */
.highlight .no { color: #008080 } /* Name.Constant */
.highlight .ni { color: #800080 } /* Name.Entity */
.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */
.highlight .nn { color: #555555 } /* Name.Namespace */
.highlight .nt { color: #000080 } /* Name.Tag */
.highlight .nv { color: #008080 } /* Name.Variable */
.highlight .ow { font-weight: bold } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mf { color: #009999 } /* Literal.Number.Float */
.highlight .mh { color: #009999 } /* Literal.Number.Hex */
.highlight .mi { color: #009999 } /* Literal.Number.Integer */
.highlight .mo { color: #009999 } /* Literal.Number.Oct */
.highlight .sb { color: #d14 } /* Literal.String.Backtick */
.highlight .sc { color: #d14 } /* Literal.String.Char */
.highlight .sd { color: #d14 } /* Literal.String.Doc */
.highlight .s2 { color: #d14 } /* Literal.String.Double */
.highlight .se { color: #d14 } /* Literal.String.Escape */
.highlight .sh { color: #d14 } /* Literal.String.Heredoc */
.highlight .si { color: #d14 } /* Literal.String.Interpol */
.highlight .sx { color: #d14 } /* Literal.String.Other */
.highlight .sr { color: #009926 } /* Literal.String.Regex */
.highlight .s1 { color: #d14 } /* Literal.String.Single */
.highlight .ss { color: #990073 } /* Literal.String.Symbol */
.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */
.highlight .vc { color: #008080 } /* Name.Variable.Class */
.highlight .vg { color: #008080 } /* Name.Variable.Global */
.highlight .vi { color: #008080 } /* Name.Variable.Instance */
.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */
.type-csharp .highlight .k { color: #0000FF }
.type-csharp .highlight .kt { color: #0000FF }
.type-csharp .highlight .nf { color: #000000; font-weight: normal }
.type-csharp .highlight .nc { color: #2B91AF }
.type-csharp .highlight .nn { color: #000000 }
.type-csharp .highlight .s { color: #A31515 }
.type-csharp .highlight .sc { color: #A31515 }

View file

@ -0,0 +1,244 @@
body {
background-color: #fff;
padding:50px;
font: normal 16px/1.5 Verdana, Arial, Helvetica, sans-serif;
color:#595959;
}
h1, h2, h3, h4, h5, h6 {
color:#222;
margin:0 0 20px;
}
p, ul, ol, table, pre, dl {
margin:0 0 20px;
}
h1, h2, h3 {
line-height:1.1;
}
h1 {
font-size:28px;
font-weight: 500;
}
h2 {
color:#393939;
font-weight: 500;
}
h3, h4, h5, h6 {
color:#494949;
font-weight: 500;
}
a {
color:#39c;
text-decoration:none;
}
a:hover {
color:#069;
}
a small {
font-size:11px;
color:#777;
margin-top:-0.3em;
display:block;
}
a:hover small {
color:#777;
}
.wrapper {
/* width:860px; */
width: 100%;
margin:0 auto;
}
blockquote {
border-left:1px solid #e5e5e5;
margin:0;
padding:0 0 0 20px;
font-style:italic;
}
code, pre {
font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, Consolas, Liberation Mono, DejaVu Sans Mono, Courier New, monospace;
color:#333;
}
pre {
font-size: 15px;
padding:8px 15px;
background: #f8f8f8;
border-radius:5px;
border:1px solid #e5e5e5;
overflow-x: auto;
}
a code {
color: inherit;
}
table {
width:100%;
border-collapse:collapse;
}
th, td {
text-align:left;
padding:5px 10px;
border-bottom:1px solid #e5e5e5;
}
dt {
color:#444;
font-weight:500;
}
th {
color:#444;
}
img {
max-width:100%;
}
header {
/* width:270px; */
width:calc(29% - 50px);
height:calc(100% - 160px);
overflow: auto;
float:left;
position:fixed;
-webkit-font-smoothing:subpixel-antialiased;
}
header li {
list-style-type: disc;
}
header ul {
padding-left: 1rem;
}
header ul > li {
margin-left: 1rem;
}
ul.no-bullets {
padding-left: 0;
}
ul.no-bullets > li {
list-style: none;
}
strong {
color:#222;
font-weight:500;
}
section {
width:70%;
max-width:54em;
float:right;
padding-bottom:50px;
}
small {
font-size:11px;
}
hr {
border:0;
background:#e5e5e5;
height:1px;
margin:0 0 20px;
}
footer {
/* width:270px; */
width:calc(24% - 50px);
height:40px;
float:left;
position:fixed;
padding:30px 0;
bottom:0px;
background-color:white;
-webkit-font-smoothing:subpixel-antialiased;
}
.post-date {
float: right;
}
.part-list-title {
margin-bottom:5px;
}
.part-entry {
margin-bottom:5px;
}
@media print, screen and (max-width: 960px) {
div.wrapper {
width:auto;
margin:0;
}
header, section, footer {
float:none;
position:static;
width:auto;
}
header {
padding-right:320px;
}
section {
border:1px solid #e5e5e5;
border-width:1px 0;
padding:20px 0;
margin:0 0 20px;
}
header a small {
display:inline;
}
}
@media print, screen and (max-width: 720px) {
body {
word-wrap:break-word;
}
header {
padding:0;
}
pre, code {
word-wrap:normal;
}
}
@media print, screen and (max-width: 480px) {
body {
padding:15px;
}
}
@media print {
body {
padding:0.4in;
font-size:12pt;
color:#444;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="93.557968mm"
height="91.220795mm"
viewBox="0 0 331.50461 323.22329"
id="svg2"
version="1.1"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="VM-Operator.svg"
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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="293.57143"
inkscape:cy="145.71429"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(799.83239,410.74206)">
<g
id="path300"
inkscape:transform-center-x="-0.49891951"
inkscape:transform-center-y="-10.814906"
transform="matrix(0.93749998,0,0,0.93749998,-364.15225,128.12438)">
<path
sodipodi:type="star"
style="fill:#326de6;fill-opacity:1;stroke:#ffffff;stroke-linecap:square;stroke-miterlimit:0;paint-order:fill markers stroke"
id="path1033"
inkscape:flatsided="false"
sodipodi:sides="7"
sodipodi:cx="-790.008"
sodipodi:cy="-357.15076"
sodipodi:r1="221.23064"
sodipodi:r2="199.10757"
sodipodi:arg1="1.1215879"
sodipodi:arg2="1.5605315"
inkscape:rounded="0"
inkscape:randomized="0"
d="m -693.93801,-157.86816 -94.02622,-0.18551 -97.95052,0.26412 -58.47935,-73.62832 -61.2776,-76.41613 21.10362,-91.6275 21.53854,-95.55347 84.79518,-40.6293 88.13578,-42.7371 84.6342,40.96358 88.36497,42.26117 20.74194,91.71007 22.05354,95.43592 -58.76943,73.39699 z"
transform="matrix(0.81788201,0,0,0.81788201,358.19384,-101.37507)"
inkscape:transform-center-x="1.2804791"
inkscape:transform-center-y="-8.9686433" />
</g>
<text
xml:space="preserve"
style="font-size:16.6665px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';letter-spacing:0px;word-spacing:0px;fill:none;stroke:#ffffff;stroke-width:1.04165;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
x="-753.07837"
y="-183.35805"
id="text300"><tspan
sodipodi:role="line"
id="tspan298"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:semi-condensed;font-size:199.997px;font-family:'Nimbus Sans Narrow';-inkscape-font-specification:'Nimbus Sans Narrow, Bold Semi-Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff6600;fill-opacity:1;stroke:#ffffff;stroke-width:1.04165;stroke-dasharray:none;stroke-opacity:1"
x="-753.07837"
y="-183.35805">VM</tspan></text>
<g
id="g1773"
transform="matrix(1.5551014,0,0,1.5551014,-923.85519,-409.37793)"
style="stroke:#ffffff;stroke-opacity:1">
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:0.692134;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136"
cx="149.36122"
cy="159.37894"
r="5.9195638" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:0.692134;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-8"
cx="162.51581"
cy="159.37894"
r="5.9195638" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:0.692134;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-1"
cx="175.67039"
cy="159.37894"
r="5.9195638" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:0.692134;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-2"
cx="188.82498"
cy="159.37894"
r="5.9195638" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:0.692134;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-8-8"
cx="155.7412"
cy="170.6261"
r="5.9195638" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:0.692134;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-1-9"
cx="168.89577"
cy="170.6261"
r="5.9195638" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:0.692134;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-2-3"
cx="182.05035"
cy="170.6261"
r="5.9195638" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:0.692134;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-8-6"
cx="162.25272"
cy="182.0706"
r="5.9195638" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:0.692134;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-1-8"
cx="175.4073"
cy="182.0706"
r="5.9195638" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:0.692134;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-2-0"
cx="168.63269"
cy="193.12045"
r="5.9195638" />
<g
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:180px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial Bold';letter-spacing:0px;word-spacing:0px;fill:#238220;fill-opacity:1;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="flowRoot4814"
transform="matrix(0.16182744,0,0,0.16070376,137.53832,122.72639)">
<path
d="m 210.10352,57.161102 h 25.92773 V 138.7236 q 0,15.9961 -2.8125,24.60938 -3.7793,11.25 -13.71094,18.10547 -9.93164,6.76757 -26.1914,6.76757 -19.07227,0 -29.35547,-10.63476 -10.28321,-10.72266 -10.3711,-31.37696 l 24.52149,-2.8125 q 0.43945,11.07422 3.25195,15.64454 4.21875,6.94336 12.83203,6.94336 8.70117,0 12.30469,-4.92188 3.60352,-5.00977 3.60352,-20.6543 z"
style="fill:#238220;fill-opacity:1;stroke:#ffffff;stroke-opacity:1"
id="path4823"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

View file

@ -0,0 +1,184 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="93.557968mm"
height="91.220795mm"
viewBox="0 0 331.50461 323.22329"
id="svg2"
version="1.1"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="VM-Operator.svg"
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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="245"
inkscape:cy="145.71429"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(799.83239,410.74206)">
<g
id="path300"
inkscape:transform-center-x="-0.49891951"
inkscape:transform-center-y="-10.814906"
transform="matrix(0.93749998,0,0,0.93749998,-364.15225,128.12438)">
<path
sodipodi:type="star"
style="fill:#326de6;fill-opacity:1;stroke:#ffffff;stroke-linecap:square;stroke-miterlimit:0;paint-order:fill markers stroke"
id="path1033"
inkscape:flatsided="false"
sodipodi:sides="7"
sodipodi:cx="-790.008"
sodipodi:cy="-357.15076"
sodipodi:r1="221.23064"
sodipodi:r2="199.10757"
sodipodi:arg1="1.1215879"
sodipodi:arg2="1.5605315"
inkscape:rounded="0"
inkscape:randomized="0"
d="m -693.93801,-157.86816 -94.02622,-0.18551 -97.95052,0.26412 -58.47935,-73.62832 -61.2776,-76.41613 21.10362,-91.6275 21.53854,-95.55347 84.79518,-40.6293 88.13578,-42.7371 84.6342,40.96358 88.36497,42.26117 20.74194,91.71007 22.05354,95.43592 -58.76943,73.39699 z"
transform="matrix(0.81788201,0,0,0.81788201,358.19384,-101.37507)"
inkscape:transform-center-x="1.2804791"
inkscape:transform-center-y="-8.9686433" />
</g>
<g
aria-label="VM"
id="text300"
style="font-size:16.6665px;-inkscape-font-specification:'sans-serif, Normal';letter-spacing:0px;word-spacing:0px;fill:none;stroke:#ffffff;stroke-width:1.04165">
<g
id="path305">
<path
style="color:#000000;-inkscape-font-specification:'Nimbus Sans Narrow, Bold Semi-Condensed';fill:#ff6600;stroke:none;-inkscape-stroke:none"
d="m -698.0792,-217.35754 -25.39961,-109.59835 h -26.39961 l 39.59941,143.59784 h 23.39965 l 39.99939,-143.59784 h -25.59961 z"
id="path312" />
<path
style="color:#000000;-inkscape-font-specification:'Nimbus Sans Narrow, Bold Semi-Condensed';fill:#ffffff;stroke:none;-inkscape-stroke:none"
d="m -750.5625,-327.47656 39.88672,144.63867 h 24.19141 l 40.29101,-144.63867 h -26.69922 l -25.18554,107.82226 -24.98633,-107.82226 z m 1.36719,1.04101 h 25.30273 l 25.30664,109.19532 1.01367,0.002 25.50586,-109.19727 h 24.50196 l -39.71094,142.55664 h -22.60742 z"
id="path325" />
</g>
<g
id="path307">
<path
style="color:#000000;-inkscape-font-specification:'Nimbus Sans Narrow, Bold Semi-Condensed';fill:#ff6600;stroke:none;-inkscape-stroke:none"
d="m -518.28172,-326.95589 h -35.39947 l -21.19968,113.99829 -21.59968,-113.99829 h -35.79946 v 143.59784 h 22.79966 v -121.79817 l 21.79967,121.79817 h 24.19963 l 22.39967,-121.79817 v 121.79817 h 22.79966 z"
id="path318" />
<path
style="color:#000000;-inkscape-font-specification:'Nimbus Sans Narrow, Bold Semi-Condensed';fill:#ffffff;stroke:none;-inkscape-stroke:none"
d="m -632.80078,-327.47656 v 144.63867 h 23.8418 v -116.45313 l 20.84179,116.45313 h 25.07032 l 21.44531,-116.60742 v 116.60742 h 23.83984 v -144.63867 h -0.51953 -35.83203 l -20.77149,111.69726 -21.16406,-111.69726 z m 1.04101,1.04101 h 34.84766 l 21.51953,113.57422 1.02344,-0.002 21.12109,-113.57227 h 34.44532 v 142.55664 h -21.75782 v -121.27734 l -1.0332,-0.0937 -22.32031,121.37109 h -23.33008 l -21.72266,-121.36914 -1.03515,0.0918 v 121.27734 h -21.75782 z"
id="path320" />
</g>
</g>
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136"
cx="-691.58337"
cy="-161.52753"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-8"
cx="-671.12665"
cy="-161.52753"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-1"
cx="-650.66992"
cy="-161.52753"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-2"
cx="-630.2132"
cy="-161.52753"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-8-8"
cx="-681.66187"
cy="-144.03705"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-1-9"
cx="-661.20514"
cy="-144.03705"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-2-3"
cx="-640.74841"
cy="-144.03705"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-8-6"
cx="-671.53577"
cy="-126.23969"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-1-8"
cx="-651.07904"
cy="-126.23969"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-2-0"
cx="-661.61426"
cy="-109.05605"
r="9.2055216" />
<g
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:180px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial Bold';letter-spacing:0px;word-spacing:0px;fill:#238220;fill-opacity:1;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="flowRoot4814"
transform="matrix(0.25165808,0,0,0.24991064,-709.96916,-218.52595)">
<path
d="m 210.10352,57.161102 h 25.92773 V 138.7236 q 0,15.9961 -2.8125,24.60938 -3.7793,11.25 -13.71094,18.10547 -9.93164,6.76757 -26.1914,6.76757 -19.07227,0 -29.35547,-10.63476 -10.28321,-10.72266 -10.3711,-31.37696 l 24.52149,-2.8125 q 0.43945,11.07422 3.25195,15.64454 4.21875,6.94336 12.83203,6.94336 8.70117,0 12.30469,-4.92188 3.60352,-5.00977 3.60352,-20.6543 z"
style="fill:#238220;fill-opacity:1;stroke:#ffffff;stroke-opacity:1"
id="path4823"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,18 @@
---
title: VM-Operator Web-GUI for Admins
layout: vm-operator
---
# Administrator view
An overview display shows the current CPU and RAM usage and a graph
with recent changes.
![VM-Operator GUI](VM-Operator-GUI-preview.png)
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
in kubernetes).
![VM-Operator GUI](VM-Operator-GUI-view.png)

View file

@ -0,0 +1,226 @@
---
title: VM-Operator Controller
layout: vm-operator
---
# The Controller
The controller component (which is part of the manager) monitors
custom resources of kind `VirtualMachine`. It creates or modifies
other resources in the cluster as required to get the VM defined
by the CR up and running.
Here is the sample definition of a VM from the
["local-path" example](https://github.com/mnlipp/VM-Operator/tree/main/example/local-path):
```yaml
apiVersion: "vmoperator.jdrupes.org/v1"
kind: VirtualMachine
metadata:
namespace: vmop-demo
name: test-vm
spec:
guestShutdownStops: false
vm:
state: Running
maximumCpus: 4
currentCpus: 2
maximumRam: 8Gi
currentRam: 4Gi
networks:
- user: {}
disks:
- volumeClaimTemplate:
metadata:
name: system
spec:
storageClassName: ""
selector:
matchLabels:
app.kubernetes.io/name: vmrunner
app.kubernetes.io/instance: test-vm
vmrunner.jdrupes.org/disk: system
resources:
requests:
storage: 40Gi
- cdrom:
image: ""
# image: https://download.fedoraproject.org/pub/fedora/linux/releases/38/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-38-1.6.iso
# image: "Fedora-Workstation-Live-x86_64-38-1.6.iso"
display:
spice:
port: 5910
# Since 3.0.0:
# generateSecret: false
```
## Pod management
The central resource created by the controller is a
[stateful set](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)
with the same name as the VM (metadata.name). Its number of replicas is
set to 1 if `spec.vm.state` is "Running" (default is "Stopped" which sets
replicas to 0).
Property `spec.guestShutdownStops` (since 2.2.0) controls the effect of a
shutdown initiated by the guest. If set to `false` (default) a new pod
is automatically created by the stateful set controller and the VM thus
restarted. If set to `true`, the runner sets `spec.vm.state` to "Stopped"
before terminating and by this prevents the creation of a new pod.
## Defining the basics
How to define the number of CPUs and the size of the RAM of the VM
should be obvious from the example. Note that changes of the current
number of CPUs and the current RAM size will be propagated to
running VMs.
## Defining disks
Maybe the most interesting part is the definition of the VM's disks.
This is done by adding one or more `volumeClaimTemplate`s to the
list of disks. As its name suggests, such a template is used by the
controller to generate a PVC.
The example template does not define any storage. Rather it references
some PV that you must have created first. This may be your first approach
if you have existing storage from running the VM outside Kubernetes
(e.g. with libvirtd).
If you have ceph or some other full fledged storage provider installed
and create a new VM, provisioning a disk can happen automatically
as shown in this example:
```yaml
disks:
- volumeClaimTemplate:
metadata:
name: system
spec:
storageClassName: rook-ceph-block
resources:
requests:
storage: 40Gi
```
The disk will be available as "/dev/*name*-disk" in the VM,
using the string from `.volumeClaimTemplate.metadata.name` as *name*.
If no name is defined in the metadata, then "/dev/disk-*n*"
is used instead, with *n* being the index of the disk
definition in the list of disks.
Apart from appending "-disk" to the name (or generating the name) the
`volumeClaimTemplate` is simply copied into the stateful set definition
for the VM (with some additional labels, see below). The controller
for stateful sets appends the started pod's name to the name of the
volume claim templates when it creates the PVCs. Therefore you'll
eventually find the PVCs as "*name*-disk-*vmName*-0"
(or "disk-*n*-*vmName*-0").
PVCs generated from stateful set definitions are considered "precious"
and never removed automatically. This behavior fits perfectly for VMs.
Usually, you do not want the disks to be removed automatically when
you (maybe accidentally) remove the CR for the VM. To simplify the lookup
for an eventual (manual) removal, all PVCs are labeled with
"app.kubernetes.io/name: vm-runner", "app.kubernetes.io/instance: *vmName*",
and "app.kubernetes.io/managed-by: vm-operator".
## Choosing an image for the runner
The image used for the runner can be configured with
[`spec.image`](https://github.com/mnlipp/VM-Operator/blob/7e094e720b7b59a5e50f4a9a4ad29a6000ec76e6/deploy/crds/vms-crd.yaml#L19).
This is a mapping with either a single key `source` or a detailed
configuration using the keys `repository`, `path` etc.
Currently two runner images are maintained. One that is based on
Arch Linux (`ghcr.io/mnlipp/org.jdrupes.vmoperator.runner.qemu-arch`) and a
second one based on Alpine (`ghcr.io/mnlipp/org.jdrupes.vmoperator.runner.qemu-alpine`).
Starting with release 1.0, all versions of runner images and managers
that have the same major release number are guaranteed to be compatible.
## Generating cloud-init data
*Since: 2.2.0*
The optional object `.spec.cloudInit` with sub-objects `.cloudInit.metaData`,
`.cloudInit.userData` and `.cloudInit.networkConfig` can be used to provide
data for
[cloud-init](https://cloudinit.readthedocs.io/en/latest/index.html).
The data from the CRD will be made available to the VM by the runner
as a vfat formatted disk (see the description of
[NoCloud](https://cloudinit.readthedocs.io/en/latest/reference/datasources/nocloud.html)).
If `.metaData.instance-id` is not defined, the controller automatically
generates it from the CRD's `resourceVersion`. If `.metaData.local-hostname`
is not defined, the controller adds this property using the value from
`metadata.name`.
Note that there are no schema definitions available for `.userData`
and `.networkConfig`. Whatever is defined in the CRD is copied to
the corresponding cloud-init file without any checks. (The introductory
comment `#cloud-config` required at the beginning of `.userData` is
generated automatically by the runner.)
## Display secret/password
*Since: 2.3.0*
You can define a display password using a Kubernetes secret.
When you start a VM, the controller checks if there is a secret
with labels "app.kubernetes.io/name: vm-runner,
app.kubernetes.io/component: display-secret,
app.kubernetes.io/instance: *vmname*" in the namespace of the
VM definition. The name of the secret can be chosen freely.
```yaml
kind: Secret
apiVersion: v1
metadata:
name: test-vm-display-secret
namespace: vmop-demo
labels:
app.kubernetes.io/name: vm-runner
app.kubernetes.io/instance: test-vm
app.kubernetes.io/component: display-secret
type: Opaque
data:
display-password: dGVzdC12bQ==
# Since 3.0.0:
# password-expiry: bmV2ZXI=
```
If such a secret for the VM is found, the VM is configured to use
the display password specified. The display password in the secret
can be updated while the VM runs[^delay]. Activating/deactivating
the display password while a VM runs is not supported by Qemu and
therefore requires stopping the VM, adding/removing the secret and
restarting the VM.
[^delay]: Be aware of the possible delay, see e.g.
[here](https://web.archive.org/web/20240223073838/https://ahmet.im/blog/kubernetes-secret-volumes-delay/).
*Since: 3.0.0*
The secret's `data` can have an additional property `data.password-expiry` which
specifies a (base64 encoded) expiry date for the password. Supported
values are those defined by qemu (`+n` seconds from now, `n` Unix
timestamp, `never` and `now`).
Unless `spec.vm.display.spice.generateSecret` is set to `false` in the VM
definition (CRD), the controller creates a secret for the display
password automatically if none is found. The secret is created
with a random password that expires immediately, which makes the
display effectively inaccessible until the secret is modified.
Note that a password set manually may be overwritten by components
of the manager unless the password-expiry is set to "never" or
some time in the future.
## Further reading
For a detailed description of the available configuration options see the
[CRD](https://github.com/mnlipp/VM-Operator/blob/main/deploy/crds/vms-crd.yaml).

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="10.64148mm"
height="12.555316mm"
viewBox="0 0 37.706033 44.487341"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="ML-Logo1.svg"
inkscape:export-filename="/home/mnl/Dokumente/mnl/ML-Logo1.png"
inkscape:export-xdpi="299.41104"
inkscape:export-ydpi="299.41104">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="132.46074"
inkscape:cy="-297.07411"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="1"
fit-margin-left="1"
fit-margin-right="1"
fit-margin-bottom="1"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-175.34341,-117.71255)">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="187.14285"
y="150.93362"
id="text3370"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3372"
x="187.14285"
y="150.93362"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono'">M</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:51.30387497px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="173.50081"
y="158.65659"
id="text3370-6"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3372-5"
x="173.50081"
y="158.65659"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono'">L</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1,60 @@
---
title: VM-Operator by mnlipp
description: A Kubernetes operator for running virtual machines (notably Qemu VMs) in pods on Kubernetes
layout: vm-operator
---
# Welcome to VM-Operator
The goal of this project is to provide the means for running Qemu
based VMs in Kubernetes pods.
The image used for the VM pods combines Qemu and a control program
for starting and managing the Qemu process. This application is called
"[the runner](runner.md)".
While you can deploy a runner manually (or with the help of some
helm templates), the preferred way is to deploy "[the manager](manager.md)"
application which acts as a Kubernetes operator for runners
and thus the VMs.
If you just want to try out things, you can skip the remainder of this
page and proceed to "[the manager](manager.md)".
## Motivation
The project was triggered by a remark in the discussion about RedHat
[dropping SPICE support](https://bugzilla.redhat.com/show_bug.cgi?id=2030592)
from the RHEL packages. Which means that you have to run Qemu in a
container on RHEL and derivatives if you want to continue using Spice.
So KubeVirt comes to mind. But
[one comment](https://bugzilla.redhat.com/show_bug.cgi?id=2030592#c4)
mentioned that the [KubeVirt](https://kubevirt.io/) project isn't
interested in supporting SPICE either.
Time to have a look at alternatives. Libvirt has become a common
tool to configure and run Qemu. But some of its functionality, notably
the management of storage for the VMs and networking is already provided
by Kubernetes. Therefore this project takes a fresh approach of
running Qemu in a pod using a simple, lightweight manager called "runner".
Providing resources to the VM is left to Kubernetes mechanisms as
much as possible.
## VMs and Pods
VMs are not the typical workload managed by Kubernetes. You can neither
have replicas nor can the containers simply be restarted without a major
impact on the "application". So there are many features for managing
pods that we cannot make use of. Qemu in its container can only be
deployed as a pod or using a stateful set with replica 1, which is rather
close to simply deploying the pod (you get the restart and some PVC
management "for free").
A second look, however, reveals that Kubernetes has more to offer.
* It has a well defined API for managing resources.
* It provides access to different kinds of managed storage for the VMs.
* Its managing features *are* useful for running the component that
manages the pods with the VMs.
And if you use Kubernetes anyway, well then the VMs within Kubernetes
provide you with a unified view of all (or most of) your workloads,
which simplifies the maintenance of your platform.

View file

@ -0,0 +1,150 @@
---
title: VM-Operator Manager
layout: vm-operator
---
# The Manager
The Manager is the program that provides the controller from the
[operator pattern](https://github.com/cncf/tag-app-delivery/blob/eece8f7307f2970f46f100f51932db106db46968/operator-wg/whitepaper/Operator-WhitePaper_v1-0.md#operator-components-in-kubernetes)
together with a Web-GUI. It should be run in a container in the cluster.
## Installation
A manager instance manages the VMs in its own namespace. The only
common (and therefore cluster scoped) resource used by all instances
is the CRD. It is available
[here](https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml)
and must be created first.
```sh
kubectl apply -f https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml
```
The example above uses the CRD from the main branch. This is okay if
you apply it once. If you want to preserve the link for automatic
upgrades, you should use a link that points to one of the release branches.
The next step is to create a namespace for the manager and the VMs, e.g.
`vmop-demo`.
```sh
kubectl create namespace vmop-demo
```
Finally you have to create an account, the role, the binding etc. The
default files for creating these resources using the default namespace
can be found in the
[deploy](https://github.com/mnlipp/VM-Operator/tree/main/deploy)
directory. I recommend to use
[kustomize](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/) to create your own configuration.
## Initial Configuration
Use one of the `kustomize.yaml` files from the
[example](https://github.com/mnlipp/VM-Operator/tree/main/example) directory
as a starting point. The directory contains two examples. Here's the file
from subdirectory `local-path`:
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
# Again, I recommend to use the deploy directory from a
# release branch for anything but test environments.
- https://github.com/mnlipp/VM-Operator/deploy
namespace: vmop-demo
patches:
- patch: |-
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: vmop-image-repository
spec:
# Default is ReadOnlyMany
accessModes:
- ReadWriteOnce
resources:
requests:
# Default is 100Gi
storage: 10Gi
# Default is to use the default storage class
storageClassName: local-path
- patch: |-
kind: ConfigMap
apiVersion: v1
metadata:
name: vm-operator
data:
config.yaml: |
"/Manager":
# "/GuiHttpServer":
# See section about the GUI
"/Controller":
"/Reconciler":
runnerDataPvc:
# Default is to use the default storage class
storageClassName: local-path
```
The sample file adds a namespace (`vmop-demo`) to all resource
definitions and patches the PVC `vmop-image-repository`. This is a volume
that is mounted into all pods that run a VM. The volume is intended
to be used as a common repository for CDROM images. The PVC must exist
and it must be bound before any pods can run.
The second patch affects the small volume that is created for each
runner and contains the VM's configuration data such as the EFI vars.
The manager's default configuration causes the PVC for this volume
to be created with no storage class (which causes the default storage
class to be used). The patch provides a new configuration file for
the manager that makes the reconciler use local-path as storage
class for this PVC. Details about the manager configuration can be
found in the next section.
Note that you need none of the patches if you are fine with using your
cluster's default storage class and this class supports ReadOnlyMany as
access mode.
Check that the pod with the manager is running:
```sh
kubectl -n vmop-demo get pods -l app.kubernetes.io/name=vm-operator
```
Proceed to the description of [the controller](controller.html)
for creating your first VM.
## Configuration Details
The [config map](https://github.com/mnlipp/VM-Operator/blob/main/deploy/vmop-config-map.yaml)
for the manager may provide a configuration file (`config.yaml`) and
a file with logging properties (`logging.properties`). Both files are mounted
into the container that runs the manager and are evaluated by the manager
on startup. If no files are provided, the manager uses built-in defaults.
The configuration file for the Manager follows the conventions of
the [JGrapes](https://jgrapes.org/) component framework.
The keys that start with a slash select the component within the
application's component hierarchy. The mapping associated with the
selected component configures this component's properties.
The available configuration options for the components can be found
in their respective JavaDocs (e.g.
[here](latest-release/javadoc/org/jdrupes/vmoperator/manager/Reconciler.html)
for the Reconciler).
## Development Configuration
The [dev-example](https://github.com/mnlipp/VM-Operator/tree/main/dev-example)
directory contains a `kustomize.yaml` that uses the development namespace
`vmop-dev` and creates a deployment for the manager with 0 replicas.
This environment can be used for running the manager in the IDE. As the
namespace to manage cannot be detected from the environment, you must use
`-c ../dev-example/config.yaml` as argument when starting the manager. This
configures it to use the namespace `vmop-dev`.

View file

@ -0,0 +1,108 @@
---
title: VM-Operator Runner
layout: vm-operator
---
# The Runner
For most use cases, Qemu needs to be started and controlled by another
program that manages the Qemu process. This program is called the
runner in this context.
The most prominent reason for this second program is that it allows
a VM to be shutdown cleanly in response to a TERM signal. Qemu handles
the TERM signal by flushing all buffers and stopping, leaving the disks in
a [crash consistent state](https://gitlab.com/qemu-project/qemu/-/issues/148).
For a graceful shutdown, a parent process must handle the TERM signal, send
the `system_powerdown` command to the qemu process and wait for its completion.
Another reason for having the runner is that another process needs to be started
before qemu if the VM is supposed to include a TPM (software TPM).
Finally, we want some kind of higher level interface for applying runtime
changes to the VM such as changing the CD or configuring the number of
CPUs and the memory.
The runner takes care of all these issues. Although it is intended to
run in a container (which runs in a Kubernetes pod) it does not require
a container. You can start and use it as an ordinary program on any
system, provided that you have the required commands (qemu, swtpm)
installed.
## Stand-alone Configuration
Upon startup, the runner reads its main configuration file
which defaults to `/etc/opt/vmrunner/config.yaml` and may be changed
using the `-c` (or `--config`) command line option.
A sample configuration file with annotated options can be found
[here](https://github.com/mnlipp/VM-Operator/blob/main/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml).
As the runner implementation uses the
[JGrapes](https://jgrapes.org/) framework, the file
follows the framework's
[conventions](https://jgrapes.org/latest-release/javadoc/org/jgrapes/util/YamlConfigurationStore.html). The top level "`/Runner`" selects
the component to be configured. Nested within is the information
to be applied to the component.
The main entries in the configuration file are the "template" and
the "vm" information. The runner processes the
[freemarker template](https://freemarker.apache.org/), using the
"vm" information to derive the qemu command. The idea is that
the "vm" section provides high level information such as the boot
mode, the number of CPUs, the RAM size and the disks. The template
defines a particular VM type, i.e. it contains the "nasty details"
that do not need to be modified for some given set of VM instances.
The templates provided with the runner can be found
[here](https://github.com/mnlipp/VM-Operator/tree/main/org.jdrupes.vmoperator.runner.qemu/templates). When details
of the VM configuration need modification, a new VM type
(i.e. a new template) has to be defined. Authoring a new
template requires some knowledge about the
[qemu invocation](https://www.qemu.org/docs/master/system/invocation.html).
Despite many "warnings" that you find in the web, configuring the
invocation arguments of qemu is only a bit (but not much) more
challenging than editing libvirt's XML.
## Running in a Pod
The real purpose of the runner is to run a VM on Kubernetes in a pod.
When running in a Kubernetes pod, `/etc/opt/vmrunner/config.yaml` should be
provided by a
[ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/).
If additional templates are required, some ReadOnlyMany PV should
be mounted in `/opt/vmrunner/templates`. The PV should contain copies
of the standard templates as well as the additional templates. Of course,
a ConfigMap can be used for this purpose again.
Networking options are rather limited. The assumption is that in general
the VM wants full network connectivity. To achieve this, the pod must
run with host networking and the host's networking must provide a
bridge that the VM can attach to. The only currently supported
alternative is the less performant
"[user networking](https://wiki.qemu.org/Documentation/Networking#User_Networking_(SLIRP))",
which may be used in a stand-alone development configuration.
## Runtime changes
The runner supports adaption to changes of the RAM size (using the
balloon device) and to changes of the number of CPUs. Note that
in order to get new CPUs online on Linux guests, you need a
[udev rule](https://docs.kernel.org/core-api/cpu_hotplug.html#user-space-notification) which is not installed by default[^simplest].
The runner also changes the images loaded in CDROM drives. If the
drive is locked, i.e. if it doesn't respond to the "open tray" command
the change will be suspended until the VM opens the tray.
Finally, `powerdownTimeout` can be changed while the qemu process runs.
[^simplest]: The simplest form of the rule is probably:
```
ACTION=="add", SUBSYSTEM=="cpu", ATTR{online}="1"
```
## Testing with Helm
There is a
[Helm Chart](https://github.com/mnlipp/VM-Operator/tree/main/org.jdrupes.vmoperator.runner.qemu/helm-test)
for testing the runner.

View file

@ -0,0 +1,29 @@
---
title: Upgrading
layout: vm-operator
---
# Upgrading
## To version 3.0.0
All configuration files are backward compatible to version 2.3.0.
Note that in order to make use of the new viewer component,
[permissions](https://mnlipp.github.io/VM-Operator/user-gui.html#control-access-to-vms)
must be configured in the CR definition. Also note that
[display secrets](https://mnlipp.github.io/VM-Operator/user-gui.html#securing-access)
are automatically created unless explicitly disabled.
## To version 2.3.0
Starting with version 2.3.0, the web GUI uses a login conlet that
supports OIDC providers. This effects the configuration of the
web GUI components.
## To version 2.2.0
Version 2.2.0 sets the stateful set's `.spec.updateStrategy.type` to
"OnDelete". This fails for no apparent reason if a definition of
the stateful set with the default value "RollingUpdate" already exists.
In order to fix this, either the stateful set or the complete VM definition
must be deleted and the manager must be restarted.

View file

@ -0,0 +1,143 @@
---
title: VM-Operator Web-GUI for Users
layout: vm-operator
---
# User view
*Since 3.0.0*
The idea of the user view is to provide an intuitive widget that
allows the users to access their own VMs and to optionally start
and stop them.
![VM-Viewer](VmViewer-preview.png)
The configuration options resulting from this seemingly simple
requirement are unexpectedly complex.
## Control access to VMs
First of all, we have to define which VMs a user can access. This
is done using the optional property `spec.permissions` of the
VM definition (CRD).
```yaml
spec:
permissions:
- role: admin
may:
- "*"
- user: test
may:
- start
- stop
- accessConsole
```
Permissions can be granted to individual users or to roles. There
is a permission for each possible action. "*" grants them all.
## Simple usage vs. expert usage
Next, there are two ways to create the VM widgets (preview conlets
in the framework's terms). They can be created on demand or
automatically for each VM that a logged in user has permission to
access. The former is the preferred way for an administrator who
has access to all VMs and needs to open a particular VM's console
for trouble shooting only. The latter is the preferred way
for a regular user who has access to a limited number of VMs.
In this case, creating the widgets automatically has the additional
benefit that regular users don't need to know how to create and
configure the widgets using the menu and the properties dialog.
Automatic synchronization of widgets and accessible VMs is controlled
by the property `syncPreviewsFor` of the VM viewer. It's an array with
objects that either specify a role or a user.
```yaml
"/Manager":
# This configures the GUI
"/GuiHttpServer":
"/ConsoleWeblet":
"/WebConsole":
"/ComponentCollector":
"/VmViewer":
syncPreviewsFor:
- role: user
- user: test
displayResource:
preferredIpVersion: ipv4
```
## Console access
Access to the VM's console is implemented by generating a
[connection file](https://manpages.debian.org/testing/virt-viewer/remote-viewer.1.en.html#CONNECTION_FILE) for virt-viewer when the user clicks on
the console icon. If automatic open is enabled for this kind of
files in the browser, the console opens without further user action.
The file contains all required and optional information to start the
remote viewer.
* The "host" is by default the IP address of the node that the
VM's pod is running on (remember that the runner uses host
networking).
* The "port" is simply taken from the VM definition.
In more complex scenarios, an administrator may have set up a load
balancer that hides the worker node's IP addresses or the worker
nodes use an internal network and can only be accessed through a
proxy. For both cases, the values to include in the connection file
can be specified as properties of `spec.vm.display.spice` in the
VM definition.
```yaml
spec:
vm:
display:
spice:
port: 5930
server: 192.168.19.32
proxyUrl: http://lgpe-spice.some.host:1234
generateSecret: true
```
The value of `server` is used as value for key "host" in the
connection file, thus overriding the default value. The
value of `proxyUrl` is used as value for key "proxy".
## Securing access
As described [previously](./controller.html#display-secretpassword),
access to a VM's display can be secured with a password. If a secret
with a password exists for a VM, the password is
included in the connection file.
While this approach is very convenient for the user, it is not
secure, because this leaves the password as plain text in a file on
the user's computer (the downloaded connection file). To work around
this, the display secret is updated with a random password with
limited validity, unless the display secret defines a `password-expiry`
in the future or with value "never" or doesn't define a
`password-expiry` at all.
The automatically generated password is the base64 encoded value
of 16 (strong) random bytes (128 random bits). It is valid for
10 seconds only. This may be challenging on a slower computer
or if users may not enable automatic open for connection files
in the browser. The validity can therefore be adjusted in the
configuration.
```yaml
"/Manager":
"/Controller":
"/DisplaySecretMonitor":
# Validity of generated password in seconds
passwordValidity: 10
```
Taking into account that the controller generates a display
secret automatically by default, this approach to securing
console access should be sufficient in all cases. (Any feedback
if something has been missed is appreciated.)

View file

@ -0,0 +1,117 @@
---
title: VM-Operator Web-GUI
layout: vm-operator
---
# The Web-GUI
The manager component provides a GUI via a web server. The web GUI is
implemented using components from the
[JGrapes WebConsole](https://jgrapes.org/WebConsole.html)
project. Configuration of the GUI therefore follows the conventions
of that framework.
The structure of the configuration information should be easy to
understand from the examples provided. In general, configuration values
are applied to the individual components that make up an application.
The hierarchy of the components is reflected in the configuration
information because components are "addressed" by their position in
that hierarchy. (See
[the package description](latest-release/javadoc/org/jdrupes/vmoperator/manager/package-summary.html)
for information about the complete component structure.)
## Network access
By default, the service is made available at port 8080 of the manager
pod. Of course, a kubernetes service and an ingress configuration must
be added as required by the environment. (See the
[definition](https://github.com/mnlipp/VM-Operator/blob/main/deploy/vmop-service.yaml)
from the
[sample deployment](https://github.com/mnlipp/VM-Operator/tree/main/deploy)).
## User Access
Access to the web GUI is controlled by the login conlet. The framework
does not include sophisticated components for user management. Rather,
it assumes that an OIDC provider is responsible for user authentication
and role management.
```yaml
"/Manager":
# "/GuiSocketServer":
# port: 8080
"/GuiHttpServer":
# This configures the GUI
"/ConsoleWeblet":
"/WebConsole":
"/LoginConlet":
# Starting with version 2.3.0 the preferred approach is to
# configure an OIDC provider for user management and
# authorization. See the text for details.
oidcProviders: {}
# Support for "local" users is provided as a fallback mechanism.
# Note that up to Version 2.2.x "users" was an object with user names
# as its properties. Starting with 2.3.0 it is a list as shown.
users:
- name: admin
fullName: Administrator
password: "Generate hash with bcrypt"
- name: test
fullName: Test Account
password: "Generate hash with bcrypt"
# Required for using OIDC, see the text for details.
"/OidcClient":
redirectUri: https://my.server.here/oauth/callback"
# May be used for assigning roles to both local users and users from
# the OIDC provider. Not needed if roles are managed by the OIDC provider.
"/RoleConfigurator":
rolesByUser:
# User admin has role admin
admin:
- admin
# Non-privileged users are users
test:
- user
# All users have role other
"*":
- other
replace: false
# Manages the permissions for the roles.
"/RoleConletFilter":
conletTypesByRole:
# Admins can use all conlets
admin:
- "*"
# Users can use the viewer conlet
user:
- org.jdrupes.vmoperator.vmviewer.VmViewer
# Others cannot use any conlet (except login conlet to log out)
other:
# Up to version 2.2.x
# - org.jgrapes.webconlet.locallogin.LoginConlet
# Starting with version 2.3.0
- org.jgrapes.webconlet.oidclogin.LoginConlet
```
How local users can be configured should be obvious from the example.
The configuration of OIDC providers for user authentication (and
optionally for role assignment) is explained in the documentation of the
[login conlet](https://jgrapes.org/javadoc-webconsole/org/jgrapes/webconlet/oidclogin/LoginConlet.html).
Details about the `RoleConfigurator` and `RoleConletFilter` can also be found
in the documentation of the
[JGrapes WebConsole](https://jgrapes.org/WebConsole.html)
project.
The configuration above allows all users with role "admin" to use all
GUI components and users with role "user" to only use the viewer conlet,
i.e. the [User view](user-gui.html). The fallback role "other" allows
all users to use the login conlet to log out.
## Views
The configuration of the components that provide the manager and
users views is explained in the respective sections.