diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index f47366a..547c1a4 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -22,10 +22,10 @@ jobs: fetch-depth: 0 - name: Install graphviz run: sudo apt-get install graphviz - - name: Set up JDK 21 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '21' + java-version: '17' distribution: 'temurin' - name: Build with Gradle - run: ./gradlew -Pwebsite.push.token=${{ secrets.WEBSITE_PUSH_TOKEN }} stage + run: ./gradlew -Prepo.access.token=${{ secrets.REPO_ACCESS_TOKEN }} stage diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml deleted file mode 100644 index d0e4ec9..0000000 --- a/.github/workflows/jekyll.yml +++ /dev/null @@ -1,89 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# Sample workflow for building and deploying a Jekyll site to GitHub Pages -name: Deploy Jekyll site to Pages - -on: - # Runs on pushes targeting the default branch - push: - branches: ["main"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between -# the run in-progress and latest queued. However, do NOT cancel -# in-progress runs as we want to allow these production deployments -# to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Build job - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' # Not needed with a .ruby-version file - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - cache-version: 0 # Increment this number if you need to re-download cached gems - working-directory: webpages - - name: Setup Pages - id: pages - uses: actions/configure-pages@v5 - - name: Build with Jekyll - # Outputs to the './_site' directory by default - run: cd webpages && bundle exec jekyll build - env: - JEKYLL_ENV: production - - name: Install graphviz - run: sudo apt-get install graphviz - - name: Set up JDK 21 - uses: actions/setup-java@v3 - with: - java-version: '21' - distribution: 'temurin' - - name: Build apidocs - run: ./gradlew apidocs - - name: Copy javadoc - run: cp -a build/javadoc webpages/_site/ - - name: Generate the sitemap - uses: cicirello/generate-sitemap@v1 - with: - path-to-root: webpages/_site - base-url-path: https://vm-operator.jdrupes.org - - name: Index pagefind - run: cd webpages && npx pagefind --source "_site" - - name: Upload artifact - # Automatically uploads an artifact from the './_site' directory by default - uses: actions/upload-pages-artifact@v3 - with: - path: './webpages/_site' - - # Deployment job - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index beab0c4..3e6b3c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,9 +18,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 with: fetch-depth: 0 + ref: main - name: Install graphviz run: sudo apt-get install graphviz - name: Install podman @@ -31,10 +32,10 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Set up JDK 21 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '21' + java-version: '17' distribution: 'temurin' - name: Push with Gradle - run: ./gradlew -Pwebsite.push.token=${{ secrets.WEBSITE_PUSH_TOKEN }} -Pdocker.registry=ghcr.io/${{ github.actor }} stage publishImage + run: ./gradlew -Prepo.access.token=${{ secrets.REPO_ACCESS_TOKEN }} -Pdocker.registry=ghcr.io/${{ github.actor }} stage pushImages diff --git a/.markdownlint.yaml b/.markdownlint.yaml deleted file mode 100644 index 6ed5002..0000000 --- a/.markdownlint.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# See [rules](https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml) - -# Default state for all rules -default: true - -# MD007/ul-indent : Unordered list indentation : -# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md007.md -MD007: - # Spaces for indent - indent: 2 - # Whether to indent the first level of the list - start_indented: true - # Spaces for first level indent (when start_indented is set) - start_indent: 2 - -# MD025/single-title/single-h1 : Multiple top-level headings in the same document : -# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md025.md -MD025: - # Heading level - level: 1 - # RegExp for matching title in front matter (disable) - front_matter_title: "" - -# MD036/no-emphasis-as-heading : Emphasis used instead of a heading : -# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md036.md -MD036: false - -# MD043/required-headings : Required heading structure : -# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md043.md -MD043: false diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs index 72733d9..44c5061 100644 --- a/.settings/org.eclipse.buildship.core.prefs +++ b/.settings/org.eclipse.buildship.core.prefs @@ -5,7 +5,7 @@ connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) connection.project.dir= eclipse.preferences.version=1 gradle.user.home= -java.home= +java.home=/usr/lib/jvm/java-17-openjdk-17.0.8.0.7-1.fc37.x86_64 jvm.arguments= offline.mode=false override.workspace.settings=true diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml deleted file mode 100644 index 56a575c..0000000 --- a/.woodpecker/build.yaml +++ /dev/null @@ -1,38 +0,0 @@ -when: -- event: push - evaluate: 'CI_SYSTEM_HOST == "woodpecker.mnl.de"' - -clone: -- name: git - image: woodpeckerci/plugin-git - settings: - partial: false - tags: true - depth: 0 - -steps: -- name: prepare - image: alpine - commands: - # Because we run the next step as user 1000 to make podman work: - - mkdir /woodpecker/workflow - - chown 1000:1000 /woodpecker/workflow - - chown -R 1000:1000 $CI_WORKSPACE - -- name: build-jars - image: registry.mnl.de/mnl/jdk21-builder:v4 - environment: - HOME: /woodpecker/workflow - REGISTRY: registry.mnl.de - REGISTRY_USER: mnl - REGISTRY_TOKEN: - from_secret: REGISTRY_TOKEN - commands: - - echo $REGISTRY_TOKEN | podman login -u $REGISTRY_USER --password-stdin $REGISTRY - - ./gradlew -Pdocker.registry=$REGISTRY/$REGISTRY_USER build apidocs publishImage - backend_options: - kubernetes: - securityContext: - privileged: true - runAsUser: 1000 - runAsGroup: 1000 diff --git a/README.md b/README.md index 09fcd25..1895bbb 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,13 @@ -[![Java CI with Gradle](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml/badge.svg)](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml) +[![Java CI with Gradle](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml/badge.svg)](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/2277842dac894de4b663c6aa2779077e)](https://app.codacy.com/gh/mnlipp/VM-Operator/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) ![Latest Manager](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=manager*&label=latest) ![Latest Runner](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=runner-qemu*&label=latest) -# Run QEMU/KVM in Kubernetes Pods +# Run Qemu in Kubernetes Pods -![Overview picture](webpages/index-pic.svg) +The goal of this project is to provide the means for running Qemu +based VMs in Kubernetes pods. -This project provides an easy to use and flexible solution for running -QEMU/KVM based VMs in Kubernetes pods. - -The central component of this solution is the kubernetes operator that -manages "runners". These run in pods and are used to start and manage -the QEMU/KVM process for the VMs (optionally together with a SW-TPM). - -A web GUI for administrators provides an overview of the VMs together -with some basic control over the VMs. A web GUI for users provides an -interface to access and optionally start, stop and reset the VMs. - -Advanced features of the operator include pooling of VMs and automatic -login. - -See the [project's home page](https://vm-operator.jdrupes.org/) +See the [project's home page](https://mnlipp.github.io/VM-Operator/) for details. + diff --git a/build.gradle b/build.gradle index eb8e59a..1a11881 100644 --- a/build.gradle +++ b/build.gradle @@ -5,10 +5,9 @@ buildscript { } plugins { - id 'org.ajoberstar.grgit' version '5.2.0' + id 'org.ajoberstar.grgit' version '5.2.0' apply false id 'org.ajoberstar.git-publish' version '4.2.0' apply false - id 'pl.allegro.tech.build.axion-release' version '1.17.2' apply false - id 'org.jdrupes.vmoperator.versioning-conventions' + id 'pl.allegro.tech.build.axion-release' version '1.15.0' apply false id 'org.jdrupes.vmoperator.java-doc-conventions' id 'eclipse' id "com.github.node-gradle.node" version "7.0.1" @@ -19,7 +18,7 @@ allprojects { } task stage { - description = 'To be executed by CI.' + description = 'To be executed by CI, build and update JavaDoc.' group = 'build' // Build everything first @@ -27,6 +26,11 @@ task stage { dependsOn subprojects.tasks.collect { tc -> tc.findByName("build") }.flatten() } + + if (JavaVersion.current() == JavaVersion.VERSION_17) { + // Publish JavaDoc + dependsOn gitPublishPush + } } eclipse { diff --git a/buildSrc/.settings/org.eclipse.jdt.core.prefs b/buildSrc/.settings/org.eclipse.jdt.core.prefs index b25073a..68fda12 100644 --- a/buildSrc/.settings/org.eclipse.jdt.core.prefs +++ b/buildSrc/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,9 @@ -# -#Wed Oct 02 14:48:43 CEST 2024 eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=21 @@ -9,5 +11,12 @@ org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=21 diff --git a/buildSrc/.settings/org.eclipse.jdt.groovy.core.prefs b/buildSrc/.settings/org.eclipse.jdt.groovy.core.prefs index 71b5e37..bf0ca13 100644 --- a/buildSrc/.settings/org.eclipse.jdt.groovy.core.prefs +++ b/buildSrc/.settings/org.eclipse.jdt.groovy.core.prefs @@ -1,3 +1,3 @@ eclipse.preferences.version=1 -groovy.compiler.level=-1 +groovy.compiler.level=40 groovy.script.filters=**/*.dsld,y,**/*.gradle,n diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 4a5db6d..a9fb634 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,3 +1,9 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This project uses @Incubating APIs which are subject to change. + */ + plugins { // Support convention plugins written in Groovy. Convention plugins // are build scripts in 'src/main' that automatically become available @@ -8,24 +14,52 @@ plugins { id 'eclipse' } +repositories { + // Use the plugin portal to apply community plugins in convention plugins. + gradlePluginPortal() +} + sourceSets { - main { - groovy { - srcDirs = ['src'] - } - resources { - srcDirs = ['resources'] - } - } + main { + groovy { + srcDirs = ['src'] + } + } + + test { + groovy { + srcDirs = ['test'] + } + } } eclipse { + project { + file { + // closure executed after .project content is loaded from existing file + // and before gradle build information is merged + beforeMerged { project -> + project.natures.clear() + project.buildCommands.clear() + } + + project.natures += 'org.eclipse.buildship.core.gradleprojectnature' + // Don't build, result not used by Eclipse anyway + // project.buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder' + } + } + + classpath { + downloadJavadoc = true + downloadSources = true + } + jdt { file { withProperties { properties -> def formatterPrefs = new Properties() - rootProject.file("../gradle/org.eclipse.jdt.core.formatter.prefs") + rootProject.file("gradle/org.eclipse.jdt.core.formatter.prefs") .withInputStream { formatterPrefs.load(it) } properties.putAll(formatterPrefs) } diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 0000000..3f67e42 --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1,7 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This settings file is used to specify which projects to include in your build-logic build. + */ + +rootProject.name = 'buildSrc' diff --git a/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle b/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle index 605dc09..e09814c 100644 --- a/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle +++ b/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle @@ -5,11 +5,6 @@ */ plugins { - // Apply the common versioning conventions. - // Put this at the start, because accessing project.version before - // this is applied makes things fail. - id 'org.jdrupes.vmoperator.versioning-conventions' - // Apply the java Plugin to add support for Java. id 'java' @@ -18,6 +13,9 @@ plugins { // Access to git information id 'org.ajoberstar.grgit' + + // Apply the common versioning conventions. + id 'org.jdrupes.vmoperator.versioning-conventions' } repositories { @@ -55,25 +53,21 @@ sourceSets { java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(17) } } jar { manifest { - def matchExpr = [ project.tagName + "*" ] - - inputs.property("gitDescriptor", - { grgit.describe(always: true, match: matchExpr) }) + inputs.property("gitDescriptor", { grgit.describe(always: true) }) // Set Git revision information in the manifests of built bundles - def gitDesc = grgit.describe(always: true, match: matchExpr) attributes([ "Implementation-Title": project.name, - "Implementation-Version": "$project.version (built from ${gitDesc})", + "Implementation-Version": "$project.version (built from ${grgit.describe(always: true)})", "Implementation-Vendor": grgit.repository.jgit.repository.config.getString("user", null, "name") + " (" + grgit.repository.jgit.repository.config.getString("user", null, "email") + ")", - "Git-Descriptor": gitDesc, + "Git-Descriptor": grgit.describe(always: true), "Git-SHA": grgit.head().id, ]) } diff --git a/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle b/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle index 6af8fa7..95d7eff 100644 --- a/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle +++ b/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle @@ -22,28 +22,31 @@ configurations { } dependencies { - markdownDoclet "org.jdrupes.mdoclet:doclet:4.0.0" - javadocTaglets "org.jdrupes.taglets:plantuml-taglet:3.0.0" + 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' + } } task apidocs (type: JavaExec) { // Does not work on JitPack, no /usr/bin/dot - enabled = JavaVersion.current() == JavaVersion.VERSION_21 + enabled = JavaVersion.current() == JavaVersion.VERSION_17 + + dependsOn javadocResources outputs.dir(docDestinationDir) inputs.file rootProject.file('overview.md') - inputs.file "${rootProject.rootDir}/misc/javadoc-overwrites.css" + inputs.file "${rootProject.rootDir}/misc/stylesheet.css" - 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' + 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' gradle.projectsEvaluated { // Make sure that other projects' compileClasspaths are resolved @@ -66,8 +69,8 @@ task apidocs (type: JavaExec) { '-package', '-use', '-linksource', - '-link', 'https://docs.oracle.com/en/java/javase/21/docs/api/', - '-link', 'https://jgrapes.org/latest-release/javadoc/', + '-link', 'https://docs.oracle.com/en/java/javase/17/docs/api/', + '-link', 'https://mnlipp.github.io/jgrapes/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', @@ -85,7 +88,7 @@ task apidocs (type: JavaExec) { '-bottom', rootProject.file("misc/javadoc.bottom.txt").text, '--allow-script-in-comments', '-Xdoclint:-html', - '--add-stylesheet', "${rootProject.rootDir}/misc/javadoc-overwrites.css", + '--main-stylesheet', "${rootProject.rootDir}/misc/stylesheet.css", '--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.formats.html=ALL-UNNAMED', '-quiet' ] @@ -94,27 +97,34 @@ 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['website.push.token'] ?: "nouser") + project.rootProject.properties['repo.access.token'] ?: "nouser") +} + +gitPublish { + repoUri = 'https://github.com/mnlipp/VM-Operator.git' + branch = 'gh-pages' + contents { + from("${rootProject.buildDir}/javadoc") { + into 'javadoc' + } + if (!findProject(':org.jdrupes.vmoperator.runner.qemu').isSnapshot + && !findProject(':org.jdrupes.vmoperator.manager').isSnapshot) { + from("${rootProject.buildDir}/javadoc") { + into 'latest-release/javadoc' + } + } + } + preserve { include '**/*' } + commitMessage = "Updated." +} + +gradle.projectsEvaluated { + tasks.gitPublishReset.mustRunAfter subprojects.tasks + .collect { tc -> tc.findByName("build") }.flatten() + tasks.gitPublishReset.mustRunAfter subprojects.tasks + .collect { tc -> tc.findByName("test") }.flatten() + tasks.gitPublishCopy.dependsOn apidocs } diff --git a/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle b/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle index 49b6f74..114db51 100644 --- a/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle +++ b/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle @@ -11,26 +11,21 @@ plugins { id 'pl.allegro.tech.build.axion-release' } -def shortened = project.name.startsWith(project.group + ".") ? - project.name.substring(project.group.length() + 1) : project.name -if (shortened == "manager") { - shortened = "manager-app"; -} -var tagName = shortened.replace('.', '-') + "-" -if (grgit.branch.current.name != "main" - && grgit.branch.current.name != "HEAD" - && !grgit.branch.current.name.startsWith("testing") - && !grgit.branch.current.name.startsWith("release") - && !grgit.branch.current.name.startsWith("develop")) { - tagName = tagName + grgit.branch.current.name.replace('/', '-') + "-" -} -project.ext.tagName = tagName - scmVersion { versionIncrementer 'incrementMinor' tag { - prefix = project.tagName + def shortened = project.name.startsWith(project.group + ".") ? + project.name.substring(project.group.length() + 1) : project.name + if (shortened == "manager") { + shortened = "manager-app"; + } + var p = shortened.replace('.', '-') + "-" + if (grgit.branch.current.name != "main" + && !grgit.branch.current.name.startsWith("release")) { + p = p + grgit.branch.current.name.replace('/', '-') + "-" + } + prefix = p } } -project.version = scmVersion.version +version = scmVersion.version ext.isSnapshot = version.endsWith('-SNAPSHOT') diff --git a/checkstyle.xml b/checkstyle.xml index 088e543..015ef09 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -30,11 +30,8 @@ - - - diff --git a/deploy/crds/vmpools-crd.yaml b/deploy/crds/vmpools-crd.yaml deleted file mode 100644 index 2144940..0000000 --- a/deploy/crds/vmpools-crd.yaml +++ /dev/null @@ -1,74 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: vmpools.vmoperator.jdrupes.org -spec: - group: vmoperator.jdrupes.org - # list of versions supported by this CustomResourceDefinition - versions: - - name: v1 - served: true - storage: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - retention: - description: >- - Defines the timeout for assignments. The time may be - specified as ISO 8601 time or duration. When specifying - a duration, it will be added to the last time the VM's - console was used to obtain the timeout. - type: string - pattern: '^(?:\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3])(?:|:?[0-5]\d))|P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+[Hh])?(?:\d+[Mm])?(?:\d+(?:\.\d{1,9})?[Ss])?)?)$' - default: "PT1h" - loginOnAssignment: - description: >- - If set to true, the user will be automatically logged in - to the VM's console when the VM is assigned to him. - type: boolean - default: false - permissions: - type: array - description: >- - Defines permissions for accessing and manipulating the Pool. - items: - type: object - description: >- - Permissions can be granted to a user or to a role. - oneOf: - - required: - - user - - required: - - role - properties: - user: - type: string - role: - type: string - may: - type: array - items: - type: string - enum: - - start - - stop - - reset - - accessConsole - - "*" - default: ["accessConsole"] - required: - - permissions - # either Namespaced or Cluster - scope: Namespaced - names: - # plural name to be used in the URL: /apis/// - plural: vmpools - # singular name to be used as an alias on the CLI and for display - singular: vmpool - # kind is normally the CamelCased singular type. Your resource manifests use this. - kind: VmPool - listKind: VmPoolList diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml index c2a7a66..1863afe 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -990,52 +990,6 @@ spec: description: Copied to cloud-init's network-config file. type: object x-kubernetes-preserve-unknown-fields: true - permissions: - type: array - description: >- - Defines permissions for accessing and manipulating the VM. - The meaning of most permissions should be obvious. The - difference between "accessConsole" and "takeConsole" is - that "takeConsole" allows the user to take control of - the console even if it is already in use by another user. - items: - type: object - description: >- - Permissions can be granted to a user or to a role. - oneOf: - - required: - - user - - required: - - role - properties: - user: - type: string - role: - type: string - may: - type: array - items: - type: string - enum: - - start - - stop - - reset - - accessConsole - - takeConsole - - "*" - default: [] - pools: - type: array - description: >- - List of pools this VM belongs to. - items: - type: string - default: [] - loggingProperties: - type: string - description: >- - Override the default logging properties for - the runner for this VM. vm: type: object description: Defines the VM. @@ -1427,36 +1381,13 @@ spec: display: type: object properties: - outputs: - type: integer - default: 1 - loggedInUser: - description: >- - The name of a user that should be automatically - logged in on the display. Note that this requires - support from an agent in the guest OS. - type: string spice: type: object properties: port: - description: >- - Port number used for the Spice server. type: integer default: 5900 - server: - description: >- - Server (address) to use for connecting - to the spice server. Defaults to the address - of the node that the VM is running on. - type: string - generateSecret: - type: boolean - default: true - proxyUrl: - description: >- - If specified, is copied to the generated - viewer configuration files. + ticket: type: string streamingVideo: type: string @@ -1470,10 +1401,6 @@ spec: type: object default: {} properties: - runnerVersion: - description: >- - The version string of the runner. - type: string cpus: description: >- Number of CPUs currently in use. @@ -1484,50 +1411,6 @@ spec: Amount of memory in use. type: string default: "0" - consoleClient: - description: >- - The hostname of the currently connected client. - type: string - default: "" - consoleUser: - description: >- - The id of the user who has last requested a console - connection. - type: string - default: "" - loggedInUser: - description: >- - The name of a user that is currently logged in by the - VM operator agent. - type: string - displayPasswordSerial: - description: >- - Counts changes of the display password. Set to -1 - by the runner if password protection is not enabled. - type: integer - default: 0 - osinfo: - description: Copy of the OS info provided by the guest agent. - type: object - x-kubernetes-preserve-unknown-fields: true - assignment: - description: >- - The assignment of this VM to a a particular user. - type: object - properties: - pool: - description: >- - The pool this VM is taken from. - type: string - user: - description: >- - The user this VM is assigned to. - type: string - lastUsed: - description: >- - The last time this VM was used by the user. - type: string - default: {} conditions: description: >- List of component conditions observed @@ -1538,30 +1421,6 @@ spec: lastTransitionTime: "1970-01-01T00:00:00Z" reason: Creation message: "Creation of CR" - - type: Booted - status: "False" - observedGeneration: 1 - lastTransitionTime: "1970-01-01T00:00:00Z" - reason: Creation - message: "Creation of CR" - - type: VmopAgentConnected - status: "False" - observedGeneration: 1 - lastTransitionTime: "1970-01-01T00:00:00Z" - reason: Creation - message: "Creation of CR" - - type: UserLoggedIn - status: "False" - observedGeneration: 1 - lastTransitionTime: "1970-01-01T00:00:00Z" - reason: Creation - message: "Creation of CR" - - type: ConsoleConnected - status: "False" - observedGeneration: 1 - lastTransitionTime: "1970-01-01T00:00:00Z" - reason: Creation - message: "Creation of CR" type: array items: type: object diff --git a/deploy/vmop-deployment.yaml b/deploy/vmop-deployment.yaml index 08316f6..648cc39 100644 --- a/deploy/vmop-deployment.yaml +++ b/deploy/vmop-deployment.yaml @@ -21,31 +21,22 @@ spec: - name: vm-operator image: >- ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:latest - imagePullPolicy: Always - env: - - name: JAVA_OPTS - # The VM operator needs about 25 MB of memory, plus 1 MB for - # each VM. The reason is that for the sake of effeciency, we - # have to keep a parsed representation of the CRD in memory, - # which requires about 512 KB per VM. While handling updates, - # we temporarily have the old and the new version of the CRD - # in memory, so we need another 512 KB per VM. - value: "-Xmx128m" - resources: - requests: - cpu: 100m - memory: 128Mi volumeMounts: - name: config mountPath: /etc/opt/vmoperator - name: vmop-image-repository mountPath: /var/local/vmop-image-repository + imagePullPolicy: Always securityContext: capabilities: drop: - ALL readOnlyRootFilesystem: true allowPrivilegeEscalation: false + resources: + requests: + cpu: 100m + memory: 128Mi volumes: - name: config configMap: diff --git a/deploy/vmop-role.yaml b/deploy/vmop-role.yaml index e1ae7bc..e1ea85b 100644 --- a/deploy/vmop-role.yaml +++ b/deploy/vmop-role.yaml @@ -9,15 +9,8 @@ rules: - vmoperator.jdrupes.org resources: - vms - - vmpools verbs: - '*' -- apiGroups: - - vmoperator.jdrupes.org - resources: - - vms/status - verbs: - - patch - apiGroups: - apps resources: @@ -35,12 +28,8 @@ rules: - apiGroups: - "" resources: - - persistentvolumeclaims - pods verbs: - - watch - list - - get - - create - delete - patch diff --git a/dev-example/.gitignore b/dev-example/.gitignore index 1e31cc5..925478d 100644 --- a/dev-example/.gitignore +++ b/dev-example/.gitignore @@ -1,4 +1 @@ /test-vm-ci.yaml -/kubeconfig.yaml -/crds/ -/.vm-operator-cmd.rc diff --git a/dev-example/Readme.md b/dev-example/Readme.md index d794b24..dfcd3e8 100644 --- a/dev-example/Readme.md +++ b/dev-example/Readme.md @@ -1,16 +1,16 @@ # Example setup for development -The CRD must be deployed independently. Apart from that, the -`kustomize.yaml` - - * creates a small cdrom image repository and - - * deploys the operator in namespace `vmop-dev` with a replica of 0. +The CRD must be deployed independently. Apart from that, the +`kustomize.yaml` +* creates a small cdrom image repository and + +* deploys the operator in namespace `vmop-dev` with a replica of 0. + This allows you to run the manager in your IDE. The `kustomize.yaml` also changes the container image repository for -the operator to a private repository for development. You have to +the operator to a private repository for development. You have to adapt this to your own repository if you also want to test your development version in a container. diff --git a/dev-example/config.yaml b/dev-example/config.yaml index 2a72bc8..cf43692 100644 --- a/dev-example/config.yaml +++ b/dev-example/config.yaml @@ -7,28 +7,8 @@ "/Controller": namespace: vmop-dev "/Reconciler": - runnerDataPvc: - storageClassName: rook-cephfs - loadBalancerService: - labels: - label1: label1 - label2: toBeReplaced - annotations: - metallb.universe.tf/loadBalancerIPs: 192.168.168.1 - metallb.universe.tf/ip-allocated-from-pool: single-common - metallb.universe.tf/allow-shared-ip: single-common - loggingProperties: | - # Defaults for namespace (VM domain) - handlers=java.util.logging.ConsoleHandler - - #org.jgrapes.level=FINE - #org.jgrapes.core.handlerTracking.level=FINER - - org.jdrupes.vmoperator.runner.qemu.level=FINEST - - java.util.logging.ConsoleHandler.level=ALL - java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter - java.util.logging.SimpleFormatter.format=%1$tb %1$td %1$tT %4$s %5$s%6$s%n + runnerData: + storageClassName: null "/GuiSocketServer": port: 8888 "/GuiHttpServer": @@ -37,34 +17,17 @@ "/WebConsole": "/LoginConlet": users: - - name: admin - fullName: Administrator - password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." - - name: operator - fullName: Operator - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test1 - fullName: Test Account 1 - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test2 - fullName: Test Account 2 - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test3 - fullName: Test Account 3 - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" + admin: + fullName: Administrator + password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." + test: + fullName: Test Account + password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" "/RoleConfigurator": rolesByUser: # User admin has role admin admin: - admin - operator: - - operator - test1: - - user - test2: - - user - test3: - - user # All users have role other "*": - other @@ -74,17 +37,6 @@ # Admins can use all conlets admin: - "*" - operator: - - org.jdrupes.vmoperator.vmmgmt.VmMgmt - - org.jdrupes.vmoperator.vmaccess.VmAccess - user: - - org.jdrupes.vmoperator.vmaccess.VmAccess # Others cannot use any conlet (except login conlet to log out) other: - - org.jgrapes.webconlet.oidclogin.LoginConlet - "/ComponentCollector": - "/VmAccess": - displayResource: - preferredIpVersion: ipv4 - syncPreviewsFor: - - role: user + - org.jgrapes.webconlet.locallogin.LoginConlet diff --git a/dev-example/gen-pool-vm-crds b/dev-example/gen-pool-vm-crds deleted file mode 100755 index f9cf692..0000000 --- a/dev-example/gen-pool-vm-crds +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -function usage() { - cat >&2 <&2 "Unknown option: $1"; exit 1;; - *) template="$1";; - esac - shift -done - -if [ -z "$template" ]; then - usage -fi - -if [ "$count" = "0" ]; then - exit 0 -fi -for number in $(seq 1 $count); do - if [ -z "$prefix" ]; then - prefix=$(basename $template .tpl.yaml) - fi - name="$prefix$(printf %03d $number)" - index=$(($number - 1)) - esh -o $destination/$name.yaml $template number=$number index=$index -done diff --git a/dev-example/kustomization.yaml b/dev-example/kustomization.yaml index 975d95f..70c6ae6 100644 --- a/dev-example/kustomization.yaml +++ b/dev-example/kustomization.yaml @@ -35,14 +35,6 @@ patches: "/Reconciler": runnerData: storageClassName: null - loadBalancerService: - labels: - label1: label1 - label2: toBeReplaced - annotations: - metallb.universe.tf/loadBalancerIPs: 192.168.168.1 - metallb.universe.tf/ip-allocated-from-pool: single-common - metallb.universe.tf/allow-shared-ip: single-common "/GuiSocketServer": port: 8888 "/GuiHttpServer": @@ -51,29 +43,17 @@ patches: "/WebConsole": "/LoginConlet": users: - - name: admin - fullName: Administrator - password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." - - name: test1 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test2 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test3 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" + admin: + fullName: Administrator + password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." + test: + fullName: Test Account + password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" "/RoleConfigurator": rolesByUser: # User admin has role admin admin: - admin - test1: - - user - test2: - - user - test3: - - user # All users have role other "*": - other @@ -83,17 +63,10 @@ patches: # Admins can use all conlets admin: - "*" - user: - - org.jdrupes.vmoperator.vmviewer.VmViewer # Others cannot use any conlet (except login conlet to log out) other: - org.jgrapes.webconlet.locallogin.LoginConlet - "/ComponentCollector": - "/VmAccess": - displayResource: - preferredIpVersion: ipv4 - syncPreviewsFor: - - role: user + - target: group: apps version: v1 diff --git a/dev-example/pool-action b/dev-example/pool-action deleted file mode 100755 index bc8fbce..0000000 --- a/dev-example/pool-action +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -function usage() { - cat >&2 <&2 "Unknown option: $1"; exit 1;; - *) if [ ! -v pool ]; then - pool="$1" - elif [ ! -v action ]; then - action="$1" - else - usage - fi;; - esac - shift -done - -if [ ! -v pool -o ! -v "action" -o ! -v context ]; then - echo >&2 "Missing arguments or context not set." - echo >&2 - usage -fi -case "$action" in - "start"|"stop"|"delete"|"delete-disks") ;; - *) usage;; -esac - -kubectl --context="$context" -n "$namespace" get vms -o json \ - | jq -r '.items[] | select(.spec.pools | contains(["'${pool}'"])) | .metadata.name' \ -| while read vmName; do - case "$action" in - start) kubectl --context="$context" -n "$namespace" patch vms "$vmName" \ - --type='merge' -p '{"spec":{"vm":{"state":"Running"}}}';; - stop) kubectl --context="$context" -n "$namespace" patch vms "$vmName" \ - --type='merge' -p '{"spec":{"vm":{"state":"Stopped"}}}';; - delete) kubectl --context="$context" -n "$namespace" delete vm/"$vmName";; - delete-disks) kubectl --context="$context" -n "$namespace" delete \ - pvc -l app.kubernetes.io/instance="$vmName" ;; - esac -done diff --git a/dev-example/test-pool.yaml b/dev-example/test-pool.yaml deleted file mode 100644 index 497aaf7..0000000 --- a/dev-example/test-pool.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: "vmoperator.jdrupes.org/v1" -kind: VmPool -metadata: - namespace: vmop-dev - name: test-vms -spec: - retention: "PT1m" - loginOnAssignment: true - permissions: - - user: admin - may: - - accessConsole - - start - - role: user - may: - - accessConsole - - start diff --git a/dev-example/test-vm-display-secret.yaml b/dev-example/test-vm-display-secret.yaml deleted file mode 100644 index a6f0fe6..0000000 --- a/dev-example/test-vm-display-secret.yaml +++ /dev/null @@ -1,13 +0,0 @@ -kind: Secret -apiVersion: v1 -metadata: - name: test-vm-display-secret - namespace: vmop-dev - 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== - password-expiry: KzMw diff --git a/dev-example/test-vm-snapshot.yaml b/dev-example/test-vm-snapshot.yaml deleted file mode 100644 index fd60a25..0000000 --- a/dev-example/test-vm-snapshot.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -apiVersion: snapshot.storage.k8s.io/v1 -kind: VolumeSnapshot -metadata: - namespace: vmop-dev - name: test-vm-system-disk-snapshot -spec: - volumeSnapshotClassName: csi-rbdplugin-snapclass - source: - persistentVolumeClaimName: test-vm-system-disk diff --git a/dev-example/test-vm.tpl.yaml b/dev-example/test-vm.tpl.yaml deleted file mode 100644 index 76adfba..0000000 --- a/dev-example/test-vm.tpl.yaml +++ /dev/null @@ -1,66 +0,0 @@ -apiVersion: "vmoperator.jdrupes.org/v1" -kind: VirtualMachine -metadata: - namespace: vmop-dev - name: test-vm<%= $(printf "%02d" ${number}) %> - annotations: - argocd.argoproj.io/sync-wave: "20" - -spec: - image: - source: ghcr.io/mnlipp/org.jdrupes.vmoperator.runner.qemu-arch:latest -# source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing -# source: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.runner.qemu-arch:latest - pullPolicy: Always - - runnerTemplate: - update: true - - permissions: - - role: admin - may: - - "*" - - guestShutdownStops: true - - cloudInit: - metaData: {} - - pools: - - test-vms - - vm: - # state: Running - bootMenu: true - maximumCpus: 4 - currentCpus: 2 - maximumRam: 6Gi - currentRam: 4Gi - - networks: - # No bridge on TC1 - # - tap: {} - - user: {} - - disks: - - volumeClaimTemplate: - metadata: - name: system - spec: - storageClassName: ceph-rbd3slow - dataSource: - name: test-vm-system-disk-snapshot - kind: VolumeSnapshot - apiGroup: snapshot.storage.k8s.io - accessModes: - - ReadWriteOnce - 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 - - display: - spice: - port: <%= $((5910 + number)) %> diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml index aa75bc3..0a8a098 100644 --- a/dev-example/test-vm.yaml +++ b/dev-example/test-vm.yaml @@ -5,23 +5,19 @@ metadata: name: test-vm spec: image: - source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing + repository: docker-registry.lan.mnl.de + path: vmoperator/org.jdrupes.vmoperator.runner.qemu-alpine pullPolicy: Always - permissions: - - user: admin - may: - - "*" - resources: requests: cpu: 1 memory: 2Gi - + guestShutdownStops: true cloudInit: {} - + vm: # state: Running bootMenu: yes @@ -32,9 +28,8 @@ spec: currentCpus: 4 networks: - # No bridge on test cluster - - user: {} - + - tap: + mac: "02:16:3e:33:58:10" disks: - volumeClaimTemplate: metadata: @@ -57,6 +52,3 @@ spec: display: spice: port: 5810 - generateSecret: true - - loadBalancerService: {} diff --git a/dev-example/vmop-agent/99-vmop-agent.rules b/dev-example/vmop-agent/99-vmop-agent.rules deleted file mode 100644 index 4a18472..0000000 --- a/dev-example/vmop-agent/99-vmop-agent.rules +++ /dev/null @@ -1,2 +0,0 @@ -SUBSYSTEM=="virtio-ports", ATTR{name}=="org.jdrupes.vmop_agent.0", \ - TAG+="systemd" ENV{SYSTEMD_WANTS}="vmop-agent.service" diff --git a/dev-example/vmop-agent/gdm/PostLogin/Default b/dev-example/vmop-agent/gdm/PostLogin/Default deleted file mode 100755 index 8a70890..0000000 --- a/dev-example/vmop-agent/gdm/PostLogin/Default +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf diff --git a/dev-example/vmop-agent/vmop-agent b/dev-example/vmop-agent/vmop-agent deleted file mode 100755 index 9f4d9e7..0000000 --- a/dev-example/vmop-agent/vmop-agent +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/bash - -# Note that this script requires "jq" to be installed and a version -# of loginctl that accepts the "-j" option. - -while [ "$#" -gt 0 ]; do - case "$1" in - --path) shift; ttyPath="$1";; - --path=*) IFS='=' read -r option value <<< "$1"; ttyPath="$value";; - esac - shift -done - -ttyPath="${ttyPath:-/dev/virtio-ports/org.jdrupes.vmop_agent.0}" - -if [ ! -w "$ttyPath" ]; then - echo >&2 "Device $ttyPath not writable" - exit 1 -fi - -# Create fd for the tty in variable con -if ! exec {con}<>"$ttyPath"; then - echo >&2 "Cannot open device $ttyPath" - exit 1 -fi - -# Temporary file for logging error messages, clear tty and signal ready -temperr=$(mktemp) -clear >/dev/tty1 -echo >&${con} "220 Hello" - -# This script uses the (shared) home directory as "dictonary" for -# synchronizing the username and the uid between hosts. -# -# Every user has a directory with his username. The directory is -# owned by root to prevent changes of access rights by the user. -# The uid and gid of the directory are equal. Thus the name of the -# directory and the id from the group ownership also provide the -# association between the username and the uid. - -# Add the user with name $1 to the host's "user database". This -# may not be invoked concurrently. -createUser() { - local missing=$1 - local uid - local userHome="/home/$missing" - local createOpts="" - - # Retrieve or create the uid for the username - if [ -d "$userHome" ]; then - # If a home directory exists, use the id from the group ownership as uid - uid=$(ls -ldn "$userHome" | head -n 1 | awk '{print $4}') - createOpts="--no-create-home" - else - # Else get the maximum of all ids from the group ownership +1 - uid=$(ls -ln "/home" | tail -n +2 | awk '{print $4}' | sort | tail -1) - uid=$(( $uid + 1 )) - if [ $uid -lt 1100 ]; then - uid=1100 - fi - createOpts="--create-home" - fi - groupadd -g $uid $missing - useradd $missing -u $uid -g $uid $createOpts -} - -# Login the user, i.e. create a desktopn for the user. -doLogin() { - user=$1 - if [ "$user" = "root" ]; then - echo >&${con} "504 Won't log in root" - return - fi - - # Check if this user is already logged in on tty2 - curUser=$(loginctl -j | jq -r '.[] | select(.tty=="tty2") | .user') - if [ "$curUser" = "$user" ]; then - echo >&${con} "201 User already logged in" - return - fi - - # Terminate a running desktop (fail safe) - attemptLogout - - # Check if username is known on this host. If not, create user - uid=$(id -u ${user} 2>/dev/null) - if [ $? != 0 ]; then - ( flock 200 - createUser ${user} - ) 200>/home/.gen-uid-lock - - # This should now work, else something went wrong - uid=$(id -u ${user} 2>/dev/null) - if [ $? != 0 ]; then - echo >&${con} "451 Cannot determine uid" - return - fi - fi - - # Configure user as auto login user - sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf - sed -i '/\[daemon\]/a AutomaticLoginEnable=true\nAutomaticLogin='$user \ - /etc/gdm/custom.conf - - # Activate user - systemctl restart gdm - if [ $? -eq 0 ]; then - echo >&${con} "201 User logged in successfully" - else - echo >&${con} "451 $(tr '\n' ' ' <${temperr})" - fi -} - -# Attempt to log out a user currently using tty1. This is an intermediate -# operation that can be invoked from other operations -attemptLogout() { - sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf - systemctl stop gdm - echo >&${con} "102 Desktop stopped" -} - -# Log out any user currently using tty1. This is invoked when executing -# the logout command and therefore sends back a 2xx return code. -# Also try to restart gdm, if it is not running. -doLogout() { - attemptLogout - systemctl restart gdm - echo >&${con} "202 User logged out" -} - -while read line <&${con}; do - case $line in - "login "*) IFS=' ' read -ra args <<< "$line"; doLogin ${args[1]};; - "logout") doLogout;; - esac -done - -onExit() { - doLogout - if [ -n "$temperr" ]; then - rm -f $temperr - fi - echo >&${con} "240 Quit" -} - -trap onExit EXIT diff --git a/dev-example/vmop-agent/vmop-agent.service b/dev-example/vmop-agent/vmop-agent.service deleted file mode 100644 index 11c64f2..0000000 --- a/dev-example/vmop-agent/vmop-agent.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=VM-Operator (Guest) Agent -BindsTo=dev-virtio\x2dports-org.jdrupes.vmop_agent.0.device -After=dev-virtio\x2dports-org.jdrupes.vmop_agent.0.device multi-user.target -IgnoreOnIsolate=True - -[Service] -UMask=0077 -#EnvironmentFile=/etc/sysconfig/vmop-agent -ExecStart=/usr/local/libexec/vmop-agent -Restart=always -RestartSec=0 - -[Install] -WantedBy=dev-virtio\x2dports-org.jdrupes.vmop_agent.0.device diff --git a/example/local-path/Readme.md b/example/local-path/Readme.md index bdba8cc..7afb948 100644 --- a/example/local-path/Readme.md +++ b/example/local-path/Readme.md @@ -1,17 +1,17 @@ # Example setup -The CRD must be deployed independently. +The CRD must be deployed independently. ```sh kubectl apply -f https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml ``` -Apart from that, the `kustomize.yaml` defines a namespace for the manager +Apart from that, the `kustomize.yaml` defines a namespace for the manager (and the VMs managed by it) and patches the repository PVC to create a small volume using local-path. -A second patch provides a new configuration file for the manager -that makes it use the local-path storage class when creating the +A second patch provides a new configuration file for the manager +that makes it use the local-path storage class when creating the small volume for a runner's data. The `kustomize.yaml` does not include the test VM. Before creating diff --git a/example/rook-ceph/Readme.md b/example/rook-ceph/Readme.md index 3756e93..1d2cfc6 100644 --- a/example/rook-ceph/Readme.md +++ b/example/rook-ceph/Readme.md @@ -1,12 +1,12 @@ # Example setup -The CRD must be deployed independently. +The CRD must be deployed independently. ```sh kubectl apply -f https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml ``` -Apart from that, the `kustomize.yaml` defines a namespace for the manager +Apart from that, the `kustomize.yaml` defines a namespace for the manager (and the VMs managed by it) and applies patches to use `rook-cephfs` as storage class (instead of the default storage class). diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index f97ebb7..0000000 --- a/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e644113..ccebba7 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a441313..8707e8b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip networkTimeout=10000 -validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index b740cf1..79a61d4 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/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,8 +83,10 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -# 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 +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"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -131,13 +133,10 @@ location of your Java installation." fi else JAVACMD=java - 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. + which java >/dev/null 2>&1 || 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. @@ -145,7 +144,7 @@ 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=SC2039,SC3045 + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +152,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=SC2039,SC3045 + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -198,15 +197,11 @@ if "$cygwin" || "$msys" ; then done fi - -# 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. +# 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. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 25da30d..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -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 +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. goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -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 +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. goto fail diff --git a/misc/DejaVuSans-Bold.woff2 b/misc/DejaVuSans-Bold.woff2 new file mode 100644 index 0000000..373095f Binary files /dev/null and b/misc/DejaVuSans-Bold.woff2 differ diff --git a/misc/DejaVuSans.woff2 b/misc/DejaVuSans.woff2 new file mode 100644 index 0000000..8437d4e Binary files /dev/null and b/misc/DejaVuSans.woff2 differ diff --git a/misc/DejaVuSansMono-Bold.woff2 b/misc/DejaVuSansMono-Bold.woff2 new file mode 100644 index 0000000..f2b469a Binary files /dev/null and b/misc/DejaVuSansMono-Bold.woff2 differ diff --git a/misc/DejaVuSansMono.woff2 b/misc/DejaVuSansMono.woff2 new file mode 100644 index 0000000..cf200e1 Binary files /dev/null and b/misc/DejaVuSansMono.woff2 differ diff --git a/misc/DejaVuSerif-Bold.woff2 b/misc/DejaVuSerif-Bold.woff2 new file mode 100644 index 0000000..655ac56 Binary files /dev/null and b/misc/DejaVuSerif-Bold.woff2 differ diff --git a/misc/DejaVuSerif.woff2 b/misc/DejaVuSerif.woff2 new file mode 100644 index 0000000..238566d Binary files /dev/null and b/misc/DejaVuSerif.woff2 differ diff --git a/misc/javadoc-overwrites.css b/misc/javadoc-overwrites.css deleted file mode 100644 index 7eed81f..0000000 --- a/misc/javadoc-overwrites.css +++ /dev/null @@ -1,2 +0,0 @@ -:root { --body-font-size: 16px;} -:root { --code-font-size: 16px;} diff --git a/misc/javadoc.bottom.txt b/misc/javadoc.bottom.txt index d5589ac..bf7dd56 100644 --- a/misc/javadoc.bottom.txt +++ b/misc/javadoc.bottom.txt @@ -4,33 +4,26 @@ TermsPrivacy

- - - - - \ No newline at end of file diff --git a/misc/stylesheet.css b/misc/stylesheet.css new file mode 100644 index 0000000..e21b9b2 --- /dev/null +++ b/misc/stylesheet.css @@ -0,0 +1,912 @@ +/* + * 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 Serif", 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-family: "DejaVu Sans", sans; + font-size:20px; +} +h2 { + font-family: "DejaVu Sans", sans; + font-size:18px; +} +h3 { + font-family: "DejaVu Sans", sans; + font-size:16px; +} +h4 { + font-family: "DejaVu Sans", sans; + font-size:15px; +} +h5 { + font-family: "DejaVu Sans", sans; + font-size:14px; +} +h6 { + font-family: "DejaVu Sans", sans; + 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; +} + +/* + * 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-family: "DejaVu Sans", sans; + font-size:80%; +} +.sub-nav { + background-color:#dee3e9; + float:left; + width:100%; + overflow:hidden; + font-family: "DejaVu Sans", sans; + 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", sans; + font-weight:bold; + margin:10px 0 0 0; + color:#4E4E4E; +} +dl.notes > dd { + margin:5px 10px 10px 0; +} +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; +} +.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; + font-family: 'DejaVu Sans'; +} +.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); +} +#method-summary-table .three-column-summary { + grid-template-columns: minmax(10%, 20%) 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-family: 'DejaVu Sans'; + 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-size:14px; + 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; + /* font-size:14px; */ + 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-family: "DejaVu Sans", sans; + font-weight:bold; +} +.sub-title, .inheritance, .all-packages-table-tab1.col-first, + .summary-table .col-first { + font-family: "DejaVu Sans", sans; +} +.deprecation-comment, .help-footnote, .preview-comment { + 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; +} +ul.ui-autocomplete li { + float:left; + clear:both; + width:100%; +} +.result-highlight { + font-weight:bold; +} +#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, \ + \ + \ + '); + 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, \ + \ + \ + '); +} + +/* + * 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; + } +} diff --git a/org.jdrupes.vmoperator.common/.eclipse-pmd b/org.jdrupes.vmoperator.common/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/org.jdrupes.vmoperator.common/.eclipse-pmd +++ b/org.jdrupes.vmoperator.common/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs b/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs deleted file mode 100644 index 8b8b906..0000000 --- a/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs +++ /dev/null @@ -1,7 +0,0 @@ -add_header=true -eclipse.preferences.version=1 -header_text=/*\n * VM-Operator\n * Copyright (C) 2024 Michael N. Lipp\n * \n * This program is free software\: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n */ -project_specific_settings=true -visibility_package=false -visibility_private=false -visibility_protected=false diff --git a/org.jdrupes.vmoperator.common/build.gradle b/org.jdrupes.vmoperator.common/build.gradle index e72cb14..ed082a1 100644 --- a/org.jdrupes.vmoperator.common/build.gradle +++ b/org.jdrupes.vmoperator.common/build.gradle @@ -10,8 +10,5 @@ plugins { dependencies { api project(':org.jdrupes.vmoperator.util') - api 'org.jgrapes:org.jgrapes.core:[1.22.1,2)' - api 'io.kubernetes:client-java:[19.0.0,20.0.0)' - api 'org.yaml:snakeyaml' - api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]' + api 'io.kubernetes:client-java:[18.0.0,19)' } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java index b9de69f..3ebe29d 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java @@ -21,7 +21,6 @@ package org.jdrupes.vmoperator.common; /** * Some constants. */ -@SuppressWarnings("PMD.DataClass") public class Constants { /** The Constant APP_NAME. */ @@ -30,98 +29,9 @@ public class Constants { /** The Constant VM_OP_NAME. */ public static final String VM_OP_NAME = "vm-operator"; - /** - * Constants related to the CRD. - */ - @SuppressWarnings("PMD.ShortClassName") - public static class Crd { - /** The Constant GROUP. */ - public static final String GROUP = "vmoperator.jdrupes.org"; + /** The Constant VM_OP_GROUP. */ + public static final String VM_OP_GROUP = "vmoperator.jdrupes.org"; - /** The Constant KIND_VM. */ - public static final String KIND_VM = "VirtualMachine"; - - /** The Constant KIND_VM_POOL. */ - public static final String KIND_VM_POOL = "VmPool"; - } - - /** - * Status related constants. - */ - public static class Status { - /** The Constant RUNNER_VERSION. */ - public static final String RUNNER_VERSION = "runnerVersion"; - - /** The Constant CPUS. */ - public static final String CPUS = "cpus"; - - /** The Constant RAM. */ - public static final String RAM = "ram"; - - /** The Constant OSINFO. */ - public static final String OSINFO = "osinfo"; - - /** The Constant DISPLAY_PASSWORD_SERIAL. */ - public static final String DISPLAY_PASSWORD_SERIAL - = "displayPasswordSerial"; - - /** The Constant LOGGED_IN_USER. */ - public static final String LOGGED_IN_USER = "loggedInUser"; - - /** The Constant CONSOLE_CLIENT. */ - public static final String CONSOLE_CLIENT = "consoleClient"; - - /** The Constant CONSOLE_USER. */ - public static final String CONSOLE_USER = "consoleUser"; - - /** The Constant ASSIGNMENT. */ - public static final String ASSIGNMENT = "assignment"; - - /** - * Conditions used in Status. - */ - public static class Condition { - /** The Constant COND_RUNNING. */ - public static final String RUNNING = "Running"; - - /** The Constant COND_BOOTED. */ - public static final String BOOTED = "Booted"; - - /** The Constant COND_VMOP_AGENT. */ - public static final String VMOP_AGENT = "VmopAgentConnected"; - - /** The Constant COND_USER_LOGGED_IN. */ - public static final String USER_LOGGED_IN = "UserLoggedIn"; - - /** The Constant COND_CONSOLE. */ - public static final String CONSOLE_CONNECTED = "ConsoleConnected"; - - /** - * Reasons used in conditions. - */ - public static class Reason { - /** The Constant NOT_REQUESTED. */ - public static final String NOT_REQUESTED = "NotRequested"; - - /** The Constant USER_LOGGED_IN. */ - public static final String LOGGED_IN = "LoggedIn"; - } - } - } - - /** - * DisplaySecret related constants. - */ - public static class DisplaySecret { - - /** The Constant NAME. */ - public static final String NAME = "display-secret"; - - /** The Constant PASSWORD. */ - public static final String PASSWORD = "display-password"; - - /** The Constant EXPIRY. */ - public static final String EXPIRY = "password-expiry"; - - } + /** The Constant VM_OP_KIND_VM. */ + public static final String VM_OP_KIND_VM = "VirtualMachine"; } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java index 68f52eb..47b7208 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java @@ -32,11 +32,13 @@ import java.util.regex.Pattern; public class Convertions { @SuppressWarnings({ "PMD.UseConcurrentHashMap", - "PMD.FieldNamingConventions" }) + "PMD.FieldNamingConventions", "PMD.VariableNamingConventions" }) private static final Map unitMap = new HashMap<>(); - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final List> unitMappings; - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final Pattern memorySize = Pattern.compile("^\\s*(\\d+(\\.\\d+)?)\\s*([A-Za-z]*)\\s*"); @@ -67,6 +69,7 @@ public class Convertions { * @param amount the amount * @return the big integer */ + @SuppressWarnings("PMD.DataflowAnomalyAnalysis") public static BigInteger parseMemory(Object amount) { if (amount == null) { return (BigInteger) amount; diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/DynamicTypeAdapterFactory.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/DynamicTypeAdapterFactory.java deleted file mode 100644 index d21eed4..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/DynamicTypeAdapterFactory.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import com.google.gson.Gson; -import com.google.gson.InstanceCreator; -import com.google.gson.JsonObject; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; -import io.kubernetes.client.openapi.ApiClient; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Type; - -/** - * A factory for creating objects. - * - * @param the generic type - * @param the generic type - */ -public class DynamicTypeAdapterFactory> implements TypeAdapterFactory { - - private final Class objectClass; - private final Class objectListClass; - - /** - * Make sure that this adapter is registered. - * - * @param client the client - */ - public void register(ApiClient client) { - if (!ModelCreator.class - .equals(client.getJSON().getGson().getAdapter(objectClass) - .getClass()) - || !ModelsCreator.class.equals(client.getJSON().getGson() - .getAdapter(objectListClass).getClass())) { - Gson gson = client.getJSON().getGson(); - client.getJSON().setGson(gson.newBuilder() - .registerTypeAdapterFactory(this).create()); - } - } - - /** - * Instantiates a new generic type adapter factory. - * - * @param objectClass the object class - * @param objectListClass the object list class - */ - public DynamicTypeAdapterFactory(Class objectClass, - Class objectListClass) { - this.objectClass = objectClass; - this.objectListClass = objectListClass; - } - - /** - * Creates a type adapter for the given type. - * - * @param the generic type - * @param gson the gson - * @param typeToken the type token - * @return the type adapter or null if the type is not handles by - * this factory - */ - @SuppressWarnings("unchecked") - @Override - public TypeAdapter create(Gson gson, TypeToken typeToken) { - if (TypeToken.get(objectClass).equals(typeToken)) { - return (TypeAdapter) new ModelCreator(gson); - } - if (TypeToken.get(objectListClass).equals(typeToken)) { - return (TypeAdapter) new ModelsCreator(gson); - } - return null; - } - - /** - * The Class ModelCreator. - */ - private class ModelCreator extends TypeAdapter - implements InstanceCreator { - private final Gson delegate; - - /** - * Instantiates a new object state creator. - * - * @param delegate the delegate - */ - public ModelCreator(Gson delegate) { - this.delegate = delegate; - } - - @Override - public O createInstance(Type type) { - try { - return objectClass.getConstructor(Gson.class, JsonObject.class) - .newInstance(delegate, null); - } catch (InstantiationException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - return null; - } - } - - @Override - public void write(JsonWriter jsonWriter, O state) - throws IOException { - jsonWriter.jsonValue(delegate.toJson(state.data())); - } - - @Override - public O read(JsonReader jsonReader) - throws IOException { - try { - return objectClass.getConstructor(Gson.class, JsonObject.class) - .newInstance(delegate, - delegate.fromJson(jsonReader, JsonObject.class)); - } catch (InstantiationException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - return null; - } - } - } - - /** - * The Class ModelsCreator. - */ - private class ModelsCreator extends TypeAdapter - implements InstanceCreator { - - private final Gson delegate; - - /** - * Instantiates a new object states creator. - * - * @param delegate the delegate - */ - public ModelsCreator(Gson delegate) { - this.delegate = delegate; - } - - @Override - public L createInstance(Type type) { - try { - return objectListClass - .getConstructor(Gson.class, JsonObject.class) - .newInstance(delegate, null); - } catch (InstantiationException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - return null; - } - } - - @Override - public void write(JsonWriter jsonWriter, L states) - throws IOException { - jsonWriter.jsonValue(delegate.toJson(states.data())); - } - - @Override - public L read(JsonReader jsonReader) - throws IOException { - try { - return objectListClass - .getConstructor(Gson.class, JsonObject.class) - .newInstance(delegate, - delegate.fromJson(jsonReader, JsonObject.class)); - } catch (InstantiationException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - return null; - } - } - } - -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java index 3870337..f61b431 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023,2024 Michael N. Lipp + * Copyright (C) 2023 Michael N. Lipp * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,141 +18,170 @@ package org.jdrupes.vmoperator.common; -import com.google.gson.JsonObject; -import io.kubernetes.client.Discovery; -import io.kubernetes.client.Discovery.APIResource; import io.kubernetes.client.common.KubernetesListObject; import io.kubernetes.client.common.KubernetesObject; -import io.kubernetes.client.common.KubernetesType; import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.apis.EventsV1Api; -import io.kubernetes.client.openapi.models.EventsV1Event; +import io.kubernetes.client.openapi.apis.ApisApi; +import io.kubernetes.client.openapi.apis.CustomObjectsApi; +import io.kubernetes.client.openapi.models.V1APIGroup; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1ObjectReference; -import io.kubernetes.client.util.Strings; +import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; +import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimList; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodList; import io.kubernetes.client.util.generic.GenericKubernetesApi; -import io.kubernetes.client.util.generic.KubernetesApiResponse; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import io.kubernetes.client.util.generic.options.DeleteOptions; import io.kubernetes.client.util.generic.options.PatchOptions; -import java.io.Reader; -import java.net.HttpURLConnection; -import java.time.OffsetDateTime; -import java.util.Map; import java.util.Optional; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.SafeConstructor; /** * Helpers for K8s API. */ -@SuppressWarnings({ "PMD.ShortClassName", "PMD.UseUtilityClass" }) +@SuppressWarnings({ "PMD.ShortClassName", "PMD.UseUtilityClass", + "PMD.DataflowAnomalyAnalysis" }) public class K8s { /** - * Returns the result from an API call as {@link Optional} if the - * call was successful. Returns an empty `Optional` if the status - * code is 404 (not found). Else throws an exception. + * Given a groupVersion, returns only the version. * - * @param the generic type - * @param response the response - * @return the optional - * @throws ApiException the API exception + * @param groupVersion the group version + * @return the string */ - public static Optional - optional(KubernetesApiResponse response) throws ApiException { - if (response.isSuccess()) { - return Optional.of(response.getObject()); + public static String version(String groupVersion) { + return groupVersion.substring(groupVersion.lastIndexOf('/') + 1); + } + + /** + * Get PVC API. + * + * @param client the client + * @return the generic kubernetes api + */ + public static GenericKubernetesApi pvcApi(ApiClient client) { + return new GenericKubernetesApi<>(V1PersistentVolumeClaim.class, + V1PersistentVolumeClaimList.class, "", "v1", + "persistentvolumeclaims", client); + } + + /** + * Get config map API. + * + * @param client the client + * @return the generic kubernetes api + */ + public static GenericKubernetesApi cmApi(ApiClient client) { + return new GenericKubernetesApi<>(V1ConfigMap.class, + V1ConfigMapList.class, "", "v1", "configmaps", client); + } + + /** + * Get pod API. + * + * @param client the client + * @return the generic kubernetes api + */ + public static GenericKubernetesApi + podApi(ApiClient client) { + return new GenericKubernetesApi<>(V1Pod.class, V1PodList.class, "", + "v1", "pods", client); + } + + /** + * Get the API for a custom resource. + * + * @param client the client + * @param group the group + * @param kind the kind + * @param namespace the namespace + * @param name the name + * @return the dynamic kubernetes api + * @throws ApiException the api exception + */ + @SuppressWarnings("PMD.UseObjectForClearerAPI") + public static Optional crApi(ApiClient client, + String group, String kind, String namespace, String name) + throws ApiException { + var apis = new ApisApi(client).getAPIVersions(); + var crdVersions = apis.getGroups().stream() + .filter(g -> g.getName().equals(group)).findFirst() + .map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream()) + .map(V1GroupVersionForDiscovery::getVersion).toList(); + var coa = new CustomObjectsApi(client); + for (var crdVersion : crdVersions) { + var crdApiRes = coa.getAPIResources(group, crdVersion) + .getResources().stream().filter(r -> kind.equals(r.getKind())) + .findFirst(); + if (crdApiRes.isEmpty()) { + continue; + } + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var crApi = new DynamicKubernetesApi(group, + crdVersion, crdApiRes.get().getName(), client); + var customResource = crApi.get(namespace, name); + if (customResource.isSuccess()) { + return Optional.of(crApi); + } } - if (response.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) { - return Optional.empty(); - } - response.throwsApiException(); - // Never reached return Optional.empty(); } /** - * Returns a new context with the given version as preferred version. + * Get an object from its metadata. * - * @param context the context - * @param version the version - * @return the API resource + * @param the generic type + * @param the generic type + * @param api the api + * @param meta the meta + * @return the object */ - public static APIResource preferred(APIResource context, String version) { - assert context.getVersions().contains(version); - return new APIResource(context.getGroup(), - context.getVersions(), version, context.getKind(), - context.getNamespaced(), context.getResourcePlural(), - context.getResourceSingular()); - } - - /** - * Return a string representation of the context (API resource). - * - * @param context the context - * @return the string - */ - @SuppressWarnings("PMD.UseLocaleWithCaseConversions") - public static String toString(APIResource context) { - return (Strings.isNullOrEmpty(context.getGroup()) ? "" - : context.getGroup() + "/") - + context.getPreferredVersion().toUpperCase() - + context.getKind(); - } - - /** - * Convert Yaml to Json. - * - * @param client the client - * @param yaml the yaml - * @return the json element - */ - public static JsonObject yamlToJson(ApiClient client, Reader yaml) { - // Avoid Yaml.load due to - // https://github.com/kubernetes-client/java/issues/2741 - Map yamlData - = new Yaml(new SafeConstructor(new LoaderOptions())).load(yaml); - - // There's no short-cut from Java (collections) to Gson - var gson = client.getJSON().getGson(); - var jsonText = gson.toJson(yamlData); - return gson.fromJson(jsonText, JsonObject.class); - } - - /** - * Lookup the specified API resource. If the version is `null` or - * empty, the preferred version in the result is the default - * returned from the server. - * - * @param client the client - * @param group the group - * @param version the version - * @param kind the kind - * @return the optional - * @throws ApiException the api exception - */ - public static Optional context(ApiClient client, - String group, String version, String kind) throws ApiException { - var apiMatch = new Discovery(client).findAll().stream() - .filter(r -> r.getGroup().equals(group) && r.getKind().equals(kind) - && (Strings.isNullOrEmpty(version) - || r.getVersions().contains(version))) - .findFirst(); - if (apiMatch.isEmpty()) { - return Optional.empty(); + public static + Optional + get(GenericKubernetesApi api, V1ObjectMeta meta) { + var response = api.get(meta.getNamespace(), meta.getName()); + if (response.isSuccess()) { + return Optional.of(response.getObject()); } - var apiRes = apiMatch.get(); - if (!Strings.isNullOrEmpty(version)) { - if (!apiRes.getVersions().contains(version)) { - return Optional.empty(); - } - apiRes = new APIResource(apiRes.getGroup(), apiRes.getVersions(), - version, apiRes.getKind(), apiRes.getNamespaced(), - apiRes.getResourcePlural(), apiRes.getResourceSingular()); - } - return Optional.of(apiRes); + return Optional.empty(); + } + + /** + * Delete an object. + * + * @param the generic type + * @param the generic type + * @param api the api + * @param object the object + */ + public static + void delete(GenericKubernetesApi api, T object) + throws ApiException { + api.delete(object.getMetadata().getNamespace(), + object.getMetadata().getName()).throwsApiException(); + } + + /** + * Delete an object. + * + * @param the generic type + * @param the generic type + * @param api the api + * @param object the object + */ + public static + void delete(GenericKubernetesApi api, T object, + DeleteOptions options) throws ApiException { + api.delete(object.getMetadata().getNamespace(), + object.getMetadata().getName(), options).throwsApiException(); } /** @@ -163,10 +192,8 @@ public class K8s { * @param api the api * @param existing the existing * @param update the update - * @return the t * @throws ApiException the api exception */ - @SuppressWarnings("PMD.GenericsNaming") public static T apply(GenericKubernetesApi api, T existing, String update) throws ApiException { @@ -186,7 +213,7 @@ public class K8s { * @return the v 1 object reference */ public static V1ObjectReference - objectReference(KubernetesObject object) { + objectReference(DynamicKubernetesObject object) { return new V1ObjectReference().apiVersion(object.getApiVersion()) .kind(object.getKind()) .namespace(object.getMetadata().getNamespace()) @@ -194,56 +221,4 @@ public class K8s { .resourceVersion(object.getMetadata().getResourceVersion()) .uid(object.getMetadata().getUid()); } - - /** - * Creates an event related to the object, adding reasonable defaults. - * - * * If `kind` is not set, it is set to "Event". - * * If `metadata.namespace` is not set, it is set - * to the object's namespace. - * * If neither `metadata.name` nor `matadata.generateName` are set, - * set `generateName` to the object's name with a dash appended. - * * If `reportingInstance` is not set, set it to the object's name. - * * If `eventTime` is not set, set it to now. - * * If `type` is not set, set it to "Normal" - * * If `regarding` is not set, set it to the given object. - * - * @param client the client - * @param object the object - * @param event the event - * @throws ApiException the api exception - */ - @SuppressWarnings("PMD.NPathComplexity") - public static void createEvent(ApiClient client, - KubernetesObject object, EventsV1Event event) - throws ApiException { - if (Strings.isNullOrEmpty(event.getKind())) { - event.kind("Event"); - } - if (event.getMetadata() == null) { - event.metadata(new V1ObjectMeta()); - } - if (Strings.isNullOrEmpty(event.getMetadata().getNamespace())) { - event.getMetadata().namespace(object.getMetadata().getNamespace()); - } - if (Strings.isNullOrEmpty(event.getMetadata().getName()) - && Strings.isNullOrEmpty(event.getMetadata().getGenerateName())) { - event.getMetadata() - .generateName(object.getMetadata().getName() + "-"); - } - if (Strings.isNullOrEmpty(event.getReportingInstance())) { - event.reportingInstance(object.getMetadata().getName()); - } - if (event.getEventTime() == null) { - event.eventTime(OffsetDateTime.now()); - } - if (Strings.isNullOrEmpty(event.getType())) { - event.type("Normal"); - } - if (event.getRegarding() == null) { - event.regarding(objectReference(object)); - } - new EventsV1Api(client).createNamespacedEvent( - object.getMetadata().getNamespace(), event, null, null, null, null); - } } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java deleted file mode 100644 index 272da2b..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java +++ /dev/null @@ -1,954 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.openapi.ApiCallback; -import io.kubernetes.client.openapi.ApiClient; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.ApiResponse; -import io.kubernetes.client.openapi.JSON; -import io.kubernetes.client.openapi.Pair; -import io.kubernetes.client.openapi.auth.Authentication; -import io.kubernetes.client.util.ClientBuilder; -import io.kubernetes.client.util.generic.options.PatchOptions; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Type; -import java.text.DateFormat; -import java.time.format.DateTimeFormatter; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import javax.net.ssl.KeyManager; -import okhttp3.Call; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Request.Builder; -import okhttp3.RequestBody; -import okhttp3.Response; - -/** - * A client with some additional properties. - */ -@SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyMethods", - "checkstyle:LineLength", "PMD.CouplingBetweenObjects", "PMD.GodClass" }) -public class K8sClient extends ApiClient { - - private ApiClient apiClient; - private PatchOptions defaultPatchOptions; - - /** - * Instantiates a new client. - * - * @throws IOException Signals that an I/O exception has occurred. - */ - public K8sClient() throws IOException { - defaultPatchOptions = new PatchOptions(); - defaultPatchOptions.setFieldManager("kubernetes-java-kubectl-apply"); - } - - private ApiClient apiClient() { - if (apiClient == null) { - try { - apiClient = ClientBuilder.standard().build(); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - return apiClient; - } - - /** - * Gets the default patch options. - * - * @return the defaultPatchOptions - */ - public PatchOptions defaultPatchOptions() { - return defaultPatchOptions; - } - - /** - * Changes the default patch options. - * - * @param patchOptions the patch options - * @return the client - */ - public K8sClient with(PatchOptions patchOptions) { - defaultPatchOptions = patchOptions; - return this; - } - - /** - * Gets the base path. - * - * @return the base path - * @see ApiClient#getBasePath() - */ - @Override - public String getBasePath() { - return apiClient().getBasePath(); - } - - /** - * Sets the base path. - * - * @param basePath the base path - * @return the api client - * @see ApiClient#setBasePath(java.lang.String) - */ - @Override - public ApiClient setBasePath(String basePath) { - return apiClient().setBasePath(basePath); - } - - /** - * Gets the http client. - * - * @return the http client - * @see ApiClient#getHttpClient() - */ - @Override - public OkHttpClient getHttpClient() { - return apiClient().getHttpClient(); - } - - /** - * Sets the http client. - * - * @param newHttpClient the new http client - * @return the api client - * @see ApiClient#setHttpClient(okhttp3.OkHttpClient) - */ - @Override - public ApiClient setHttpClient(OkHttpClient newHttpClient) { - return apiClient().setHttpClient(newHttpClient); - } - - /** - * Gets the json. - * - * @return the json - * @see ApiClient#getJSON() - */ - @SuppressWarnings("abbreviationAsWordInName") - @Override - public JSON getJSON() { - return apiClient().getJSON(); - } - - /** - * Sets the JSON. - * - * @param json the json - * @return the api client - * @see ApiClient#setJSON(io.kubernetes.client.openapi.JSON) - */ - @SuppressWarnings("abbreviationAsWordInName") - @Override - public ApiClient setJSON(JSON json) { - return apiClient().setJSON(json); - } - - /** - * Checks if is verifying ssl. - * - * @return true, if is verifying ssl - * @see ApiClient#isVerifyingSsl() - */ - @Override - public boolean isVerifyingSsl() { - return apiClient().isVerifyingSsl(); - } - - /** - * Sets the verifying ssl. - * - * @param verifyingSsl the verifying ssl - * @return the api client - * @see ApiClient#setVerifyingSsl(boolean) - */ - @Override - public ApiClient setVerifyingSsl(boolean verifyingSsl) { - return apiClient().setVerifyingSsl(verifyingSsl); - } - - /** - * Gets the ssl ca cert. - * - * @return the ssl ca cert - * @see ApiClient#getSslCaCert() - */ - @Override - public InputStream getSslCaCert() { - return apiClient().getSslCaCert(); - } - - /** - * Sets the ssl ca cert. - * - * @param sslCaCert the ssl ca cert - * @return the api client - * @see ApiClient#setSslCaCert(java.io.InputStream) - */ - @Override - public ApiClient setSslCaCert(InputStream sslCaCert) { - return apiClient().setSslCaCert(sslCaCert); - } - - /** - * Gets the key managers. - * - * @return the key managers - * @see ApiClient#getKeyManagers() - */ - @Override - public KeyManager[] getKeyManagers() { - return apiClient().getKeyManagers(); - } - - /** - * Sets the key managers. - * - * @param managers the managers - * @return the api client - * @see ApiClient#setKeyManagers(javax.net.ssl.KeyManager[]) - */ - @Override - public ApiClient setKeyManagers(KeyManager[] managers) { - return apiClient().setKeyManagers(managers); - } - - /** - * Gets the date format. - * - * @return the date format - * @see ApiClient#getDateFormat() - */ - @Override - public DateFormat getDateFormat() { - return apiClient().getDateFormat(); - } - - /** - * Sets the date format. - * - * @param dateFormat the date format - * @return the api client - * @see ApiClient#setDateFormat(java.text.DateFormat) - */ - @Override - public ApiClient setDateFormat(DateFormat dateFormat) { - return apiClient().setDateFormat(dateFormat); - } - - /** - * Sets the sql date format. - * - * @param dateFormat the date format - * @return the api client - * @see ApiClient#setSqlDateFormat(java.text.DateFormat) - */ - @Override - public ApiClient setSqlDateFormat(DateFormat dateFormat) { - return apiClient().setSqlDateFormat(dateFormat); - } - - /** - * Sets the offset date time format. - * - * @param dateFormat the date format - * @return the api client - * @see ApiClient#setOffsetDateTimeFormat(java.time.format.DateTimeFormatter) - */ - @Override - public ApiClient setOffsetDateTimeFormat(DateTimeFormatter dateFormat) { - return apiClient().setOffsetDateTimeFormat(dateFormat); - } - - /** - * Sets the local date format. - * - * @param dateFormat the date format - * @return the api client - * @see ApiClient#setLocalDateFormat(java.time.format.DateTimeFormatter) - */ - @Override - public ApiClient setLocalDateFormat(DateTimeFormatter dateFormat) { - return apiClient().setLocalDateFormat(dateFormat); - } - - /** - * Sets the lenient on json. - * - * @param lenientOnJson the lenient on json - * @return the api client - * @see ApiClient#setLenientOnJson(boolean) - */ - @Override - public ApiClient setLenientOnJson(boolean lenientOnJson) { - return apiClient().setLenientOnJson(lenientOnJson); - } - - /** - * Gets the authentications. - * - * @return the authentications - * @see ApiClient#getAuthentications() - */ - @Override - public Map getAuthentications() { - return apiClient().getAuthentications(); - } - - /** - * Gets the authentication. - * - * @param authName the auth name - * @return the authentication - * @see ApiClient#getAuthentication(java.lang.String) - */ - @Override - public Authentication getAuthentication(String authName) { - return apiClient().getAuthentication(authName); - } - - /** - * Sets the username. - * - * @param username the new username - * @see ApiClient#setUsername(java.lang.String) - */ - @Override - public void setUsername(String username) { - apiClient().setUsername(username); - } - - /** - * Sets the password. - * - * @param password the new password - * @see ApiClient#setPassword(java.lang.String) - */ - @Override - public void setPassword(String password) { - apiClient().setPassword(password); - } - - /** - * Sets the api key. - * - * @param apiKey the new api key - * @see ApiClient#setApiKey(java.lang.String) - */ - @Override - public void setApiKey(String apiKey) { - apiClient().setApiKey(apiKey); - } - - /** - * Sets the api key prefix. - * - * @param apiKeyPrefix the new api key prefix - * @see ApiClient#setApiKeyPrefix(java.lang.String) - */ - @Override - public void setApiKeyPrefix(String apiKeyPrefix) { - apiClient().setApiKeyPrefix(apiKeyPrefix); - } - - /** - * Sets the access token. - * - * @param accessToken the new access token - * @see ApiClient#setAccessToken(java.lang.String) - */ - @Override - public void setAccessToken(String accessToken) { - apiClient().setAccessToken(accessToken); - } - - /** - * Sets the user agent. - * - * @param userAgent the user agent - * @return the api client - * @see ApiClient#setUserAgent(java.lang.String) - */ - @Override - public ApiClient setUserAgent(String userAgent) { - return apiClient().setUserAgent(userAgent); - } - - /** - * To string. - * - * @return the string - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return apiClient().toString(); - } - - /** - * Adds the default header. - * - * @param key the key - * @param value the value - * @return the api client - * @see ApiClient#addDefaultHeader(java.lang.String, java.lang.String) - */ - @Override - public ApiClient addDefaultHeader(String key, String value) { - return apiClient().addDefaultHeader(key, value); - } - - /** - * Adds the default cookie. - * - * @param key the key - * @param value the value - * @return the api client - * @see ApiClient#addDefaultCookie(java.lang.String, java.lang.String) - */ - @Override - public ApiClient addDefaultCookie(String key, String value) { - return apiClient().addDefaultCookie(key, value); - } - - /** - * Checks if is debugging. - * - * @return true, if is debugging - * @see ApiClient#isDebugging() - */ - @Override - public boolean isDebugging() { - return apiClient().isDebugging(); - } - - /** - * Sets the debugging. - * - * @param debugging the debugging - * @return the api client - * @see ApiClient#setDebugging(boolean) - */ - @Override - public ApiClient setDebugging(boolean debugging) { - return apiClient().setDebugging(debugging); - } - - /** - * Gets the temp folder path. - * - * @return the temp folder path - * @see ApiClient#getTempFolderPath() - */ - @Override - public String getTempFolderPath() { - return apiClient().getTempFolderPath(); - } - - /** - * Sets the temp folder path. - * - * @param tempFolderPath the temp folder path - * @return the api client - * @see ApiClient#setTempFolderPath(java.lang.String) - */ - @Override - public ApiClient setTempFolderPath(String tempFolderPath) { - return apiClient().setTempFolderPath(tempFolderPath); - } - - /** - * Gets the connect timeout. - * - * @return the connect timeout - * @see ApiClient#getConnectTimeout() - */ - @Override - public int getConnectTimeout() { - return apiClient().getConnectTimeout(); - } - - /** - * Sets the connect timeout. - * - * @param connectionTimeout the connection timeout - * @return the api client - * @see ApiClient#setConnectTimeout(int) - */ - @Override - public ApiClient setConnectTimeout(int connectionTimeout) { - return apiClient().setConnectTimeout(connectionTimeout); - } - - /** - * Gets the read timeout. - * - * @return the read timeout - * @see ApiClient#getReadTimeout() - */ - @Override - public int getReadTimeout() { - return apiClient().getReadTimeout(); - } - - /** - * Sets the read timeout. - * - * @param readTimeout the read timeout - * @return the api client - * @see ApiClient#setReadTimeout(int) - */ - @Override - public ApiClient setReadTimeout(int readTimeout) { - return apiClient().setReadTimeout(readTimeout); - } - - /** - * Gets the write timeout. - * - * @return the write timeout - * @see ApiClient#getWriteTimeout() - */ - @Override - public int getWriteTimeout() { - return apiClient().getWriteTimeout(); - } - - /** - * Sets the write timeout. - * - * @param writeTimeout the write timeout - * @return the api client - * @see ApiClient#setWriteTimeout(int) - */ - @Override - public ApiClient setWriteTimeout(int writeTimeout) { - return apiClient().setWriteTimeout(writeTimeout); - } - - /** - * Parameter to string. - * - * @param param the param - * @return the string - * @see ApiClient#parameterToString(java.lang.Object) - */ - @Override - public String parameterToString(Object param) { - return apiClient().parameterToString(param); - } - - /** - * Parameter to pair. - * - * @param name the name - * @param value the value - * @return the list - * @see ApiClient#parameterToPair(java.lang.String, java.lang.Object) - */ - @Override - public List parameterToPair(String name, Object value) { - return apiClient().parameterToPair(name, value); - } - - /** - * Parameter to pairs. - * - * @param collectionFormat the collection format - * @param name the name - * @param value the value - * @return the list - * @see ApiClient#parameterToPairs(java.lang.String, java.lang.String, java.util.Collection) - */ - @SuppressWarnings({ "rawtypes", "PMD.AvoidDuplicateLiterals" }) - @Override - public List parameterToPairs(String collectionFormat, String name, - Collection value) { - return apiClient().parameterToPairs(collectionFormat, name, value); - } - - /** - * Collection path parameter to string. - * - * @param collectionFormat the collection format - * @param value the value - * @return the string - * @see ApiClient#collectionPathParameterToString(java.lang.String, java.util.Collection) - */ - @SuppressWarnings("rawtypes") - @Override - public String collectionPathParameterToString(String collectionFormat, - Collection value) { - return apiClient().collectionPathParameterToString(collectionFormat, - value); - } - - /** - * Sanitize filename. - * - * @param filename the filename - * @return the string - * @see ApiClient#sanitizeFilename(java.lang.String) - */ - @Override - public String sanitizeFilename(String filename) { - return apiClient().sanitizeFilename(filename); - } - - /** - * Checks if is json mime. - * - * @param mime the mime - * @return true, if is json mime - * @see ApiClient#isJsonMime(java.lang.String) - */ - @Override - public boolean isJsonMime(String mime) { - return apiClient().isJsonMime(mime); - } - - /** - * Select header accept. - * - * @param accepts the accepts - * @return the string - * @see ApiClient#selectHeaderAccept(java.lang.String[]) - */ - @Override - public String selectHeaderAccept(String[] accepts) { - return apiClient().selectHeaderAccept(accepts); - } - - /** - * Select header content type. - * - * @param contentTypes the content types - * @return the string - * @see ApiClient#selectHeaderContentType(java.lang.String[]) - */ - @Override - public String selectHeaderContentType(String[] contentTypes) { - return apiClient().selectHeaderContentType(contentTypes); - } - - /** - * Escape string. - * - * @param str the str - * @return the string - * @see ApiClient#escapeString(java.lang.String) - */ - @Override - public String escapeString(String str) { - return apiClient().escapeString(str); - } - - /** - * Deserialize. - * - * @param the generic type - * @param response the response - * @param returnType the return type - * @return the t - * @throws ApiException the api exception - * @see ApiClient#deserialize(okhttp3.Response, java.lang.reflect.Type) - */ - @Override - public T deserialize(Response response, Type returnType) - throws ApiException { - return apiClient().deserialize(response, returnType); - } - - /** - * Serialize. - * - * @param obj the obj - * @param contentType the content type - * @return the request body - * @throws ApiException the api exception - * @see ApiClient#serialize(java.lang.Object, java.lang.String) - */ - @Override - public RequestBody serialize(Object obj, String contentType) - throws ApiException { - return apiClient().serialize(obj, contentType); - } - - /** - * Download file from response. - * - * @param response the response - * @return the file - * @throws ApiException the api exception - * @see ApiClient#downloadFileFromResponse(okhttp3.Response) - */ - @Override - public File downloadFileFromResponse(Response response) - throws ApiException { - return apiClient().downloadFileFromResponse(response); - } - - /** - * Prepare download file. - * - * @param response the response - * @return the file - * @throws IOException Signals that an I/O exception has occurred. - * @see ApiClient#prepareDownloadFile(okhttp3.Response) - */ - @Override - public File prepareDownloadFile(Response response) throws IOException { - return apiClient().prepareDownloadFile(response); - } - - /** - * Execute. - * - * @param the generic type - * @param call the call - * @return the api response - * @throws ApiException the api exception - * @see ApiClient#execute(okhttp3.Call) - */ - @Override - public ApiResponse execute(Call call) throws ApiException { - return apiClient().execute(call); - } - - /** - * Execute. - * - * @param the generic type - * @param call the call - * @param returnType the return type - * @return the api response - * @throws ApiException the api exception - * @see ApiClient#execute(okhttp3.Call, java.lang.reflect.Type) - */ - @Override - public ApiResponse execute(Call call, Type returnType) - throws ApiException { - return apiClient().execute(call, returnType); - } - - /** - * Execute async. - * - * @param the generic type - * @param call the call - * @param callback the callback - * @see ApiClient#executeAsync(okhttp3.Call, io.kubernetes.client.openapi.ApiCallback) - */ - @Override - public void executeAsync(Call call, ApiCallback callback) { - apiClient().executeAsync(call, callback); - } - - /** - * Execute async. - * - * @param the generic type - * @param call the call - * @param returnType the return type - * @param callback the callback - * @see ApiClient#executeAsync(okhttp3.Call, java.lang.reflect.Type, io.kubernetes.client.openapi.ApiCallback) - */ - @Override - public void executeAsync(Call call, Type returnType, - ApiCallback callback) { - apiClient().executeAsync(call, returnType, callback); - } - - /** - * Handle response. - * - * @param the generic type - * @param response the response - * @param returnType the return type - * @return the t - * @throws ApiException the api exception - * @see ApiClient#handleResponse(okhttp3.Response, java.lang.reflect.Type) - */ - @Override - public T handleResponse(Response response, Type returnType) - throws ApiException { - return apiClient().handleResponse(response, returnType); - } - - /** - * Builds the call. - * - * @param path the path - * @param method the method - * @param queryParams the query params - * @param collectionQueryParams the collection query params - * @param body the body - * @param headerParams the header params - * @param cookieParams the cookie params - * @param formParams the form params - * @param authNames the auth names - * @param callback the callback - * @return the call - * @throws ApiException the api exception - * @see ApiClient#buildCall(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback) - */ - @SuppressWarnings({ "rawtypes" }) - @Override - public Call buildCall(String path, String method, List queryParams, - List collectionQueryParams, Object body, - Map headerParams, Map cookieParams, - Map formParams, String[] authNames, - ApiCallback callback) throws ApiException { - return apiClient().buildCall(path, method, queryParams, - collectionQueryParams, body, headerParams, cookieParams, formParams, - authNames, callback); - } - - /** - * Builds the request. - * - * @param path the path - * @param method the method - * @param queryParams the query params - * @param collectionQueryParams the collection query params - * @param body the body - * @param headerParams the header params - * @param cookieParams the cookie params - * @param formParams the form params - * @param authNames the auth names - * @param callback the callback - * @return the request - * @throws ApiException the api exception - * @see ApiClient#buildRequest(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback) - */ - @SuppressWarnings({ "rawtypes" }) - @Override - public Request buildRequest(String path, String method, - List queryParams, List collectionQueryParams, - Object body, Map headerParams, - Map cookieParams, Map formParams, - String[] authNames, ApiCallback callback) throws ApiException { - return apiClient().buildRequest(path, method, queryParams, - collectionQueryParams, body, headerParams, cookieParams, formParams, - authNames, callback); - } - - /** - * Builds the url. - * - * @param path the path - * @param queryParams the query params - * @param collectionQueryParams the collection query params - * @return the string - * @see ApiClient#buildUrl(java.lang.String, java.util.List, java.util.List) - */ - @Override - public String buildUrl(String path, List queryParams, - List collectionQueryParams) { - return apiClient().buildUrl(path, queryParams, collectionQueryParams); - } - - /** - * Process header params. - * - * @param headerParams the header params - * @param reqBuilder the req builder - * @see ApiClient#processHeaderParams(java.util.Map, okhttp3.Request.Builder) - */ - @Override - public void processHeaderParams(Map headerParams, - Builder reqBuilder) { - apiClient().processHeaderParams(headerParams, reqBuilder); - } - - /** - * Process cookie params. - * - * @param cookieParams the cookie params - * @param reqBuilder the req builder - * @see ApiClient#processCookieParams(java.util.Map, okhttp3.Request.Builder) - */ - @Override - public void processCookieParams(Map cookieParams, - Builder reqBuilder) { - apiClient().processCookieParams(cookieParams, reqBuilder); - } - - /** - * Update params for auth. - * - * @param authNames the auth names - * @param queryParams the query params - * @param headerParams the header params - * @param cookieParams the cookie params - * @see ApiClient#updateParamsForAuth(java.lang.String[], java.util.List, java.util.Map, java.util.Map) - */ - @Override - public void updateParamsForAuth(String[] authNames, List queryParams, - Map headerParams, - Map cookieParams) { - apiClient().updateParamsForAuth(authNames, queryParams, headerParams, - cookieParams); - } - - /** - * Builds the request body form encoding. - * - * @param formParams the form params - * @return the request body - * @see ApiClient#buildRequestBodyFormEncoding(java.util.Map) - */ - @Override - public RequestBody - buildRequestBodyFormEncoding(Map formParams) { - return apiClient().buildRequestBodyFormEncoding(formParams); - } - - /** - * Builds the request body multipart. - * - * @param formParams the form params - * @return the request body - * @see ApiClient#buildRequestBodyMultipart(java.util.Map) - */ - @Override - public RequestBody - buildRequestBodyMultipart(Map formParams) { - return apiClient().buildRequestBodyMultipart(formParams); - } - - /** - * Guess content type from file. - * - * @param file the file - * @return the string - * @see ApiClient#guessContentTypeFromFile(java.io.File) - */ - @Override - public String guessContentTypeFromFile(File file) { - return apiClient().guessContentTypeFromFile(file); - } - -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java deleted file mode 100644 index 59b4d12..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java +++ /dev/null @@ -1,396 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.common.KubernetesListObject; -import io.kubernetes.client.common.KubernetesObject; -import io.kubernetes.client.custom.V1Patch; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.Strings; -import io.kubernetes.client.util.generic.GenericKubernetesApi; -import io.kubernetes.client.util.generic.options.GetOptions; -import io.kubernetes.client.util.generic.options.ListOptions; -import io.kubernetes.client.util.generic.options.PatchOptions; -import java.net.HttpURLConnection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -/** - * A stub for cluster scoped objects. This stub provides the - * functions common to all Kubernetes objects, but uses variables - * for all types. This class should be used as base class only. - * - * @param the generic type - * @param the generic type - */ -@SuppressWarnings({ "PMD.CouplingBetweenObjects" }) -public class K8sClusterGenericStub { - protected final K8sClient client; - private final GenericKubernetesApi api; - protected final APIResource context; - protected final String name; - - /** - * Instantiates a new stub for the object specified. If the object - * exists in the context specified, the version (see - * {@link #version()} is bound to the existing object's version. - * Else the stub is dangling with the version set to the context's - * preferred version. - * - * @param objectClass the object class - * @param objectListClass the object list class - * @param client the client - * @param context the context - * @param name the name - */ - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - protected K8sClusterGenericStub(Class objectClass, - Class objectListClass, K8sClient client, APIResource context, - String name) { - this.client = client; - this.name = name; - - // Bind version - var foundVersion = context.getPreferredVersion(); - GenericKubernetesApi testApi = null; - GetOptions mdOpts - = new GetOptions().isPartialObjectMetadataRequest(true); - for (var version : candidateVersions(context)) { - testApi = new GenericKubernetesApi<>(objectClass, objectListClass, - context.getGroup(), version, context.getResourcePlural(), - client); - if (testApi.get(name, mdOpts).isSuccess()) { - foundVersion = version; - break; - } - } - if (foundVersion.equals(context.getPreferredVersion())) { - this.context = context; - } else { - this.context = K8s.preferred(context, foundVersion); - } - - api = Optional.ofNullable(testApi) - .orElseGet(() -> new GenericKubernetesApi<>(objectClass, - objectListClass, group(), version(), plural(), client)); - } - - /** - * Gets the context. - * - * @return the context - */ - public APIResource context() { - return context; - } - - /** - * Gets the group. - * - * @return the group - */ - public String group() { - return context.getGroup(); - } - - /** - * Gets the version. - * - * @return the version - */ - public String version() { - return context.getPreferredVersion(); - } - - /** - * Gets the kind. - * - * @return the kind - */ - public String kind() { - return context.getKind(); - } - - /** - * Gets the plural. - * - * @return the plural - */ - public String plural() { - return context.getResourcePlural(); - } - - /** - * Gets the name. - * - * @return the name - */ - public String name() { - return name; - } - - /** - * Delete the Kubernetes object. - * - * @throws ApiException the API exception - */ - public void delete() throws ApiException { - var result = api.delete(name); - if (result.isSuccess() - || result.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) { - return; - } - result.throwsApiException(); - } - - /** - * Retrieves and returns the current state of the object. - * - * @return the object's state - * @throws ApiException the api exception - */ - public Optional model() throws ApiException { - return K8s.optional(api.get(name)); - } - - /** - * Updates the object's status. - * - * @param object the current state of the object (passed to `status`) - * @param status function that returns the new status - * @return the updated model or empty if not successful - * @throws ApiException the api exception - */ - public Optional updateStatus(O object, - Function status) throws ApiException { - return K8s.optional(api.updateStatus(object, status)); - } - - /** - * Updates the status. - * - * @param status the status - * @return the kubernetes api response - * the updated model or empty if not successful - * @throws ApiException the api exception - */ - public Optional updateStatus(Function status) - throws ApiException { - return updateStatus(api.get(name).throwsApiException().getObject(), - status); - } - - /** - * Patch the object. - * - * @param patchType the patch type - * @param patch the patch - * @param options the options - * @return the kubernetes api response - * @throws ApiException the api exception - */ - public Optional patch(String patchType, V1Patch patch, - PatchOptions options) throws ApiException { - return K8s - .optional(api.patch(name, patchType, patch, options)); - } - - /** - * Patch the object using default options. - * - * @param patchType the patch type - * @param patch the patch - * @return the kubernetes api response - * @throws ApiException the api exception - */ - public Optional - patch(String patchType, V1Patch patch) throws ApiException { - PatchOptions opts = new PatchOptions(); - return patch(patchType, patch, opts); - } - - /** - * A supplier for generic stubs. - * - * @param the object type - * @param the object list type - * @param the result type - */ - @FunctionalInterface - public interface GenericSupplier> { - - /** - * Gets a new stub. - * - * @param objectClass the object class - * @param objectListClass the object list class - * @param client the client - * @param context the API resource - * @param name the name - * @return the result - */ - R get(Class objectClass, Class objectListClass, K8sClient client, - APIResource context, String name); - } - - @Override - @SuppressWarnings("PMD.UseLocaleWithCaseConversions") - public String toString() { - return (Strings.isNullOrEmpty(group()) ? "" : group() + "/") - + version().toUpperCase() + kind() + " " + name; - } - - /** - * Get an object stub. If the version in parameter - * `gvk` is an empty string, the stub refers to the first object - * found with matching group and kind. - * - * @param the object type - * @param the object list type - * @param the stub type - * @param objectClass the object class - * @param objectListClass the object list class - * @param client the client - * @param gvk the group, version and kind - * @param name the name - * @param provider the provider - * @return the stub if the object exists - * @throws ApiException the api exception - */ - public static > - R get(Class objectClass, Class objectListClass, - K8sClient client, GroupVersionKind gvk, String name, - GenericSupplier provider) throws ApiException { - var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(), - gvk.getKind()); - if (context.isEmpty()) { - throw new ApiException("No known API for " + gvk.getGroup() - + "/" + gvk.getVersion() + " " + gvk.getKind()); - } - return provider.get(objectClass, objectListClass, client, context.get(), - name); - } - - /** - * Get an object stub. - * - * @param the object type - * @param the object list type - * @param the stub type - * @param objectClass the object class - * @param objectListClass the object list class - * @param client the client - * @param context the context - * @param name the name - * @param provider the provider - * @return the stub if the object exists - * @throws ApiException the api exception - */ - public static > - R get(Class objectClass, Class objectListClass, - K8sClient client, APIResource context, String name, - GenericSupplier provider) { - return provider.get(objectClass, objectListClass, client, context, - name); - } - - /** - * Get an object stub for a newly created object. - * - * @param the object type - * @param the object list type - * @param the stub type - * @param objectClass the object class - * @param objectListClass the object list class - * @param client the client - * @param context the context - * @param model the model - * @param provider the provider - * @return the stub if the object exists - * @throws ApiException the api exception - */ - public static > - R create(Class objectClass, Class objectListClass, - K8sClient client, APIResource context, O model, - GenericSupplier provider) throws ApiException { - var api = new GenericKubernetesApi<>(objectClass, objectListClass, - context.getGroup(), context.getPreferredVersion(), - context.getResourcePlural(), client); - api.create(model).throwsApiException(); - return provider.get(objectClass, objectListClass, client, - context, model.getMetadata().getName()); - } - - /** - * Get the stubs for the objects that match - * the criteria from the given options. - * - * @param the object type - * @param the object list type - * @param the stub type - * @param objectClass the object class - * @param objectListClass the object list class - * @param client the client - * @param context the context - * @param options the options - * @param provider the provider - * @return the collection - * @throws ApiException the api exception - */ - public static > - Collection list(Class objectClass, Class objectListClass, - K8sClient client, APIResource context, - ListOptions options, GenericSupplier provider) - throws ApiException { - var result = new ArrayList(); - for (var version : candidateVersions(context)) { - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - var api = new GenericKubernetesApi<>(objectClass, objectListClass, - context.getGroup(), version, context.getResourcePlural(), - client); - var objs = api.list(options).throwsApiException(); - for (var item : objs.getObject().getItems()) { - result.add(provider.get(objectClass, objectListClass, client, - context, item.getMetadata().getName())); - } - } - return result; - } - - private static List candidateVersions(APIResource context) { - var result = new LinkedList<>(context.getVersions()); - result.remove(context.getPreferredVersion()); - result.add(0, context.getPreferredVersion()); - return result; - } - -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java deleted file mode 100644 index 2392d3e..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import io.kubernetes.client.common.KubernetesObject; -import io.kubernetes.client.openapi.models.V1ObjectMeta; - -/** - * Represents a Kubernetes object using a JSON data structure. - * Some information that is common to all Kubernetes objects, - * notably the metadata, is made available through the methods - * defined by {@link KubernetesObject}. - */ -public class K8sDynamicModel implements KubernetesObject { - - private final V1ObjectMeta metadata; - private final JsonObject data; - - /** - * Instantiates a new model from the JSON representation. - * - * @param delegate the gson instance to use for extracting structured data - * @param json the JSON - */ - public K8sDynamicModel(Gson delegate, JsonObject json) { - this.data = json; - metadata = delegate.fromJson(data.get("metadata"), V1ObjectMeta.class); - } - - @Override - public String getApiVersion() { - return apiVersion(); - } - - /** - * Gets the API version. (Abbreviated method name for convenience.) - * - * @return the API version - */ - public String apiVersion() { - return data.get("apiVersion").getAsString(); - } - - @Override - public String getKind() { - return kind(); - } - - /** - * Gets the kind. (Abbreviated method name for convenience.) - * - * @return the kind - */ - public String kind() { - return data.get("kind").getAsString(); - } - - @Override - public V1ObjectMeta getMetadata() { - return metadata; - } - - /** - * Gets the metadata. (Abbreviated method name for convenience.) - * - * @return the metadata - */ - public V1ObjectMeta metadata() { - return metadata; - } - - /** - * Gets the data. - * - * @return the data - */ - public JsonObject data() { - return data; - } - - /** - * Convenience method for getting the status. - * - * @return the JSON object describing the status - */ - public JsonObject statusJson() { - return data.getAsJsonObject("status"); - } - - @Override - public String toString() { - return data.toString(); - } - -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java deleted file mode 100644 index d165c10..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import io.kubernetes.client.common.KubernetesListObject; - -/** - * Represents a list of Kubernetes objects each of which is - * represented using a JSON data structure. - * Some information that is common to all Kubernetes objects, - * notably the metadata, is made available through the methods - * defined by {@link KubernetesListObject}. - */ -public class K8sDynamicModels extends K8sDynamicModelsBase { - - /** - * Initialize the object list using the given JSON data. - * - * @param delegate the gson instance to use for extracting structured data - * @param data the data - */ - public K8sDynamicModels(Gson delegate, JsonObject data) { - super(K8sDynamicModel.class, delegate, data); - } - -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelsBase.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelsBase.java deleted file mode 100644 index 4e21c0e..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelsBase.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import io.kubernetes.client.common.KubernetesListObject; -import io.kubernetes.client.openapi.Configuration; -import io.kubernetes.client.openapi.models.V1ListMeta; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * Represents a list of Kubernetes objects each of which is - * represented using a JSON data structure. - * Some information that is common to all Kubernetes objects, - * notably the metadata, is made available through the methods - * defined by {@link KubernetesListObject}. - */ -public class K8sDynamicModelsBase - implements KubernetesListObject { - - private final JsonObject data; - private final V1ListMeta metadata; - private final List items; - - /** - * Initialize the object list using the given JSON data. - * - * @param itemClass the item class - * @param delegate the gson instance to use for extracting structured data - * @param data the data - */ - public K8sDynamicModelsBase(Class itemClass, Gson delegate, - JsonObject data) { - this.data = data; - metadata = delegate.fromJson(data.get("metadata"), V1ListMeta.class); - items = new ArrayList<>(); - for (JsonElement e : data.get("items").getAsJsonArray()) { - try { - items.add(itemClass.getConstructor(Gson.class, JsonObject.class) - .newInstance(delegate, e.getAsJsonObject())); - } catch (InstantiationException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException - | NoSuchMethodException | SecurityException exc) { - throw new IllegalArgumentException(exc); - } - } - } - - @Override - public String getApiVersion() { - return apiVersion(); - } - - /** - * Gets the API version. (Abbreviated method name for convenience.) - * - * @return the API version - */ - public String apiVersion() { - return data.get("apiVersion").getAsString(); - } - - @Override - public String getKind() { - return kind(); - } - - /** - * Gets the kind. (Abbreviated method name for convenience.) - * - * @return the kind - */ - public String kind() { - return data.get("kind").getAsString(); - } - - @Override - public V1ListMeta getMetadata() { - return metadata; - } - - /** - * Gets the metadata. (Abbreviated method name for convenience.) - * - * @return the metadata - */ - public V1ListMeta metadata() { - return metadata; - } - - /** - * Returns the JSON representation of this object. - * - * @return the JOSN representation - */ - public JsonObject data() { - return data; - } - - @Override - public List getItems() { - return items; - } - - /** - * Sets the api version. - * - * @param apiVersion the new api version - */ - public void setApiVersion(String apiVersion) { - data.addProperty("apiVersion", apiVersion); - } - - /** - * Sets the kind. - * - * @param kind the new kind - */ - public void setKind(String kind) { - data.addProperty("kind", kind); - } - - /** - * Sets the metadata. - * - * @param objectMeta the new metadata - */ - public void setMetadata(V1ListMeta objectMeta) { - data.add("metadata", - Configuration.getDefaultApiClient().getJSON().getGson() - .toJsonTree(objectMeta)); - } - - @Override - public int hashCode() { - return Objects.hash(data); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - K8sDynamicModelsBase other = (K8sDynamicModelsBase) obj; - return Objects.equals(data, other.data); - } -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java deleted file mode 100644 index c0303c2..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.io.Reader; -import java.util.Collection; - -/** - * A stub for namespaced custom objects. It uses a dynamic model - * (see {@link K8sDynamicModel}) for representing the object's - * state and can therefore be used for any kind of object, especially - * custom objects. - */ -public class K8sDynamicStub - extends K8sDynamicStubBase { - - private static DynamicTypeAdapterFactory taf = new K8sDynamicModelTypeAdapterFactory(); - - /** - * Instantiates a new dynamic stub. - * - * @param client the client - * @param context the context - * @param namespace the namespace - * @param name the name - */ - public K8sDynamicStub(K8sClient client, - APIResource context, String namespace, String name) { - super(K8sDynamicModel.class, K8sDynamicModels.class, taf, client, - context, namespace, name); - } - - /** - * Get a dynamic object stub. If the version in parameter - * `gvk` is an empty string, the stub refers to the first object with - * matching group and kind. - * - * @param client the client - * @param gvk the group, version and kind - * @param namespace the namespace - * @param name the name - * @return the stub if the object exists - * @throws ApiException the api exception - */ - public static K8sDynamicStub get(K8sClient client, - GroupVersionKind gvk, String namespace, String name) - throws ApiException { - return new K8sDynamicStub(client, apiResource(client, gvk), namespace, - name); - } - - /** - * Get a dynamic object stub. - * - * @param client the client - * @param context the context - * @param namespace the namespace - * @param name the name - * @return the stub if the object exists - * @throws ApiException the api exception - */ - public static K8sDynamicStub get(K8sClient client, - APIResource context, String namespace, String name) { - return new K8sDynamicStub(client, context, namespace, name); - } - - /** - * Creates a stub from yaml. - * - * @param client the client - * @param context the context - * @param yaml the yaml - * @return the k 8 s dynamic stub - * @throws ApiException the api exception - */ - public static K8sDynamicStub createFromYaml(K8sClient client, - APIResource context, Reader yaml) throws ApiException { - var model = new K8sDynamicModel(client.getJSON().getGson(), - K8s.yamlToJson(client, yaml)); - return K8sGenericStub.create(K8sDynamicModel.class, - K8sDynamicModels.class, client, context, model, - (c, ns, n) -> new K8sDynamicStub(c, context, ns, n)); - } - - /** - * Get the stubs for the objects in the given namespace that match - * the criteria from the given options. - * - * @param client the client - * @param namespace the namespace - * @param options the options - * @return the collection - * @throws ApiException the api exception - */ - public static Collection list(K8sClient client, - APIResource context, String namespace, ListOptions options) - throws ApiException { - return K8sGenericStub.list(K8sDynamicModel.class, - K8sDynamicModels.class, client, context, namespace, options, - (c, ns, n) -> new K8sDynamicStub(c, context, ns, n)); - } - - /** - * Get the stubs for the objects in the given namespace. - * - * @param client the client - * @param namespace the namespace - * @return the collection - * @throws ApiException the api exception - */ - public static Collection list(K8sClient client, - APIResource context, String namespace) - throws ApiException { - return list(client, context, namespace, new ListOptions()); - } - - /** - * A factory for creating K8sDynamicModel(s) objects. - */ - public static class K8sDynamicModelTypeAdapterFactory extends - DynamicTypeAdapterFactory { - - /** - * Instantiates a new dynamic model type adapter factory. - */ - public K8sDynamicModelTypeAdapterFactory() { - super(K8sDynamicModel.class, K8sDynamicModels.class); - } - } - -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java deleted file mode 100644 index ae3f012..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; - -/** - * A stub for namespaced custom objects. It uses a dynamic model - * (see {@link K8sDynamicModel}) for representing the object's - * state and can therefore be used for any kind of object, especially - * custom objects. - */ -public abstract class K8sDynamicStubBase> extends K8sGenericStub { - - /** - * Instantiates a new dynamic stub. - * - * @param objectClass the object class - * @param objectListClass the object list class - * @param client the client - * @param context the context - * @param namespace the namespace - * @param name the name - */ - public K8sDynamicStubBase(Class objectClass, - Class objectListClass, DynamicTypeAdapterFactory taf, - K8sClient client, APIResource context, String namespace, - String name) { - super(objectClass, objectListClass, client, context, namespace, name); - taf.register(client); - } -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java deleted file mode 100644 index 9ba376f..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java +++ /dev/null @@ -1,474 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.common.KubernetesListObject; -import io.kubernetes.client.common.KubernetesObject; -import io.kubernetes.client.custom.V1Patch; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.Strings; -import io.kubernetes.client.util.generic.GenericKubernetesApi; -import io.kubernetes.client.util.generic.KubernetesApiResponse; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; -import io.kubernetes.client.util.generic.options.GetOptions; -import io.kubernetes.client.util.generic.options.ListOptions; -import io.kubernetes.client.util.generic.options.PatchOptions; -import io.kubernetes.client.util.generic.options.UpdateOptions; -import java.net.HttpURLConnection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -/** - * A stub for namespaced custom objects. This stub provides the - * functions common to all Kubernetes objects, but uses variables - * for all types. This class should be used as base class only. - * - * @param the generic type - * @param the generic type - */ -@SuppressWarnings({ "PMD.TooManyMethods" }) -public class K8sGenericStub { - protected final K8sClient client; - private final GenericKubernetesApi api; - protected final APIResource context; - protected final String namespace; - protected final String name; - - /** - * Instantiates a new stub for the object specified. If the object - * exists in the context specified, the version (see - * {@link #version()} is bound to the existing object's version. - * Else the stub is dangling with the version set to the context's - * preferred version. - * - * @param objectClass the object class - * @param objectListClass the object list class - * @param client the client - * @param context the context - * @param namespace the namespace - * @param name the name - */ - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - protected K8sGenericStub(Class objectClass, Class objectListClass, - K8sClient client, APIResource context, String namespace, - String name) { - this.client = client; - this.namespace = namespace; - this.name = name; - - // Bind version - var foundVersion = context.getPreferredVersion(); - GenericKubernetesApi testApi = null; - GetOptions mdOpts - = new GetOptions().isPartialObjectMetadataRequest(true); - for (var version : candidateVersions(context)) { - testApi = new GenericKubernetesApi<>(objectClass, objectListClass, - context.getGroup(), version, context.getResourcePlural(), - client); - if (testApi.get(namespace, name, mdOpts) - .isSuccess()) { - foundVersion = version; - break; - } - } - if (foundVersion.equals(context.getPreferredVersion())) { - this.context = context; - } else { - this.context = K8s.preferred(context, foundVersion); - } - - api = Optional.ofNullable(testApi) - .orElseGet(() -> new GenericKubernetesApi<>(objectClass, - objectListClass, group(), version(), plural(), client)); - } - - /** - * Gets the context. - * - * @return the context - */ - public APIResource context() { - return context; - } - - /** - * Gets the group. - * - * @return the group - */ - public String group() { - return context.getGroup(); - } - - /** - * Gets the version. - * - * @return the version - */ - public String version() { - return context.getPreferredVersion(); - } - - /** - * Gets the kind. - * - * @return the kind - */ - public String kind() { - return context.getKind(); - } - - /** - * Gets the plural. - * - * @return the plural - */ - public String plural() { - return context.getResourcePlural(); - } - - /** - * Gets the namespace. - * - * @return the namespace - */ - public String namespace() { - return namespace; - } - - /** - * Gets the name. - * - * @return the name - */ - public String name() { - return name; - } - - /** - * Delete the Kubernetes object. - * - * @throws ApiException the API exception - */ - public void delete() throws ApiException { - var result = api.delete(namespace, name); - if (result.isSuccess() - || result.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) { - return; - } - result.throwsApiException(); - } - - /** - * Retrieves and returns the current state of the object. - * - * @return the object's state - * @throws ApiException the api exception - */ - public Optional model() throws ApiException { - return K8s.optional(api.get(namespace, name)); - } - - /** - * Updates the object's status. Does not retry in case of conflict. - * - * @param object the current state of the object (passed to `status`) - * @param updater function that returns the new status - * @return the updated model or empty if the object was not found - * @throws ApiException the api exception - */ - public Optional updateStatus(O object, Function updater) - throws ApiException { - return K8s.optional(api.updateStatus(object, updater)); - } - - /** - * Updates the status of the given object. In case of conflict, - * get the current version of the object and tries again. Retries - * up to `retries` times. - * - * @param updater the function updating the status - * @param current the current state of the object, used for the first - * attempt to update - * @param retries the retries in case of conflict - * @return the updated model or empty if the object was not found - * @throws ApiException the api exception - */ - @SuppressWarnings({ "PMD.AssignmentInOperand" }) - public Optional updateStatus(Function updater, O current, - int retries) throws ApiException { - while (true) { - try { - if (current == null) { - current = api.get(namespace, name) - .throwsApiException().getObject(); - } - return updateStatus(current, updater); - } catch (ApiException e) { - if (HttpURLConnection.HTTP_CONFLICT != e.getCode() - || retries-- <= 0) { - throw e; - } - // Get current version for new attempt - current = null; - } - } - } - - /** - * Gets the object and updates the status. In case of conflict, retries - * up to `retries` times. - * - * @param updater the function updating the status - * @param retries the retries in case of conflict - * @return the updated model or empty if the object was not found - * @throws ApiException the api exception - */ - public Optional updateStatus(Function updater, int retries) - throws ApiException { - return updateStatus(updater, null, retries); - } - - /** - * Updates the status of the given object. In case of conflict, - * get the current version of the object and tries again. Retries - * up to `retries` times. - * - * @param updater the function updating the status - * @param current the current - * @return the kubernetes api response - * the updated model or empty if not successful - * @throws ApiException the api exception - */ - public Optional updateStatus(Function updater, O current) - throws ApiException { - return updateStatus(updater, current, 16); - } - - /** - * Updates the status. In case of conflict, retries up to 16 times. - * - * @param updater the function updating the status - * @return the kubernetes api response - * the updated model or empty if not successful - * @throws ApiException the api exception - */ - public Optional updateStatus(Function updater) - throws ApiException { - return updateStatus(updater, null); - } - - /** - * Patch the object. - * - * @param patchType the patch type - * @param patch the patch - * @param options the options - * @return the kubernetes api response if successful - * @throws ApiException the api exception - */ - public Optional patch(String patchType, V1Patch patch, - PatchOptions options) throws ApiException { - return K8s - .optional(api.patch(namespace, name, patchType, patch, options) - .throwsApiException()); - } - - /** - * Patch the object using default options. - * - * @param patchType the patch type - * @param patch the patch - * @return the kubernetes api response if successful - * @throws ApiException the api exception - */ - public Optional - patch(String patchType, V1Patch patch) throws ApiException { - PatchOptions opts = new PatchOptions(); - return patch(patchType, patch, opts); - } - - /** - * Apply the given definition. - * - * @param def the def - * @return the kubernetes api response if successful - * @throws ApiException the api exception - */ - public Optional apply(DynamicKubernetesObject def) throws ApiException { - PatchOptions opts = new PatchOptions(); - opts.setForce(true); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - return patch(V1Patch.PATCH_FORMAT_APPLY_YAML, - new V1Patch(client.getJSON().serialize(def)), opts); - } - - /** - * Update the object. - * - * @param object the object - * @return the kubernetes api response - * @throws ApiException the api exception - */ - public KubernetesApiResponse update(O object) throws ApiException { - return api.update(object).throwsApiException(); - } - - /** - * Update the object. - * - * @param object the object - * @param options the options - * @return the kubernetes api response - * @throws ApiException the api exception - */ - public KubernetesApiResponse update(O object, UpdateOptions options) - throws ApiException { - return api.update(object, options).throwsApiException(); - } - - /** - * A supplier for generic stubs. - * - * @param the object type - * @param the object list type - * @param the result type - */ - @FunctionalInterface - public interface GenericSupplier> { - - /** - * Gets a new stub. - * - * @param client the client - * @param namespace the namespace - * @param name the name - * @return the result - */ - R get(K8sClient client, String namespace, String name); - } - - @Override - @SuppressWarnings("PMD.UseLocaleWithCaseConversions") - public String toString() { - return (Strings.isNullOrEmpty(group()) ? "" : group() + "/") - + version().toUpperCase() + kind() + " " + namespace + ":" + name; - } - - /** - * Get a namespaced object stub for a newly created object. - * - * @param the object type - * @param the object list type - * @param the stub type - * @param objectClass the object class - * @param objectListClass the object list class - * @param client the client - * @param context the context - * @param model the model - * @param provider the provider - * @return the stub if the object exists - * @throws ApiException the api exception - */ - public static > - R create(Class objectClass, Class objectListClass, - K8sClient client, APIResource context, O model, - GenericSupplier provider) throws ApiException { - var api = new GenericKubernetesApi<>(objectClass, objectListClass, - context.getGroup(), context.getPreferredVersion(), - context.getResourcePlural(), client); - api.create(model).throwsApiException(); - return provider.get(client, model.getMetadata().getNamespace(), - model.getMetadata().getName()); - } - - /** - * Get the stubs for the objects in the given namespace that match - * the criteria from the given options. - * - * @param the object type - * @param the object list type - * @param the stub type - * @param objectClass the object class - * @param objectListClass the object list class - * @param client the client - * @param context the context - * @param namespace the namespace - * @param options the options - * @param provider the provider - * @return the collection - * @throws ApiException the api exception - */ - public static > - Collection list(Class objectClass, Class objectListClass, - K8sClient client, APIResource context, String namespace, - ListOptions options, GenericSupplier provider) - throws ApiException { - var result = new ArrayList(); - for (var version : candidateVersions(context)) { - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - var api = new GenericKubernetesApi<>(objectClass, objectListClass, - context.getGroup(), version, context.getResourcePlural(), - client); - var objs = api.list(namespace, options).throwsApiException(); - for (var item : objs.getObject().getItems()) { - result.add(provider.get(client, namespace, - item.getMetadata().getName())); - } - } - return result; - } - - private static List candidateVersions(APIResource context) { - var result = new LinkedList<>(context.getVersions()); - result.remove(context.getPreferredVersion()); - result.add(0, context.getPreferredVersion()); - return result; - } - - /** - * Api resource. - * - * @param client the client - * @param gvk the gvk - * @return the API resource - * @throws ApiException the api exception - */ - public static APIResource apiResource(K8sClient client, - GroupVersionKind gvk) throws ApiException { - var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(), - gvk.getKind()); - if (context.isEmpty()) { - throw new ApiException("No known API for " + gvk.getGroup() - + "/" + gvk.getVersion() + " " + gvk.getKind()); - } - return context.get(); - } - -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java deleted file mode 100644 index 9e22382..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.common.KubernetesListObject; -import io.kubernetes.client.common.KubernetesObject; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.Watch.Response; -import io.kubernetes.client.util.generic.GenericKubernetesApi; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.time.Duration; -import java.time.Instant; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.jgrapes.core.Components; - -/** - * An observer that watches namespaced resources in a given context and - * invokes a handler on changes. - * - * @param the object type for the context - * @param the object list type for the context - */ -public class K8sObserver { - - /** - * The type of change reported by {@link Response} as enum. - */ - public enum ResponseType { - ADDED, MODIFIED, DELETED - } - - protected final Logger logger = Logger.getLogger(getClass().getName()); - - protected final K8sClient client; - protected final GenericKubernetesApi api; - protected final APIResource context; - protected final String namespace; - protected final ListOptions options; - protected final Thread thread; - protected BiConsumer> handler; - protected BiConsumer, Throwable> onTerminated; - - /** - * Create and start a new observer for objects in the given context - * (using preferred version) and namespace with the given options. - * - * @param objectClass the object class - * @param objectListClass the object list class - * @param client the client - * @param context the context - * @param namespace the namespace - * @param options the options - */ - @SuppressWarnings({ "PMD.AvoidCatchingThrowable", - "PMD.CognitiveComplexity", "PMD.AvoidCatchingGenericException" }) - public K8sObserver(Class objectClass, Class objectListClass, - K8sClient client, APIResource context, String namespace, - ListOptions options) { - this.client = client; - this.context = context; - this.namespace = namespace; - this.options = options; - - api = new GenericKubernetesApi<>(objectClass, objectListClass, - context.getGroup(), context.getPreferredVersion(), - context.getResourcePlural(), client); - thread = (Components.useVirtualThreads() ? Thread.ofVirtual() - : Thread.ofPlatform()).unstarted(() -> { - try { - logger.fine(() -> "Observing " + context.getResourcePlural() - + " (" + context.getPreferredVersion() + ")" - + Optional.ofNullable(options.getLabelSelector()) - .map(ls -> " with labels " + ls).orElse("") - + " in " + namespace); - - // Watch sometimes terminates without apparent reason. - while (!Thread.currentThread().isInterrupted()) { - Instant startedAt = Instant.now(); - try { - var changed - = api.watch(namespace, options).iterator(); - while (changed.hasNext()) { - var response = changed.next(); - logger.fine(() -> "Resource " - + context.getKind() + "/" - + response.object.getMetadata().getName() - + " " + response.type); - handler.accept(client, response); - } - } catch (ApiException | RuntimeException e) { - logger.log(Level.FINE, e, () -> "Problem watching" - + " resource " + context.getKind() - + " (will retry): " + e.getMessage()); - delayRestart(startedAt); - } - } - if (onTerminated != null) { - onTerminated.accept(this, null); - } - } catch (Throwable e) { - logger.log(Level.SEVERE, e, () -> "Probem watching: " - + e.getMessage()); - if (onTerminated != null) { - onTerminated.accept(this, e); - } - } - }); - } - - @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") - private void delayRestart(Instant started) { - var runningFor = Duration - .between(started, Instant.now()).toMillis(); - if (runningFor < 5000) { - logger.log(Level.FINE, () -> "Waiting... "); - try { - Thread.sleep(5000 - runningFor); - } catch (InterruptedException e1) { // NOPMD - // Retry - } - logger.log(Level.FINE, () -> "Retrying"); - } - } - - /** - * Sets the handler. - * - * @param handler the handler - * @return the observer - */ - public K8sObserver - handler(BiConsumer> handler) { - this.handler = handler; - return this; - } - - /** - * Sets a function to invoke if the observer terminates. First argument - * is this observer, the second is the throwable that caused the - * abnormal termination or `null` if the observer was terminated - * by {@link #stop()}. - * - * @param onTerminated the on terminated - * @return the observer - */ - public K8sObserver onTerminated( - BiConsumer, Throwable> onTerminated) { - this.onTerminated = onTerminated; - return this; - } - - /** - * Start the observer. - * - * @return the observer - */ - public K8sObserver start() { - if (handler == null) { - throw new IllegalStateException("No handler defined"); - } - thread.start(); - return this; - } - - /** - * Stops the observer. - * - * @return the observer - */ - public K8sObserver stop() { - thread.interrupt(); - return this; - } - - /** - * Returns the client. - * - * @return the client - */ - public K8sClient client() { - return client; - } - - /** - * Returns the context. - * - * @return the context - */ - public APIResource context() { - return context; - } - - /** - * Returns the observed namespace. - * - * @return the namespace - */ - public String getNamespace() { - return namespace; - } - - /** - * Returns the options for object selection. - * - * @return the list options - */ - public ListOptions options() { - return options; - } - - @Override - public String toString() { - return "Observer for " + K8s.toString(context) + " " + namespace; - } - -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java deleted file mode 100644 index 07c59b2..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.openapi.models.V1ConfigMap; -import io.kubernetes.client.openapi.models.V1ConfigMapList; -import java.util.List; - -/** - * A stub for config maps (v1). - */ -public class K8sV1ConfigMapStub - extends K8sGenericStub { - - public static final APIResource CONTEXT = new APIResource("", List.of("v1"), - "v1", "ConfigMap", true, "configmaps", "configmap"); - - /** - * Instantiates a new stub. - * - * @param client the client - * @param namespace the namespace - * @param name the name - */ - protected K8sV1ConfigMapStub(K8sClient client, String namespace, - String name) { - super(V1ConfigMap.class, V1ConfigMapList.class, client, - CONTEXT, namespace, name); - } - - /** - * Gets the stub for the given namespace and name. - * - * @param client the client - * @param namespace the namespace - * @param name the name - * @return the config map stub - */ - public static K8sV1ConfigMapStub get(K8sClient client, String namespace, - String name) { - return new K8sV1ConfigMapStub(client, namespace, name); - } -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java deleted file mode 100644 index 9075a84..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.custom.V1Patch; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1DeploymentList; -import java.util.List; -import java.util.Optional; - -/** - * A stub for pods (v1). - */ -public class K8sV1DeploymentStub - extends K8sGenericStub { - - /** The deployment's context. */ - public static final APIResource CONTEXT = new APIResource("apps", - List.of("v1"), "v1", "Pod", true, "deployments", "deployment"); - - /** - * Instantiates a new stub. - * - * @param client the client - * @param namespace the namespace - * @param name the name - */ - protected K8sV1DeploymentStub(K8sClient client, String namespace, - String name) { - super(V1Deployment.class, V1DeploymentList.class, client, - CONTEXT, namespace, name); - } - - /** - * Scales the deployment. - * - * @param replicas the replicas - * @return the new model or empty if not successful - * @throws ApiException the API exception - */ - public Optional scale(int replicas) throws ApiException { - return patch(V1Patch.PATCH_FORMAT_JSON_PATCH, - new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/replicas" - + "\", \"value\": " + replicas + "}]"), - client.defaultPatchOptions()); - } - - /** - * Gets the stub for the given namespace and name. - * - * @param client the client - * @param namespace the namespace - * @param name the name - * @return the deployment stub - */ - public static K8sV1DeploymentStub get(K8sClient client, String namespace, - String name) { - return new K8sV1DeploymentStub(client, namespace, name); - } -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java deleted file mode 100644 index ea1237d..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1Node; -import io.kubernetes.client.openapi.models.V1NodeList; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.util.Collection; -import java.util.List; - -/** - * A stub for nodes (v1). - */ -public class K8sV1NodeStub extends K8sClusterGenericStub { - - public static final APIResource CONTEXT = new APIResource("", List.of("v1"), - "v1", "Node", true, "nodes", "node"); - - /** - * Instantiates a new stub. - * - * @param client the client - * @param name the name - */ - protected K8sV1NodeStub(K8sClient client, String name) { - super(V1Node.class, V1NodeList.class, client, CONTEXT, name); - } - - /** - * Gets the stub for the given name. - * - * @param client the client - * @param name the name - * @return the config map stub - */ - public static K8sV1NodeStub get(K8sClient client, String name) { - return new K8sV1NodeStub(client, name); - } - - /** - * Get the stubs for the objects that match - * the criteria from the given options. - * - * @param client the client - * @param options the options - * @return the collection - * @throws ApiException the api exception - */ - public static Collection list(K8sClient client, - ListOptions options) throws ApiException { - return K8sClusterGenericStub.list(V1Node.class, V1NodeList.class, - client, CONTEXT, options, K8sV1NodeStub::getGeneric); - } - - /** - * Provide {@link GenericSupplier}. - */ - @SuppressWarnings({ "PMD.UnusedFormalParameter" }) - private static K8sV1NodeStub getGeneric(Class objectClass, - Class objectListClass, K8sClient client, - APIResource context, String name) { - return new K8sV1NodeStub(client, name); - } - -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java deleted file mode 100644 index f21bb47..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1Pod; -import io.kubernetes.client.openapi.models.V1PodList; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.util.Collection; -import java.util.List; - -/** - * A stub for pods (v1). - */ -public class K8sV1PodStub extends K8sGenericStub { - - /** The pods' context. */ - public static final APIResource CONTEXT - = new APIResource("", List.of("v1"), "v1", "Pod", true, "pods", "pod"); - - /** - * Instantiates a new stub. - * - * @param client the client - * @param namespace the namespace - * @param name the name - */ - protected K8sV1PodStub(K8sClient client, String namespace, String name) { - super(V1Pod.class, V1PodList.class, client, CONTEXT, namespace, name); - } - - /** - * Gets the stub for the given namespace and name. - * - * @param client the client - * @param namespace the namespace - * @param name the name - * @return the kpod stub - */ - public static K8sV1PodStub get(K8sClient client, String namespace, - String name) { - return new K8sV1PodStub(client, namespace, name); - } - - /** - * Get the stubs for the objects in the given namespace that match - * the criteria from the given options. - * - * @param client the client - * @param namespace the namespace - * @param options the options - * @return the collection - * @throws ApiException the api exception - */ - public static Collection list(K8sClient client, - String namespace, ListOptions options) throws ApiException { - return K8sGenericStub.list(V1Pod.class, V1PodList.class, client, - CONTEXT, namespace, options, (clnt, nscp, - name) -> new K8sV1PodStub(clnt, nscp, name)); - } -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PvcStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PvcStub.java deleted file mode 100644 index c46a60f..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PvcStub.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; -import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimList; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.util.Collection; -import java.util.List; - -/** - * A stub for pods (v1). - */ -public class K8sV1PvcStub extends - K8sGenericStub { - - /** The pods' context. */ - public static final APIResource CONTEXT - = new APIResource("", List.of("v1"), "v1", "PersistentVolumeClaim", - true, "persistentvolumeclaims", "persistentvolumeclaim"); - - /** - * Instantiates a new stub. - * - * @param client the client - * @param namespace the namespace - * @param name the name - */ - protected K8sV1PvcStub(K8sClient client, String namespace, String name) { - super(V1PersistentVolumeClaim.class, V1PersistentVolumeClaimList.class, - client, CONTEXT, namespace, name); - } - - /** - * Gets the stub for the given namespace and name. - * - * @param client the client - * @param namespace the namespace - * @param name the name - * @return the kpod stub - */ - public static K8sV1PvcStub get(K8sClient client, String namespace, - String name) { - return new K8sV1PvcStub(client, namespace, name); - } - - /** - * Get the stubs for the objects in the given namespace that match - * the criteria from the given options. - * - * @param client the client - * @param namespace the namespace - * @param options the options - * @return the collection - * @throws ApiException the api exception - */ - public static Collection list(K8sClient client, - String namespace, ListOptions options) throws ApiException { - return K8sGenericStub.list(V1PersistentVolumeClaim.class, - V1PersistentVolumeClaimList.class, client, CONTEXT, namespace, - options, (clnt, nscp, name) -> new K8sV1PvcStub(clnt, nscp, name)); - } -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java deleted file mode 100644 index 9c1c086..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1Secret; -import io.kubernetes.client.openapi.models.V1SecretList; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.util.Collection; -import java.util.List; - -/** - * A stub for secrets (v1). - */ -public class K8sV1SecretStub extends K8sGenericStub { - - public static final APIResource CONTEXT = new APIResource("", List.of("v1"), - "v1", "Secret", true, "secrets", "secret"); - - /** - * Instantiates a new stub. - * - * @param client the client - * @param namespace the namespace - * @param name the name - */ - protected K8sV1SecretStub(K8sClient client, String namespace, - String name) { - super(V1Secret.class, V1SecretList.class, client, - CONTEXT, namespace, name); - } - - /** - * Gets the stub for the given namespace and name. - * - * @param client the client - * @param namespace the namespace - * @param name the name - * @return the config map stub - */ - public static K8sV1SecretStub get(K8sClient client, String namespace, - String name) { - return new K8sV1SecretStub(client, namespace, name); - } - - /** - * Creates an object stub from a model. - * - * @param client the client - * @param model the model - * @return the k 8 s dynamic stub - * @throws ApiException the api exception - */ - public static K8sV1SecretStub create(K8sClient client, V1Secret model) - throws ApiException { - return K8sGenericStub.create(V1Secret.class, - V1SecretList.class, client, CONTEXT, model, K8sV1SecretStub::new); - } - - /** - * Get the stubs for the objects in the given namespace that match - * the criteria from the given options. - * - * @param client the client - * @param namespace the namespace - * @param options the options - * @return the collection - * @throws ApiException the api exception - */ - public static Collection list(K8sClient client, - String namespace, ListOptions options) throws ApiException { - return K8sGenericStub.list(V1Secret.class, V1SecretList.class, client, - CONTEXT, namespace, options, K8sV1SecretStub::new); - } -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java deleted file mode 100644 index 863f86f..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1Service; -import io.kubernetes.client.openapi.models.V1ServiceList; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.util.Collection; -import java.util.List; - -/** - * A stub for secrets (v1). - */ -public class K8sV1ServiceStub extends K8sGenericStub { - - public static final APIResource CONTEXT = new APIResource("", List.of("v1"), - "v1", "Service", true, "services", "service"); - - /** - * Instantiates a new stub. - * - * @param client the client - * @param namespace the namespace - * @param name the name - */ - protected K8sV1ServiceStub(K8sClient client, String namespace, - String name) { - super(V1Service.class, V1ServiceList.class, client, CONTEXT, namespace, - name); - } - - /** - * Gets the stub for the given namespace and name. - * - * @param client the client - * @param namespace the namespace - * @param name the name - * @return the config map stub - */ - public static K8sV1ServiceStub get(K8sClient client, String namespace, - String name) { - return new K8sV1ServiceStub(client, namespace, name); - } - - /** - * Get the stubs for the objects in the given namespace that match - * the criteria from the given options. - * - * @param client the client - * @param namespace the namespace - * @param options the options - * @return the collection - * @throws ApiException the api exception - */ - public static Collection list(K8sClient client, - String namespace, ListOptions options) throws ApiException { - return K8sGenericStub.list(V1Service.class, V1ServiceList.class, client, - CONTEXT, namespace, options, - (clnt, nscp, name) -> new K8sV1ServiceStub(clnt, nscp, name)); - } -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java deleted file mode 100644 index be30b00..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.openapi.models.V1StatefulSet; -import io.kubernetes.client.openapi.models.V1StatefulSetList; -import java.util.List; - -/** - * A stub for stateful sets (v1). - */ -public class K8sV1StatefulSetStub - extends K8sGenericStub { - - /** The stateful sets' context */ - public static final APIResource CONTEXT - = new APIResource("apps", List.of("v1"), "v1", "StatefulSet", true, - "statefulsets", "statefulset"); - - /** - * Instantiates a new stub. - * - * @param client the client - * @param namespace the namespace - * @param name the name - */ - protected K8sV1StatefulSetStub(K8sClient client, String namespace, - String name) { - super(V1StatefulSet.class, V1StatefulSetList.class, client, CONTEXT, - namespace, name); - } - - /** - * Gets the stub for the given namespace and name. - * - * @param client the client - * @param namespace the namespace - * @param name the name - * @return the stateful set stub - */ - public static K8sV1StatefulSetStub get(K8sClient client, String namespace, - String name) { - return new K8sV1StatefulSetStub(client, namespace, name); - } -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java deleted file mode 100644 index a0b66bf..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java +++ /dev/null @@ -1,499 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import io.kubernetes.client.openapi.JSON; -import io.kubernetes.client.openapi.models.V1Condition; -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.Constants.Status.Condition; -import org.jdrupes.vmoperator.common.Constants.Status.Condition.Reason; -import org.jdrupes.vmoperator.util.DataPath; - -/** - * Represents a VM definition. - */ -@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" }) -public class VmDefinition extends K8sDynamicModel { - - @SuppressWarnings({ "unused" }) - private static final Logger logger - = Logger.getLogger(VmDefinition.class.getName()); - @SuppressWarnings("PMD.FieldNamingConventions") - private static final Gson gson = new JSON().getGson(); - @SuppressWarnings("PMD.FieldNamingConventions") - private static final ObjectMapper objectMapper - = new ObjectMapper().registerModule(new JavaTimeModule()); - - private final Model model; - private VmExtraData extraData; - - /** - * The VM state from the VM definition. - */ - public enum RequestedVmState { - STOPPED, RUNNING - } - - /** - * Permissions for accessing and manipulating the VM. - */ - public enum Permission { - START("start"), STOP("stop"), RESET("reset"), - ACCESS_CONSOLE("accessConsole"), TAKE_CONSOLE("takeConsole"); - - @SuppressWarnings("PMD.UseConcurrentHashMap") - private static Map reprs = new HashMap<>(); - - static { - for (var value : EnumSet.allOf(Permission.class)) { - reprs.put(value.repr, value); - } - } - - private final String repr; - - Permission(String repr) { - this.repr = repr; - } - - /** - * Create permission from representation in CRD. - * - * @param value the value - * @return the permission - */ - @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") - public static Set parse(String value) { - if ("*".equals(value)) { - return EnumSet.allOf(Permission.class); - } - return Set.of(reprs.get(value)); - } - - /** - * To string. - * - * @return the string - */ - @Override - public String toString() { - return repr; - } - } - - /** - * Permissions granted to a user or role. - * - * @param user the user - * @param role the role - * @param may the may - */ - public record Grant(String user, String role, Set may) { - - /** - * To string. - * - * @return the string - */ - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - if (user != null) { - builder.append("User ").append(user); - } else { - builder.append("Role ").append(role); - } - builder.append(" may=").append(may).append(']'); - return builder.toString(); - } - } - - /** - * The assignment information. - * - * @param pool the pool - * @param user the user - * @param lastUsed the last used - */ - public record Assignment(String pool, String user, Instant lastUsed) { - } - - /** - * Instantiates a new vm definition. - * - * @param delegate the delegate - * @param json the json - */ - public VmDefinition(Gson delegate, JsonObject json) { - super(delegate, json); - model = gson.fromJson(json, Model.class); - } - - /** - * Gets the spec. - * - * @return the spec - */ - public Map spec() { - return model.getSpec(); - } - - /** - * Get a value from the spec using {@link DataPath#get}. - * - * @param the generic type - * @param selectors the selectors - * @return the value, if found - */ - public Optional fromSpec(Object... selectors) { - return DataPath.get(spec(), selectors); - } - - /** - * The pools that this VM belongs to. - * - * @return the list - */ - public List pools() { - return this.> fromSpec("pools") - .orElse(Collections.emptyList()); - } - - /** - * Get a value from the `spec().get("vm")` using {@link DataPath#get}. - * - * @param the generic type - * @param selectors the selectors - * @return the value, if found - */ - public Optional fromVm(Object... selectors) { - return DataPath.get(spec(), "vm") - .flatMap(vm -> DataPath.get(vm, selectors)); - } - - /** - * Gets the status. - * - * @return the status - */ - public Map status() { - return model.getStatus(); - } - - /** - * Get a value from the status using {@link DataPath#get}. - * - * @param the generic type - * @param selectors the selectors - * @return the value, if found - */ - public Optional fromStatus(Object... selectors) { - return DataPath.get(status(), selectors); - } - - /** - * The assignment information. - * - * @return the optional - */ - public Optional assignment() { - return this.> fromStatus(Status.ASSIGNMENT) - .filter(m -> !m.isEmpty()).map(a -> new Assignment( - a.get("pool").toString(), a.get("user").toString(), - Instant.parse(a.get("lastUsed").toString()))); - } - - /** - * Return a condition from the status. - * - * @param name the condition's name - * @return the status, if the condition is defined - */ - public Optional condition(String name) { - return this.>> fromStatus("conditions") - .orElse(Collections.emptyList()).stream() - .filter(cond -> DataPath.get(cond, "type") - .map(name::equals).orElse(false)) - .findFirst() - .map(cond -> objectMapper.convertValue(cond, V1Condition.class)); - } - - /** - * Return a condition's status. - * - * @param name the condition's name - * @return the status, if the condition is defined - */ - public Optional conditionStatus(String name) { - return this.>> fromStatus("conditions") - .orElse(Collections.emptyList()).stream() - .filter(cond -> DataPath.get(cond, "type") - .map(name::equals).orElse(false)) - .findFirst().map(cond -> DataPath.get(cond, "status") - .map("True"::equals).orElse(false)); - } - - /** - * Return true if the console is in use. - * - * @return true, if successful - */ - public boolean consoleConnected() { - return conditionStatus("ConsoleConnected").orElse(false); - } - - /** - * Return the last known console user. - * - * @return the optional - */ - public Optional consoleUser() { - return this. fromStatus(Status.CONSOLE_USER); - } - - /** - * Set extra data (unknown to kubernetes). - * @return the VM definition - */ - /* default */ VmDefinition extra(VmExtraData extraData) { - this.extraData = extraData; - return this; - } - - /** - * Return the extra data. - * - * @return the data - */ - public VmExtraData extra() { - return extraData; - } - - /** - * Returns the definition's name. - * - * @return the string - */ - public String name() { - return metadata().getName(); - } - - /** - * Returns the definition's namespace. - * - * @return the string - */ - public String namespace() { - return metadata().getNamespace(); - } - - /** - * Return the requested VM state. - * - * @return the string - */ - public RequestedVmState vmState() { - return fromVm("state") - .map(s -> "Running".equals(s) ? RequestedVmState.RUNNING - : RequestedVmState.STOPPED) - .orElse(RequestedVmState.STOPPED); - } - - /** - * Collect all permissions for the given user with the given roles. - * If permission "takeConsole" is granted, the result will also - * contain "accessConsole" to simplify checks. - * - * @param user the user - * @param roles the roles - * @return the sets the - */ - public Set permissionsFor(String user, - Collection roles) { - var result = this.>> fromSpec("permissions") - .orElse(Collections.emptyList()).stream() - .filter(p -> DataPath.get(p, "user").map(u -> u.equals(user)) - .orElse(false) - || DataPath.get(p, "role").map(roles::contains).orElse(false)) - .map(p -> DataPath.> get(p, "may") - .orElse(Collections.emptyList()).stream()) - .flatMap(Function.identity()) - .map(Permission::parse).map(Set::stream) - .flatMap(Function.identity()) - .collect(Collectors.toCollection(HashSet::new)); - - // Take console implies access console, simplify checks - if (result.contains(Permission.TAKE_CONSOLE)) { - result.add(Permission.ACCESS_CONSOLE); - } - return result; - } - - /** - * Check if the console is accessible. Always returns `true` if - * the VM is running and the permissions allow taking over the - * console. Else, returns `true` if - * - * * the permissions allow access to the console and - * - * * the VM is running and - * - * * the console is currently unused or used by the given user and - * - * * if user login is requested, the given user is logged in. - * - * @param user the user - * @param permissions the permissions - * @return true, if successful - */ - @SuppressWarnings("PMD.SimplifyBooleanReturns") - public boolean consoleAccessible(String user, Set permissions) { - // Basic checks - if (!conditionStatus(Condition.RUNNING).orElse(false)) { - return false; - } - if (permissions.contains(Permission.TAKE_CONSOLE)) { - return true; - } - if (!permissions.contains(Permission.ACCESS_CONSOLE)) { - return false; - } - - // If the console is in use by another user, deny access - if (conditionStatus(Condition.CONSOLE_CONNECTED).orElse(false) - && !consoleUser().map(cu -> cu.equals(user)).orElse(false)) { - return false; - } - - // If no login is requested, allow access, else check if user matches - if (condition(Condition.USER_LOGGED_IN).map(V1Condition::getReason) - .map(r -> Reason.NOT_REQUESTED.equals(r)).orElse(false)) { - return true; - } - return user.equals(status().get(Status.LOGGED_IN_USER)); - } - - /** - * Get the display password serial. - * - * @return the optional - */ - public Optional displayPasswordSerial() { - return this. fromStatus(Status.DISPLAY_PASSWORD_SERIAL) - .map(Number::longValue); - } - - /** - * Hash code. - * - * @return the int - */ - @Override - public int hashCode() { - return Objects.hash(metadata().getNamespace(), metadata().getName()); - } - - /** - * Equals. - * - * @param obj the obj - * @return true, if successful - */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - VmDefinition other = (VmDefinition) obj; - return Objects.equals(metadata().getNamespace(), - other.metadata().getNamespace()) - && Objects.equals(metadata().getName(), other.metadata().getName()); - } - - /** - * The Class Model. - */ - public static class Model { - - private Map spec; - private Map status; - - /** - * Gets the spec. - * - * @return the spec - */ - public Map getSpec() { - return spec; - } - - /** - * Sets the spec. - * - * @param spec the spec to set - */ - public void setSpec(Map spec) { - this.spec = spec; - } - - /** - * Gets the status. - * - * @return the status - */ - public Map getStatus() { - return status; - } - - /** - * Sets the status. - * - * @param status the status to set - */ - public void setStatus(Map status) { - this.status = status; - } - - } - -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java deleted file mode 100644 index 377220a..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.io.Reader; -import java.util.Collection; - -/** - * A stub for namespaced custom objects. It uses a dynamic model - * (see {@link K8sDynamicModel}) for representing the object's - * state and can therefore be used for any kind of object, especially - * custom objects. - */ -public class VmDefinitionStub - extends K8sDynamicStubBase { - - private static DynamicTypeAdapterFactory taf = new VmDefintionModelTypeAdapterFactory(); - - /** - * Instantiates a new stub for VM defintions. - * - * @param client the client - * @param context the context - * @param namespace the namespace - * @param name the name - */ - public VmDefinitionStub(K8sClient client, APIResource context, - String namespace, String name) { - super(VmDefinition.class, VmDefinitions.class, taf, client, - context, namespace, name); - } - - /** - * Get a dynamic object stub. If the version in parameter - * `gvk` is an empty string, the stub refers to the first object with - * matching group and kind. - * - * @param client the client - * @param gvk the group, version and kind - * @param namespace the namespace - * @param name the name - * @return the stub if the object exists - * @throws ApiException the api exception - */ - public static VmDefinitionStub get(K8sClient client, - GroupVersionKind gvk, String namespace, String name) - throws ApiException { - return new VmDefinitionStub(client, apiResource(client, gvk), namespace, - name); - } - - /** - * Get a dynamic object stub. - * - * @param client the client - * @param context the context - * @param namespace the namespace - * @param name the name - * @return the stub if the object exists - * @throws ApiException the api exception - */ - public static VmDefinitionStub get(K8sClient client, - APIResource context, String namespace, String name) { - return new VmDefinitionStub(client, context, namespace, name); - } - - /** - * Creates a stub from yaml. - * - * @param client the client - * @param context the context - * @param yaml the yaml - * @return the k 8 s dynamic stub - * @throws ApiException the api exception - */ - public static VmDefinitionStub createFromYaml(K8sClient client, - APIResource context, Reader yaml) throws ApiException { - var model = new VmDefinition(client.getJSON().getGson(), - K8s.yamlToJson(client, yaml)); - return K8sGenericStub.create(VmDefinition.class, - VmDefinitions.class, client, context, model, - (c, ns, n) -> new VmDefinitionStub(c, context, ns, n)); - } - - /** - * Get the stubs for the objects in the given namespace that match - * the criteria from the given options. - * - * @param client the client - * @param namespace the namespace - * @param options the options - * @return the collection - * @throws ApiException the api exception - */ - public static Collection list(K8sClient client, - APIResource context, String namespace, ListOptions options) - throws ApiException { - return K8sGenericStub.list(VmDefinition.class, - VmDefinitions.class, client, context, namespace, options, - (c, ns, n) -> new VmDefinitionStub(c, context, ns, n)); - } - - /** - * Get the stubs for the objects in the given namespace. - * - * @param client the client - * @param namespace the namespace - * @return the collection - * @throws ApiException the api exception - */ - public static Collection list(K8sClient client, - APIResource context, String namespace) - throws ApiException { - return list(client, context, namespace, new ListOptions()); - } - - /** - * A factory for creating VmDefinitionModel(s) objects. - */ - public static class VmDefintionModelTypeAdapterFactory extends - DynamicTypeAdapterFactory { - - /** - * Instantiates a new dynamic model type adapter factory. - */ - public VmDefintionModelTypeAdapterFactory() { - super(VmDefinition.class, VmDefinitions.class); - } - } - -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java deleted file mode 100644 index c79654e..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; - -/** - * Represents a list of {@link VmDefinition}s. - */ -public class VmDefinitions - extends K8sDynamicModelsBase { - - /** - * Initialize the object list using the given JSON data. - * - * @param delegate the gson instance to use for extracting structured data - * @param data the data - */ - public VmDefinitions(Gson delegate, JsonObject data) { - super(VmDefinition.class, delegate, data); - } -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmExtraData.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmExtraData.java deleted file mode 100644 index e1565c5..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmExtraData.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import io.kubernetes.client.util.Strings; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Represents internally used dynamic data associated with a - * {@link VmDefinition}. - */ -public class VmExtraData { - - private static final Logger logger - = Logger.getLogger(VmExtraData.class.getName()); - - private final VmDefinition vmDef; - private String nodeName = ""; - private List nodeAddresses = Collections.emptyList(); - private long resetCount; - - /** - * Initializes a new instance. - * - * @param vmDef the VM definition - */ - public VmExtraData(VmDefinition vmDef) { - this.vmDef = vmDef; - vmDef.extra(this); - } - - /** - * Sets the node info. - * - * @param name the name - * @param addresses the addresses - * @return the VM extra data - */ - public VmExtraData nodeInfo(String name, List addresses) { - nodeName = name; - nodeAddresses = addresses; - return this; - } - - /** - * Return the node name. - * - * @return the string - */ - public String nodeName() { - return nodeName; - } - - /** - * Gets the node addresses. - * - * @return the nodeAddresses - */ - public List nodeAddresses() { - return nodeAddresses; - } - - /** - * Sets the reset count. - * - * @param resetCount the reset count - * @return the vm extra data - */ - public VmExtraData resetCount(long resetCount) { - this.resetCount = resetCount; - return this; - } - - /** - * Returns the reset count. - * - * @return the long - */ - public long resetCount() { - return resetCount; - } - - /** - * Create a connection file. - * - * @param password the password - * @param preferredIpVersion the preferred IP version - * @param deleteConnectionFile the delete connection file - * @return the string - */ - public Optional connectionFile(String password, - Class preferredIpVersion, boolean deleteConnectionFile) { - var addr = displayIp(preferredIpVersion); - if (addr.isEmpty()) { - logger - .severe(() -> "Failed to find display IP for " + vmDef.name()); - return Optional.empty(); - } - var port = vmDef. fromVm("display", "spice", "port") - .map(Number::longValue); - if (port.isEmpty()) { - logger - .severe(() -> "No port defined for display of " + vmDef.name()); - return Optional.empty(); - } - StringBuffer data = new StringBuffer(100) - .append("[virt-viewer]\ntype=spice\nhost=") - .append(addr.get().getHostAddress()).append("\nport=") - .append(port.get().toString()) - .append('\n'); - if (password != null) { - data.append("password=").append(password).append('\n'); - } - vmDef. fromVm("display", "spice", "proxyUrl") - .ifPresent(u -> { - if (!Strings.isNullOrEmpty(u)) { - data.append("proxy=").append(u).append('\n'); - } - }); - if (deleteConnectionFile) { - data.append("delete-this-file=1\n"); - } - return Optional.of(data.toString()); - } - - private Optional displayIp(Class preferredIpVersion) { - Optional server = vmDef.fromVm("display", "spice", "server"); - if (server.isPresent()) { - var srv = server.get(); - try { - var addr = InetAddress.getByName(srv); - logger.fine(() -> "Using IP address from CRD for " - + vmDef.metadata().getName() + ": " + addr); - return Optional.of(addr); - } catch (UnknownHostException e) { - logger.log(Level.SEVERE, e, () -> "Invalid server address " - + srv + ": " + e.getMessage()); - return Optional.empty(); - } - } - var addrs = nodeAddresses.stream().map(a -> { - try { - return InetAddress.getByName(a); - } catch (UnknownHostException e) { - logger.warning(() -> "Invalid IP address: " + a); - return null; - } - }).filter(Objects::nonNull).toList(); - logger.fine( - () -> "Known IP addresses for " + vmDef.name() + ": " + addrs); - return addrs.stream() - .filter(a -> preferredIpVersion.isAssignableFrom(a.getClass())) - .findFirst().or(() -> addrs.stream().findFirst()); - } - -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java deleted file mode 100644 index f7aaa67..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import java.time.Duration; -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.jdrupes.vmoperator.common.VmDefinition.Assignment; -import org.jdrupes.vmoperator.common.VmDefinition.Grant; -import org.jdrupes.vmoperator.common.VmDefinition.Permission; -import org.jdrupes.vmoperator.util.DataPath; - -/** - * Represents a VM pool. - */ -public class VmPool { - - private final String name; - private String retention; - private boolean loginOnAssignment; - private boolean defined; - private List permissions = Collections.emptyList(); - private final Set vms - = Collections.synchronizedSet(new HashSet<>()); - - /** - * Instantiates a new vm pool. - * - * @param name the name - */ - public VmPool(String name) { - this.name = name; - } - - /** - * Fill the properties of a provisionally created pool from - * the definition. - * - * @param definition the definition - */ - public void defineFrom(VmPool definition) { - retention = definition.retention(); - permissions = definition.permissions(); - loginOnAssignment = definition.loginOnAssignment(); - defined = true; - } - - /** - * Returns the name. - * - * @return the name - */ - public String name() { - return name; - } - - /** - * Checks if is login on assignment. - * - * @return the loginOnAssignment - */ - public boolean loginOnAssignment() { - return loginOnAssignment; - } - - /** - * Checks if is defined. - * - * @return the result - */ - public boolean isDefined() { - return defined; - } - - /** - * Marks the pool as undefined. - */ - public void setUndefined() { - defined = false; - } - - /** - * Gets the retention. - * - * @return the retention - */ - public String retention() { - return retention; - } - - /** - * Permissions granted for a VM from the pool. - * - * @return the permissions - */ - public List permissions() { - return permissions; - } - - /** - * Returns the VM names. - * - * @return the vms - */ - public Set vms() { - return vms; - } - - /** - * Collect all permissions for the given user with the given roles. - * - * @param user the user - * @param roles the roles - * @return the sets the - */ - public Set permissionsFor(String user, - Collection roles) { - return permissions.stream() - .filter(g -> DataPath.get(g, "user").map(u -> u.equals(user)) - .orElse(false) - || DataPath.get(g, "role").map(roles::contains).orElse(false)) - .map(g -> DataPath.> get(g, "may") - .orElse(Collections.emptySet()).stream()) - .flatMap(Function.identity()).collect(Collectors.toSet()); - } - - /** - * Checks if the given VM belongs to the pool and is not in use. - * - * @param vmDef the vm def - * @return true, if is assignable - */ - @SuppressWarnings("PMD.SimplifyBooleanReturns") - public boolean isAssignable(VmDefinition vmDef) { - // Check if the VM is in the pool - if (!vmDef.pools().contains(name)) { - return false; - } - - // Check if the VM is not in use - if (vmDef.consoleConnected()) { - return false; - } - - // If not assigned, it's usable - if (vmDef.assignment().isEmpty()) { - return true; - } - - // Check if it is to be retained - if (vmDef.assignment().map(Assignment::lastUsed).map(this::retainUntil) - .map(ru -> Instant.now().isBefore(ru)).orElse(false)) { - return false; - } - - // Additional check in case lastUsed has not been updated - // by PoolMonitor#onVmResourceChanged() yet ("race condition") - if (vmDef.condition("ConsoleConnected") - .map(cc -> cc.getLastTransitionTime().toInstant()) - .map(this::retainUntil) - .map(ru -> Instant.now().isBefore(ru)).orElse(false)) { - return false; - } - return true; - } - - /** - * Return the instant until which an assignment should be retained. - * - * @param lastUsed the last used - * @return the instant - */ - public Instant retainUntil(Instant lastUsed) { - if (retention.startsWith("P")) { - return lastUsed.plus(Duration.parse(retention)); - } - return Instant.parse(retention); - } - - /** - * To string. - * - * @return the string - */ - @Override - @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", - "PMD.AvoidSynchronizedStatement" }) - public String toString() { - StringBuilder builder = new StringBuilder(50); - builder.append("VmPool [name=").append(name).append(", permissions=") - .append(permissions).append(", vms="); - if (vms.size() <= 3) { - builder.append(vms); - } else { - synchronized (vms) { - builder.append('[').append(vms.stream().limit(3) - .map(s -> s + ",").collect(Collectors.joining())) - .append("...]"); - } - } - builder.append(']'); - return builder.toString(); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/.eclipse-pmd b/org.jdrupes.vmoperator.manager.events/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/org.jdrupes.vmoperator.manager.events/.eclipse-pmd +++ b/org.jdrupes.vmoperator.manager.events/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.manager.events/build.gradle b/org.jdrupes.vmoperator.manager.events/build.gradle index bb4b8d8..56c364f 100644 --- a/org.jdrupes.vmoperator.manager.events/build.gradle +++ b/org.jdrupes.vmoperator.manager.events/build.gradle @@ -9,5 +9,6 @@ plugins { } dependencies { + api 'org.jgrapes:org.jgrapes.core:[1.19.0,2)' api project(':org.jdrupes.vmoperator.common') } diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/AssignVm.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/AssignVm.java deleted file mode 100644 index 7252c6a..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/AssignVm.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import org.jdrupes.vmoperator.manager.events.GetVms.VmData; -import org.jgrapes.core.Event; - -/** - * Assign a VM from a pool to a user. - */ -public class AssignVm extends Event { - - private final String fromPool; - private final String toUser; - - /** - * Instantiates a new event. - * - * @param fromPool the from pool - * @param toUser the to user - */ - public AssignVm(String fromPool, String toUser) { - this.fromPool = fromPool; - this.toUser = toUser; - } - - /** - * Gets the pool to assign from. - * - * @return the pool - */ - public String fromPool() { - return fromPool; - } - - /** - * Gets the user to assign to. - * - * @return the to user - */ - public String toUser() { - return toUser; - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelDictionary.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelDictionary.java deleted file mode 100644 index 2b23532..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelDictionary.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import java.util.Collection; -import java.util.Optional; -import java.util.Set; -import org.jgrapes.core.Channel; - -/** - * Supports the lookup of a channel by a name (an id). As a convenience, - * it is possible to additionally associate arbitrary data with the entry - * (and thus with the channel). Note that this interface defines a - * read-only view of the dictionary. - * - * @param the key type - * @param the channel type - * @param the type of the associated data - */ -public interface ChannelDictionary { - - /** - * Combines the channel and the associated data. - * - * @param the channel type - * @param the type of the associated data - * @param channel the channel - * @param associated the associated - */ - public record Value(C channel, A associated) { - } - - /** - * Returns all known keys. - * - * @return the keys - */ - Set keys(); - - /** - * Return all known values. - * - * @return the collection - */ - Collection> values(); - - /** - * Returns the channel and associates data registered for the key - * or an empty optional if no entry exists. - * - * @param key the key - * @return the result - */ - Optional> value(K key); - - /** - * Return all known channels. - * - * @return the collection - */ - default Collection channels() { - return values().stream().map(v -> v.channel).toList(); - } - - /** - * Returns the channel registered for the key or an empty optional - * if no mapping exists. - * - * @param key the key - * @return the optional - */ - default Optional channel(K key) { - return value(key).map(b -> b.channel); - } - - /** - * Returns all known associated data. - * - * @return the collection - */ - default Collection associated() { - return values().stream() - .filter(v -> v.associated() != null) - .map(v -> v.associated).toList(); - } - - /** - * Return the data associated with the entry for the channel. - * - * @param key the key - * @return the data - */ - default Optional associated(K key) { - return value(key).map(b -> b.associated); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelManager.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelManager.java deleted file mode 100644 index da36123..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelManager.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import java.util.Collection; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import org.jgrapes.core.Channel; - -/** - * Provides an actively managed implementation of the {@link ChannelDictionary}. - * - * The {@link ChannelManager} can be used for housekeeping by any component - * that creates channels. It can be shared between this component and - * some other component, preferably passing it as {@link ChannelDictionary} - * (the read-only view) to the second component. Alternatively, the other - * component can use a {@link ChannelTracker} to track the mappings using - * events. - * - * @param the key type - * @param the channel type - * @param the type of the associated data - */ -public class ChannelManager - implements ChannelDictionary { - - private final Map> entries = new ConcurrentHashMap<>(); - private final Function supplier; - - /** - * Instantiates a new channel manager. - * - * @param supplier the supplier that creates new channels - */ - public ChannelManager(Function supplier) { - this.supplier = supplier; - } - - /** - * Instantiates a new channel manager without a default supplier. - */ - public ChannelManager() { - this(k -> null); - } - - /** - * Return all keys. - * - * @return the keys. - */ - @Override - public Set keys() { - return entries.keySet(); - } - - /** - * Return all known values. - * - * @return the collection - */ - @Override - public Collection> values() { - return entries.values(); - } - - /** - * Returns the channel and associates data registered for the key - * or an empty optional if no mapping exists. - * - * @param key the key - * @return the result - */ - public Optional> value(K key) { - return Optional.ofNullable(entries.get(key)); - } - - /** - * Store the given data. - * - * @param key the key - * @param channel the channel - * @param associated the associated - * @return the channel manager - */ - public ChannelManager put(K key, C channel, A associated) { - entries.put(key, new Value<>(channel, associated)); - return this; - } - - /** - * Store the given data. - * - * @param key the key - * @param channel the channel - * @return the channel manager - */ - public ChannelManager put(K key, C channel) { - put(key, channel, null); - return this; - } - - /** - * Creates a new channel without adding it to the channel manager. - * After fully initializing the channel, it should be added to the - * manager using {@link #put(K, C)}. - * - * @param key the key - * @return the c - */ - public C createChannel(K key) { - return supplier.apply(key); - } - - /** - * Returns the {@link Channel} for the given name, creating it using - * the supplier passed to the constructor if it doesn't exist yet. - * - * @param key the key - * @return the channel - */ - public C channelGet(K key) { - return computeIfAbsent(key, supplier); - } - - /** - * Returns the {@link Channel} for the given name, creating it using - * the given supplier if it doesn't exist yet. - * - * @param key the key - * @param supplier the supplier - * @return the channel - */ - public C computeIfAbsent(K key, Function supplier) { - return entries.computeIfAbsent(key, - k -> new Value<>(supplier.apply(k), null)).channel(); - } - - /** - * Associate the entry for the channel with the given data. The entry - * for the channel must already exist. - * - * @param key the key - * @param data the data - * @return the channel manager - */ - public ChannelManager associate(K key, A data) { - Optional.ofNullable(entries.computeIfPresent(key, - (k, existing) -> new Value<>(existing.channel(), data))); - return this; - } - - /** - * Removes the channel with the given name. - * - * @param name the name - */ - public void remove(String name) { - entries.remove(name); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelTracker.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelTracker.java deleted file mode 100644 index 8a41908..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelTracker.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import org.jgrapes.core.Channel; - -/** - * Used to track mapping from a key to a channel. Entries must - * be maintained by handlers for "add/remove" (or "open/close") - * events delivered on the channels that are to be - * made available by the tracker. - * - * The channels are stored in the dictionary using {@link WeakReference}s. - * Removing entries is therefore best practice but not an absolute necessity - * as entries for cleared references are removed when one of the methods - * {@link #values()}, {@link #channels()} or {@link #associated()} is called. - * - * @param the key type - * @param the channel type - * @param the type of the associated data - */ -public class ChannelTracker - implements ChannelDictionary { - - private final Map> entries = new ConcurrentHashMap<>(); - - /** - * Combines the channel and associated data. - * - * @param the generic type - * @param the generic type - */ - @SuppressWarnings("PMD.ShortClassName") - private static class Data { - public final WeakReference channel; - public A associated; - - /** - * Instantiates a new value. - * - * @param channel the channel - */ - public Data(C channel) { - this.channel = new WeakReference<>(channel); - } - } - - @Override - public Set keys() { - return entries.keySet(); - } - - @Override - public Collection> values() { - var result = new ArrayList>(); - for (var itr = entries.entrySet().iterator(); itr.hasNext();) { - var value = itr.next().getValue(); - var channel = value.channel.get(); - if (channel == null) { - itr.remove(); - continue; - } - result.add(new Value<>(channel, value.associated)); - } - return result; - } - - /** - * Returns the channel and associates data registered for the key - * or an empty optional if no mapping exists. - * - * @param key the key - * @return the result - */ - public Optional> value(K key) { - var value = entries.get(key); - if (value == null) { - return Optional.empty(); - } - var channel = value.channel.get(); - if (channel == null) { - // Cleanup old reference - entries.remove(key); - return Optional.empty(); - } - return Optional.of(new Value<>(channel, value.associated)); - } - - /** - * Store the given data. - * - * @param key the key - * @param channel the channel - * @param associated the associated - * @return the channel manager - */ - public ChannelTracker put(K key, C channel, A associated) { - Data data = new Data<>(channel); - data.associated = associated; - entries.put(key, data); - return this; - } - - /** - * Store the given data. - * - * @param key the key - * @param channel the channel - * @return the channel manager - */ - public ChannelTracker put(K key, C channel) { - put(key, channel, null); - return this; - } - - /** - * Associate the entry for the channel with the given data. The entry - * for the channel must already exist. - * - * @param key the key - * @param data the data - * @return the channel manager - */ - public ChannelTracker associate(K key, A data) { - Optional.ofNullable(entries.get(key)) - .ifPresent(v -> v.associated = data); - return this; - } - - /** - * Removes the channel with the given name. - * - * @param name the name - */ - public void remove(String name) { - entries.remove(name); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplaySecret.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplaySecret.java deleted file mode 100644 index dc47b4a..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplaySecret.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jgrapes.core.Event; - -/** - * Gets the current display secret and optionally updates it. - */ -public class GetDisplaySecret extends Event { - - private final VmDefinition vmDef; - private final String user; - - /** - * Instantiates a new request for the display secret. - * After handling the event, a result of `null` means that - * no secret is needed. No result means that the console - * is not accessible. - * - * @param vmDef the vm name - * @param user the requesting user - */ - public GetDisplaySecret(VmDefinition vmDef, String user) { - this.vmDef = vmDef; - this.user = user; - } - - /** - * Gets the VM definition. - * - * @return the VM definition - */ - public VmDefinition vmDefinition() { - return vmDef; - } - - /** - * Return the id of the user who has requested the password. - * - * @return the string - */ - public String user() { - return user; - } - - /** - * Returns `true` if a password is available. May only be called - * when the event is completed. Note that the password returned - * by {@link #secret()} may be `null`, indicating that no password - * is needed. - * - * @return true, if successful - */ - public boolean secretAvailable() { - if (!isDone()) { - throw new IllegalStateException("Event is not done."); - } - return !currentResults().isEmpty(); - } - - /** - * Return the secret. May only be called when the event has been - * completed with a valid result (see {@link #secretAvailable()}). - * - * @return the password. A value of `null` means that no password - * is required. - */ - public String secret() { - if (!isDone() || currentResults().isEmpty()) { - throw new IllegalStateException("Event is not done."); - } - return currentResults().get(0); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetPools.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetPools.java deleted file mode 100644 index b563c9c..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetPools.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import org.jdrupes.vmoperator.common.VmPool; -import org.jgrapes.core.Event; - -/** - * Gets the known pools' definitions. - */ -public class GetPools extends Event> { - - private String name; - private String user; - private List roles = Collections.emptyList(); - - /** - * Return only the pool with the given name. - * - * @param name the name - * @return the returns the vms - */ - public GetPools withName(String name) { - this.name = name; - return this; - } - - /** - * Return only {@link VmPool}s that are accessible by - * the given user or roles. - * - * @param user the user - * @param roles the roles - * @return the event - */ - public GetPools accessibleFor(String user, List roles) { - this.user = user; - this.roles = roles; - return this; - } - - /** - * Returns the name filter criterion, if set. - * - * @return the optional - */ - public Optional name() { - return Optional.ofNullable(name); - } - - /** - * Returns the user filter criterion, if set. - * - * @return the optional - */ - public Optional forUser() { - return Optional.ofNullable(user); - } - - /** - * Returns the roles criterion. - * - * @return the list - */ - public List forRoles() { - return roles; - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java deleted file mode 100644 index 0e24013..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jgrapes.core.Event; - -/** - * Gets the known VMs' definitions and channels. - */ -public class GetVms extends Event> { - - private String name; - private String user; - private List roles = Collections.emptyList(); - private String fromPool; - private String toUser; - - /** - * Return only the VMs with the given name. - * - * @param name the name - * @return the returns the vms - */ - public GetVms withName(String name) { - this.name = name; - return this; - } - - /** - * Return only {@link VmDefinition}s that are accessible by - * the given user or roles. - * - * @param user the user - * @param roles the roles - * @return the event - */ - public GetVms accessibleFor(String user, List roles) { - this.user = user; - this.roles = roles; - return this; - } - - /** - * Return only {@link VmDefinition}s that are assigned from the given pool. - * - * @param pool the pool - * @return the returns the vms - */ - public GetVms assignedFrom(String pool) { - this.fromPool = pool; - return this; - } - - /** - * Return only {@link VmDefinition}s that are assigned to the given user. - * - * @param user the user - * @return the returns the vms - */ - public GetVms assignedTo(String user) { - this.toUser = user; - return this; - } - - /** - * Returns the name filter criterion, if set. - * - * @return the optional - */ - public Optional name() { - return Optional.ofNullable(name); - } - - /** - * Returns the user filter criterion, if set. - * - * @return the optional - */ - public Optional user() { - return Optional.ofNullable(user); - } - - /** - * Returns the roles criterion. - * - * @return the list - */ - public List roles() { - return roles; - } - - /** - * Returns the pool filter criterion, if set. - * - * @return the optional - */ - public Optional fromPool() { - return Optional.ofNullable(fromPool); - } - - /** - * Returns the user filter criterion, if set. - * - * @return the optional - */ - public Optional toUser() { - return Optional.ofNullable(toUser); - } - - /** - * Return tuple. - * - * @param definition the definition - * @param channel the channel - */ - public record VmData(VmDefinition definition, VmChannel channel) { - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java index 9e19255..8f735da 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java @@ -24,6 +24,7 @@ import org.jgrapes.core.Event; /** * Modifies a VM. */ +@SuppressWarnings("PMD.DataClass") public class ModifyVm extends Event { private final String name; diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PodChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PodChanged.java deleted file mode 100644 index 8bbcfe8..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PodChanged.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import io.kubernetes.client.openapi.models.V1Pod; -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Event; - -/** - * Indicates a change in a pod that runs a VM. - */ -public class PodChanged extends Event { - - private final V1Pod pod; - private final K8sObserver.ResponseType type; - - /** - * Instantiates a new VM changed event. - * - * @param pod the pod - * @param type the type - */ - public PodChanged(V1Pod pod, K8sObserver.ResponseType type) { - this.pod = pod; - this.type = type; - } - - /** - * Gets the pod. - * - * @return the pod - */ - public V1Pod pod() { - return pod; - } - - /** - * Returns the type. - * - * @return the type - */ - public K8sObserver.ResponseType type() { - return type; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(Components.objectName(this)).append(" [") - .append(pod.getMetadata().getName()).append(' ').append(type); - if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); - } - builder.append(']'); - return builder.toString(); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ResetVm.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ResetVm.java deleted file mode 100644 index 778820e..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ResetVm.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import org.jgrapes.core.Event; - -/** - * Triggers a reset of the VM. - */ -public class ResetVm extends Event { - - private final String vmName; - - /** - * Instantiates a new event. - * - * @param vmName the vm name - */ - public ResetVm(String vmName) { - this.vmName = vmName; - } - - /** - * Gets the vm name. - * - * @return the vm name - */ - public String vmName() { - return vmName; - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java deleted file mode 100644 index b4fcf56..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import org.jdrupes.vmoperator.common.VmPool; -import org.jgrapes.core.Event; - -/** - * Note the assignment to a user in the VM status. - */ -public class UpdateAssignment extends Event { - - private final VmPool fromPool; - private final String toUser; - - /** - * Instantiates a new event. - * - * @param fromPool the pool from which the VM was assigned - * @param toUser the to user - */ - public UpdateAssignment(VmPool fromPool, String toUser) { - this.fromPool = fromPool; - this.toUser = toUser; - } - - /** - * Gets the pool from which the VM was assigned. - * - * @return the pool - */ - public VmPool fromPool() { - return fromPool; - } - - /** - * Gets the user to whom the VM was assigned. - * - * @return the to user - */ - public String toUser() { - return toUser; - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java index 73507ae..bc06e68 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java @@ -18,21 +18,21 @@ package org.jdrupes.vmoperator.manager.events; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.VmDefinition; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import org.jgrapes.core.Channel; -import org.jgrapes.core.Event; import org.jgrapes.core.EventPipeline; import org.jgrapes.core.Subchannel.DefaultSubchannel; /** * A subchannel used to send the events related to a specific VM. */ +@SuppressWarnings("PMD.DataClass") public class VmChannel extends DefaultSubchannel { private final EventPipeline pipeline; - private final K8sClient client; - private VmDefinition definition; + private final ApiClient client; + private DynamicKubernetesObject vmDefinition; private long generation = -1; /** @@ -43,7 +43,7 @@ public class VmChannel extends DefaultSubchannel { * @param client the client */ public VmChannel(Channel mainChannel, EventPipeline pipeline, - K8sClient client) { + ApiClient client) { super(mainChannel); this.pipeline = pipeline; this.client = client; @@ -55,18 +55,19 @@ public class VmChannel extends DefaultSubchannel { * @param definition the definition * @return the watch channel */ - public VmChannel setVmDefinition(VmDefinition definition) { - this.definition = definition; + @SuppressWarnings("PMD.LinguisticNaming") + public VmChannel setVmDefinition(DynamicKubernetesObject definition) { + this.vmDefinition = definition; return this; } /** * Returns the last known definition of the VM. * - * @return the defintion + * @return the json object */ - public VmDefinition vmDefinition() { - return definition; + public DynamicKubernetesObject vmDefinition() { + return vmDefinition; } /** @@ -85,6 +86,7 @@ public class VmChannel extends DefaultSubchannel { * @param generation the generation to set * @return true if value has changed */ + @SuppressWarnings("PMD.LinguisticNaming") public boolean setGeneration(long generation) { if (this.generation == generation) { return false; @@ -102,25 +104,12 @@ public class VmChannel extends DefaultSubchannel { return pipeline; } - /** - * Fire the given event on this channel, using the associated - * {@link #pipeline()}. - * - * @param the generic type - * @param event the event - * @return the t - */ - public > T fire(T event) { - pipeline.fire(event, this); - return event; - } - /** * Returns the API client. * * @return the API client */ - public K8sClient client() { + public ApiClient client() { return client; } } diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmResourceChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java similarity index 56% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmResourceChanged.java rename to org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java index eac30fb..fd5d43c 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmResourceChanged.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java @@ -18,42 +18,48 @@ package org.jdrupes.vmoperator.manager.events; -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jdrupes.vmoperator.common.VmDefinition; +import io.kubernetes.client.openapi.models.V1APIResource; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import org.jgrapes.core.Channel; import org.jgrapes.core.Components; import org.jgrapes.core.Event; /** - * Indicates a change in a VM "resource". Note that the resource - * combines the VM CR's metadata (mostly immutable), the VM CR's - * "spec" part, the VM CR's "status" subresource and state information - * from the pod. Consumers that are only interested in "spec" changes - * should check {@link #specChanged()} before processing the event any - * further. + * Indicates a change in a VM definition. Note that the definition + * consists of the metadata (mostly immutable), the "spec" and the + * "status" parts. Consumers that are only interested in "spec" + * changes should check {@link #specChanged()} before processing + * the event any further. */ @SuppressWarnings("PMD.DataClass") -public class VmResourceChanged extends Event { +public class VmDefChanged extends Event { - private final K8sObserver.ResponseType type; - private final VmDefinition vmDefinition; + /** + * The type of change. + */ + public enum Type { + ADDED, MODIFIED, DELETED + } + + private final Type type; private final boolean specChanged; - private final boolean podChanged; + private final V1APIResource crd; + private final DynamicKubernetesObject vmDef; /** * Instantiates a new VM changed event. * * @param type the type - * @param vmDefinition the VM definition * @param specChanged the spec part changed + * @param crd the crd + * @param vmDefinition the VM definition */ - public VmResourceChanged(K8sObserver.ResponseType type, - VmDefinition vmDefinition, boolean specChanged, - boolean podChanged) { + public VmDefChanged(Type type, boolean specChanged, V1APIResource crd, + DynamicKubernetesObject vmDefinition) { this.type = type; - this.vmDefinition = vmDefinition; this.specChanged = specChanged; - this.podChanged = podChanged; + this.crd = crd; + this.vmDef = vmDefinition; } /** @@ -61,19 +67,10 @@ public class VmResourceChanged extends Event { * * @return the type */ - public K8sObserver.ResponseType type() { + public Type type() { return type; } - /** - * Return the VM definition. - * - * @return the VM definition - */ - public VmDefinition vmDefinition() { - return vmDefinition; - } - /** * Indicates if the "spec" part changed. */ @@ -82,19 +79,31 @@ public class VmResourceChanged extends Event { } /** - * Indicates if the pod status changed. + * Returns the Crd. + * + * @return the v 1 API resource */ - public boolean podChanged() { - return podChanged; + public V1APIResource crd() { + return crd; + } + + /** + * Returns the object. + * + * @return the object. + */ + public DynamicKubernetesObject vmDefinition() { + return vmDef; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(Components.objectName(this)).append(" [") - .append(vmDefinition.name()).append(' ').append(type); + .append(vmDef.getMetadata().getName()).append(' ').append(type); if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); + builder.append(", channels="); + builder.append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmPoolChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmPoolChanged.java deleted file mode 100644 index 0c3f3a1..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmPoolChanged.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import org.jdrupes.vmoperator.common.VmPool; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Event; - -/** - * Indicates a change in a pool configuration. - */ -public class VmPoolChanged extends Event { - - private final VmPool vmPool; - private final boolean deleted; - - /** - * Instantiates a new VM changed event. - * - * @param pool the pool - * @param deleted true, if the pool was deleted - */ - public VmPoolChanged(VmPool pool, boolean deleted) { - vmPool = pool; - this.deleted = deleted; - } - - /** - * Instantiates a new VM changed event for an existing pool. - * - * @param pool the pool - */ - public VmPoolChanged(VmPool pool) { - this(pool, false); - } - - /** - * Returns the VM pool. - * - * @return the vm pool - */ - public VmPool vmPool() { - return vmPool; - } - - /** - * Pool has been deleted. - * - * @return true, if successful - */ - public boolean deleted() { - return deleted; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(30); - builder.append(Components.objectName(this)) - .append(" ["); - if (deleted) { - builder.append("Deleted: "); - } - builder.append(vmPool); - if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); - } - builder.append(']'); - return builder.toString(); - } -} diff --git a/org.jdrupes.vmoperator.manager/.eclipse-pmd b/org.jdrupes.vmoperator.manager/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/org.jdrupes.vmoperator.manager/.eclipse-pmd +++ b/org.jdrupes.vmoperator.manager/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.manager/.gitignore b/org.jdrupes.vmoperator.manager/.gitignore deleted file mode 100644 index 50a6b62..0000000 --- a/org.jdrupes.vmoperator.manager/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/logging.properties diff --git a/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs b/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs index 03e8200..6f3b6d4 100644 --- a/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs +++ b/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs @@ -1,6 +1,6 @@ add_header=true eclipse.preferences.version=1 -header_text=/*\n * VM-Operator\n * Copyright (C) 2024 Michael N. Lipp\n * \n * This program is free software\: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n */ +header_text=/*\n * VM-Operator\n * Copyright (C) 2023 Michael N. Lipp\n * \n * This program is free software\: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n */ project_specific_settings=true replacements=\n\n\nReturns the\nSets the\nAdds the\nEdits the\nRemoves the\nInits the\nParses the\nCreates the\nBuilds the\nChecks if is\nPrints the\nChecks for\n\n\n visibility_package=false diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle index 4ce4ed0..d403be8 100644 --- a/org.jdrupes.vmoperator.manager/build.gradle +++ b/org.jdrupes.vmoperator.manager/build.gradle @@ -13,16 +13,17 @@ dependencies { implementation 'commons-cli:commons-cli:1.5.0' - implementation 'org.jgrapes:org.jgrapes.util:[1.38.1,2)' - implementation 'org.jgrapes:org.jgrapes.io:[2.12.1,3)' - implementation 'org.jgrapes:org.jgrapes.http:[3.5.0,4)' + implementation 'org.jgrapes:org.jgrapes.core:[1.19.0,2)' + implementation 'org.jgrapes:org.jgrapes.io:[2.7.0,3)' + implementation 'org.jgrapes:org.jgrapes.http:[3.1.0,4)' + implementation 'org.jgrapes:org.jgrapes.util:[1.31.0,2)' - implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.3.0,3)' - implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.8.0,2)' - implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.4.0,2)' - implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.7.0,2)' - implementation 'org.jgrapes:org.jgrapes.webconlet.markdowndisplay:[1.2.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.3.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.5.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.0.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconlet.locallogin:[1.0.0,2)' + runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.locallogin:[0.1.0,2)' runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.4.0,2)' runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.logviewer:[0.2.0,2)' @@ -31,8 +32,9 @@ dependencies { runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)' runtimeOnly 'org.apache.logging.log4j:log4j-to-jul:2.20.0' - runtimeOnly project(':org.jdrupes.vmoperator.vmmgmt') - runtimeOnly project(':org.jdrupes.vmoperator.vmaccess') + runtimeOnly project(':org.jdrupes.vmoperator.vmconlet') + + testImplementation 'io.fabric8:kubernetes-client:[6.8.1,6.9)' } application { @@ -44,69 +46,75 @@ application { mainClass = 'org.jdrupes.vmoperator.manager.Manager' } -project.ext.gitBranch = grgit.branch.current.name.replace('/', '-') -def registry = "${project.rootProject.properties['docker.registry']}" -def rootVersion = rootProject.version - task buildImage(type: Exec) { dependsOn installDist inputs.files 'src/org/jdrupes/vmoperator/manager/Containerfile' commandLine 'podman', 'build', '--pull', - '-t', "${project.name}:${project.gitBranch}",\ + '-t', "${project.name}:${project.version}",\ '-f', 'src/org/jdrupes/vmoperator/manager/Containerfile', '.' } -task pushImage(type: Exec) { +task tagLatestImage(type: Exec) { dependsOn buildImage - // Don't push without testing first - dependsOn test - - commandLine 'podman', 'push', '--tls-verify=false', \ - "${project.name}:${project.gitBranch}", \ - "${registry}/${project.name}:${project.gitBranch}" -} - -task tagWithVersion(type: Exec) { - dependsOn pushImage - - enabled = !rootVersion.contains("SNAPSHOT") - commandLine 'podman', 'push', \ - "${project.name}:${project.gitBranch}",\ - "${registry}/${project.name}:${project.version}" -} - -task tagAsLatest(type: Exec) { - dependsOn tagWithVersion - - enabled = !rootVersion.contains("SNAPSHOT") - && !rootVersion.contains("alpha") \ - && !rootVersion.contains("beta") \ + 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', 'push', \ - "${project.name}:${project.gitBranch}",\ - "${registry}/${project.name}:latest" + commandLine 'podman', 'tag', "${project.name}:${project.version}",\ + "${project.name}:latest" } -task publishImage { - dependsOn pushImage - dependsOn tagWithVersion - dependsOn tagAsLatest +task buildLatestImage { + dependsOn buildImage + dependsOn tagLatestImage +} + +task pushImage(type: Exec) { + dependsOn buildImage + + commandLine 'podman', 'push', '--tls-verify=false', \ + "localhost/${project.name}:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}:${project.version}" +} + +task pushLatestImage(type: Exec) { + dependsOn buildLatestImage + + 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', 'push', '--tls-verify=false', \ + "localhost/${project.name}:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}:latest" } task pushForTest(type: Exec) { dependsOn buildImage commandLine 'podman', 'push', '--tls-verify=false', \ - "${project.name}:${project.gitBranch}", \ - "${project.rootProject.properties['docker.testRegistry']}" \ + "localhost/${project.name}:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + "/${project.name}:test" } +task pushImages { + // Don't push without testing first + dependsOn test + dependsOn pushImage + dependsOn pushLatestImage +} + test { enabled = project.hasProperty("k8s.testCluster") diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview.md b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview.md deleted file mode 100644 index b6b9efa..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview.md +++ /dev/null @@ -1,5 +0,0 @@ -You can use the "puzzle piece" icon on the top right corner of the -page to add display widgets (conlets) to the overview tab. - -Use the "full screen" icon on the top right corner of any -conlet (if available) to get a detailed view. diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview_de.md b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview_de.md deleted file mode 100644 index bec5f3e..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview_de.md +++ /dev/null @@ -1,6 +0,0 @@ -Verwenden Sie das "Puzzle"-Icon auf der rechten oberen Ecke -der Seite, um Anzeige-Widgets (Conlets) hinzuzufügen. - -Wenn sich in der rechten oberen Ecke eines Conlets ein Vollbild-Icon -befindet, können Sie es verwenden, um eine Detailansicht in einem neuen -Register anzufordern. diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-footer.ftl.html b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-footer.ftl.html index 72596d5..8147dca 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-footer.ftl.html +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-footer.ftl.html @@ -1,3 +1,3 @@
-Copyright © Michael N. Lipp 2023, 2025 +Copyright © Michael N. Lipp 2023
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties index 6bcc3a2..ec22a06 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties @@ -17,4 +17,3 @@ # consoleTitle = VM-Operator -introTitle = Usage \ No newline at end of file diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n_de.properties b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n_de.properties deleted file mode 100644 index dcbba93..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n_de.properties +++ /dev/null @@ -1,19 +0,0 @@ -# -# VM-Operator -# Copyright (C) 2024 Michael N. Lipp -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License -# for more details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program; if not, see . -# - -introTitle = Benutzung diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/logging.properties b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/logging.properties index 2a16af6..9e6d0f5 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/logging.properties +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/logging.properties @@ -1,6 +1,6 @@ # # VM-Operator -# Copyright (C) 2025 Michael N. Lipp +# Copyright (C) 2023 Michael N. Lipp # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by @@ -19,7 +19,10 @@ handlers=java.util.logging.ConsoleHandler, \ org.jgrapes.webconlet.logviewer.LogViewerHandler -org.jdrupes.vmoperator.level=FINE +org.jgrapes.level=FINE +org.jgrapes.core.handlerTracking.level=FINER + +org.jdrupes.vmoperator.manager.level=FINE java.util.logging.ConsoleHandler.level=ALL java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml index 0200021..451a465 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml @@ -1,138 +1,135 @@ apiVersion: v1 kind: ConfigMap metadata: - namespace: ${ cr.namespace() } - name: ${ cr.name() } + namespace: ${ cr.metadata.namespace.asString } + name: ${ cr.metadata.name.asString } labels: app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } annotations: vmoperator.jdrupes.org/version: ${ managerVersion } ownerReferences: - - apiVersion: ${ cr.apiVersion() } - kind: ${ constants.Crd.KIND_VM } - name: ${ cr.name() } - uid: ${ cr.metadata().getUid() } + - apiVersion: ${ cr.apiVersion.asString } + kind: ${ constants.VM_OP_KIND_VM } + name: ${ cr.metadata.name.asString } + uid: ${ cr.metadata.uid.asString } controller: false - + data: config.yaml: | "/Runner": # The directory used to store data files. Defaults to (depending on # values available): - # * $XDG_DATA_HOME/vmrunner/${ cr.name() } - # * $HOME/.local/share/vmrunner/${ cr.name() } - # * ./${ cr.name() } + # * $XDG_DATA_HOME/vmrunner/${ cr.metadata.name.asString } + # * $HOME/.local/share/vmrunner/${ cr.metadata.name.asString } + # * ./${ cr.metadata.name.asString } dataDir: /var/local/vm-data # The directory used to store runtime files. Defaults to (depending on # values available): - # * $XDG_RUNTIME_DIR/vmrunner/${ cr.name() } - # * /tmp/$USER/vmrunner/${ cr.name() } - # * /tmp/vmrunner/${ cr.name() } - # runtimeDir: "$XDG_RUNTIME_DIR/vmrunner/${ cr.name() }" + # * $XDG_RUNTIME_DIR/vmrunner/${ cr.metadata.name.asString } + # * /tmp/$USER/vmrunner/${ cr.metadata.name.asString } + # * /tmp/vmrunner/${ cr.metadata.name.asString } + # runtimeDir: "$XDG_RUNTIME_DIR/vmrunner/${ cr.metadata.name.asString }" - <#assign spec = cr.spec() /> # The template to use. Resolved relative to /usr/share/vmrunner/templates. # template: "Standard-VM-latest.ftl.yaml" - <#if spec.runnerTemplate?? && spec.runnerTemplate.source?? > - template: ${ spec.runnerTemplate.source } + <#if cr.spec.runnerTemplate?? && cr.spec.runnerTemplate.source?? > + template: ${ cr.spec.runnerTemplate.source.asString } # The template is copied to the data diretory when the VM starts for # the first time. Subsequent starts use the copy unless this option is set. - <#if spec.runnerTemplate?? && spec.runnerTemplate.update?? > - updateTemplate: ${ spec.runnerTemplate.update?c } + <#if cr.spec.runnerTemplate?? && cr.spec.runnerTemplate.update?? > + updateTemplate: ${ cr.spec.runnerTemplate.update.asBoolean?c } # Whether a shutdown initiated by the guest stops the pod deployment - guestShutdownStops: ${ (spec.guestShutdownStops!false)?c } + guestShutdownStops: ${ cr.spec.guestShutdownStops!false?c } - # When incremented, the VM is reset. The value has no default value, - # i.e. if you start the VM without a value for this property, and - # decide to trigger a reset later, you have to first set the value - # and then inrement it. - resetCounter: ${ cr.extra().resetCount()?c } - # Forward the cloud-init data if provided - <#if spec.cloudInit??> + <#if cr.spec.cloudInit??> cloudInit: - metaData: ${ toJson(adjustCloudInitMeta(spec.cloudInit.metaData!{}, cr.metadata())) } - <#if spec.cloudInit.userData??> - userData: ${ toJson(spec.cloudInit.userData) } + <#if cr.spec.cloudInit.metaData??> + metaData: ${ cr.spec.cloudInit.metaData.toString() } + <#else> + metaData: {} + + <#if cr.spec.cloudInit.userData??> + userData: ${ cr.spec.cloudInit.userData.toString() } <#else> userData: {} - <#if spec.cloudInit.networkConfig??> - networkConfig: ${ toJson(spec.cloudInit.networkConfig) } + <#if cr.spec.cloudInit.networkConfig??> + networkConfig: ${ cr.spec.cloudInit.networkConfig.toString() } # Define the VM (required) vm: # The VM's name (required) - name: ${ cr.name() } + name: ${ cr.metadata.name.asString } # The machine's uuid. If none is specified, a uuid is generated # and stored in the data directory. If the uuid is important # (e.g. because licenses depend on it) it is recommaned to specify # it here explicitly or to carefully backup the data directory. # uuid: "generated uuid" - <#if spec.vm.machineUuid??> - uuid: "${ spec.vm.machineUuid }" + <#if cr.spec.vm.machineUuid??> + uuid: "${ cr.spec.vm.machineUuid.asString }" # Whether to provide a software TPM (defaults to false) # useTpm: false - useTpm: ${ spec.vm.useTpm?c } + useTpm: ${ cr.spec.vm.useTpm.asBoolean?c } # How to boot (see https://github.com/mnlipp/VM-Operator/blob/main/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml): # * bios # * uefi[-4m] # * secure[-4m] - firmware: ${ spec.vm.firmware } + firmware: ${ cr.spec.vm.firmware.asString } # Whether to show a boot menu. # bootMenu: false - bootMenu: ${ spec.vm.bootMenu?c } + bootMenu: ${ cr.spec.vm.bootMenu.asBoolean?c } # When terminating, a graceful powerdown is attempted. If it # doesn't succeed within the given timeout (seconds) SIGTERM # is sent to Qemu. # powerdownTimeout: 900 - powerdownTimeout: ${ spec.vm.powerdownTimeout?c } + powerdownTimeout: ${ cr.spec.vm.powerdownTimeout.asLong?c } # CPU settings - cpuModel: ${ spec.vm.cpuModel } + cpuModel: ${ cr.spec.vm.cpuModel.asString } # Setting maximumCpus to 1 omits the "-smp" options. The defaults (0) # cause the corresponding property to be omitted from the "-smp" option. # If currentCpus is greater than maximumCpus, the latter is adjusted. - <#if spec.vm.maximumCpus?? > - maximumCpus: ${ parseQuantity(spec.vm.maximumCpus)?c } + <#if cr.spec.vm.maximumCpus?? > + maximumCpus: ${ parseQuantity(cr.spec.vm.maximumCpus.asString)?c } - <#if spec.vm.cpuTopology?? > - sockets: ${ spec.vm.cpuTopology.sockets?c } - diesPerSocket: ${ spec.vm.cpuTopology.diesPerSocket?c } - coresPerDie: ${ spec.vm.cpuTopology.coresPerDie?c } - threadsPerCore: ${ spec.vm.cpuTopology.threadsPerCore?c } + <#if cr.spec.vm.cpuTopology?? > + sockets: ${ cr.spec.vm.cpuTopology.sockets.asInt?c } + diesPerSocket: ${ cr.spec.vm.cpuTopology.diesPerSocket.asInt?c } + coresPerDie: ${ cr.spec.vm.cpuTopology.coresPerDie.asInt?c } + threadsPerCore: ${ cr.spec.vm.cpuTopology.threadsPerCore.asInt?c } - <#if spec.vm.currentCpus?? > - currentCpus: ${ parseQuantity(spec.vm.currentCpus)?c } + <#if cr.spec.vm.currentCpus?? > + currentCpus: ${ parseQuantity(cr.spec.vm.currentCpus.asString)?c } # RAM settings # Maximum defaults to 1G - maximumRam: "${ formatMemory(parseQuantity(spec.vm.maximumRam)) }" - <#if spec.vm.currentRam?? > - currentRam: "${ formatMemory(parseQuantity(spec.vm.currentRam)) }" + maximumRam: "${ formatMemory(parseQuantity(cr.spec.vm.maximumRam.asString)) }" + <#if cr.spec.vm.currentRam?? > + currentRam: "${ formatMemory(parseQuantity(cr.spec.vm.currentRam.asString)) }" # RTC settings. # rtcBase: utc # rtcClock: rt - rtcBase: ${ spec.vm.rtcBase } - rtcClock: ${ spec.vm.rtcClock } + rtcBase: ${ cr.spec.vm.rtcBase.asString } + rtcClock: ${ cr.spec.vm.rtcClock.asString } # Network settings # Supported types are "tap" and "user" (for debugging). Type "user" @@ -144,19 +141,19 @@ data: # mac: (undefined) network: <#assign nwCounter = 0/> - <#list spec.vm.networks as itf> + <#list cr.spec.vm.networks.asList() as itf> <#if itf.tap??> - type: tap - device: ${ itf.tap.device } - bridge: ${ itf.tap.bridge } + device: ${ itf.tap.device.asString } + bridge: ${ itf.tap.bridge.asString } <#if itf.tap.mac??> - mac: "${ itf.tap.mac }" + mac: "${ itf.tap.mac.asString }" <#elseif itf.user??> - type: user - device: ${ itf.user.device } + device: ${ itf.user.device.asString } <#if itf.user.net??> - net: "${ itf.user.net }" + net: "${ itf.user.net.asString }" <#assign nwCounter += 1/> @@ -172,11 +169,11 @@ data: # file: (undefined) drives: <#assign drvCounter = 0/> - <#list spec.vm.disks as disk> + <#list cr.spec.vm.disks.asList() as disk> <#if disk.volumeClaimTemplate?? && disk.volumeClaimTemplate.metadata?? && disk.volumeClaimTemplate.metadata.name??> - <#assign diskName = disk.volumeClaimTemplate.metadata.name + "-disk"> + <#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk"> <#else> <#assign diskName = "disk-" + drvCounter> @@ -184,36 +181,30 @@ data: - type: raw resource: /dev/${ diskName } <#if disk.bootindex??> - bootindex: ${ disk.bootindex?c } + bootindex: ${ disk.bootindex.asInt?c } <#assign drvCounter = drvCounter + 1/> <#if disk.cdrom??> - type: ide-cd - file: "${ imageLocation(disk.cdrom.image) }" + file: "${ disk.cdrom.image.asString }" <#if disk.bootindex??> - bootindex: ${ disk.bootindex?c } + bootindex: ${ disk.bootindex.asInt?c } display: - <#if spec.vm.display.outputs?? > - outputs: ${ spec.vm.display.outputs?c } - - <#if loginRequestedFor?? > - loggedInUser: "${ loginRequestedFor }" - - <#if spec.vm.display.spice??> + <#if cr.spec.vm.display.spice??> spice: - port: ${ spec.vm.display.spice.port?c } - <#if spec.vm.display.spice.ticket??> - ticket: "${ spec.vm.display.spice.ticket }" + port: ${ cr.spec.vm.display.spice.port.asInt?c } + <#if cr.spec.vm.display.spice.ticket??> + ticket: "${ cr.spec.vm.display.spice.ticket.asString }" - <#if spec.vm.display.spice.streamingVideo??> - streaming-video: "${ spec.vm.display.spice.streamingVideo }" + <#if cr.spec.vm.display.spice.streamingVideo??> + ticket: "${ cr.spec.vm.display.spice.streamingVideo.asString }" - usbRedirects: ${ spec.vm.display.spice.usbRedirects?c } + usbRedirects: ${ cr.spec.vm.display.spice.usbRedirects.asInt?c } logging.properties: | diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDataPvc.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDataPvc.ftl.yaml deleted file mode 100644 index ddb638c..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDataPvc.ftl.yaml +++ /dev/null @@ -1,18 +0,0 @@ -kind: PersistentVolumeClaim -apiVersion: v1 -metadata: - namespace: ${ cr.namespace() } - name: ${ runnerDataPvcName } - labels: - app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } - app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } -spec: - accessModes: - - ReadWriteOnce - <#if reconciler.runnerDataPvc?? && reconciler.runnerDataPvc.storageClassName??> - storageClassName: ${ reconciler.runnerDataPvc.storageClassName } - - resources: - requests: - storage: 1Mi diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDiskPvc.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDiskPvc.ftl.yaml deleted file mode 100644 index 8258d55..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDiskPvc.ftl.yaml +++ /dev/null @@ -1,16 +0,0 @@ -kind: PersistentVolumeClaim -apiVersion: v1 -metadata: - namespace: ${ cr.namespace() } - name: ${ disk.generatedPvcName } - labels: - app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } - app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } - <#if disk.volumeClaimTemplate.metadata?? - && disk.volumeClaimTemplate.metadata.annotations??> - annotations: - ${ toJson(disk.volumeClaimTemplate.metadata.annotations) } - -spec: - ${ toJson(disk.volumeClaimTemplate.spec) } diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml index b7215a5..2c32aa6 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml @@ -1,26 +1,26 @@ apiVersion: v1 kind: Service metadata: - namespace: ${ cr.namespace() } - name: ${ cr.name() } + namespace: ${ cr.metadata.namespace.asString } + name: ${ cr.metadata.name.asString } labels: app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } annotations: vmoperator.jdrupes.org/version: ${ managerVersion } ownerReferences: - - apiVersion: ${ cr.apiVersion() } - kind: ${ constants.Crd.KIND_VM } - name: ${ cr.name() } - uid: ${ cr.metadata().getUid() } + - apiVersion: ${ cr.apiVersion.asString } + kind: ${ constants.VM_OP_KIND_VM } + name: ${ cr.metadata.name.asString } + uid: ${ cr.metadata.uid.asString } controller: false spec: type: LoadBalancer ports: - name: spice - port: ${ cr.spec().vm.display.spice.port?c } + port: ${ cr.spec.vm.display.spice.port.asInt?c } selector: app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml deleted file mode 100644 index 7518ad3..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml +++ /dev/null @@ -1,135 +0,0 @@ -kind: Pod -apiVersion: v1 -metadata: - namespace: ${ cr.namespace() } - name: ${ cr.name() } - labels: - app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } - app.kubernetes.io/component: ${ constants.APP_NAME } - app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } - annotations: - # Triggers update of config map mounted in pod - # See https://ahmet.im/blog/kubernetes-secret-volumes-delay/ - vmrunner.jdrupes.org/cmVersion: "${ configMapResourceVersion }" - vmoperator.jdrupes.org/version: ${ managerVersion } - ownerReferences: - - apiVersion: ${ cr.apiVersion() } - kind: ${ constants.Crd.KIND_VM } - name: ${ cr.name() } - uid: ${ cr.metadata().getUid() } - blockOwnerDeletion: true - controller: false -<#assign spec = cr.spec() /> -spec: - containers: - - name: ${ cr.name() } - <#assign image = spec.image> - <#if image.source??> - image: ${ image.source } - <#else> - image: ${ image.repository }/${ image.path }<#if image.version??>:${ image.version } - - <#if image.pullPolicy??> - imagePullPolicy: ${ image.pullPolicy } - - <#if spec.vm.display.spice??> - ports: - <#if spec.vm.display.spice??> - - name: spice - containerPort: ${ spec.vm.display.spice.port?c } - protocol: TCP - - - volumeMounts: - # Not needed because pod is priviledged: - # - mountPath: /dev/kvm - # name: dev-kvm - # - mountPath: /dev/net/tun - # name: dev-tun - # - mountPath: /sys/fs/cgroup - # name: cgroup - - name: config - mountPath: /etc/opt/vmrunner - - name: runner-data - mountPath: /var/local/vm-data - - name: vmop-image-repository - mountPath: ${ constants.IMAGE_REPO_PATH } - volumeDevices: - <#list spec.vm.disks as disk> - <#if disk.volumeClaimTemplate??> - - name: ${ disk.generatedDiskName } - devicePath: /dev/${ disk.generatedDiskName } - - - securityContext: - privileged: true - <#if spec.resources??> - resources: ${ toJson(spec.resources) } - <#else> - <#if spec.vm.currentCpus?? || spec.vm.currentRam?? > - resources: - requests: - <#if spec.vm.currentCpus?? > - <#assign factor = 2.0 /> - <#if reconciler.cpuOvercommit??> - <#assign factor = reconciler.cpuOvercommit * 1.0 /> - - cpu: ${ (parseQuantity(spec.vm.currentCpus) / factor)?c } - - <#if spec.vm.currentRam?? > - <#assign factor = 1.25 /> - <#if reconciler.ramOvercommit??> - <#assign factor = reconciler.ramOvercommit * 1.0 /> - - memory: ${ (parseQuantity(spec.vm.currentRam) / factor)?floor?c } - - - - volumes: - # Not needed because pod is priviledged: - # - name: dev-kvm - # hostPath: - # path: /dev/kvm - # type: CharDevice - # - hostPath: - # path: /dev/net/tun - # type: CharDevice - # name: dev-tun - # - name: cgroup - # hostPath: - # path: /sys/fs/cgroup - - name: config - projected: - sources: - - configMap: - name: ${ cr.name() } - <#if displaySecret??> - - secret: - name: ${ displaySecret } - - - name: vmop-image-repository - persistentVolumeClaim: - claimName: vmop-image-repository - - name: runner-data - persistentVolumeClaim: - claimName: ${ runnerDataPvcName } - <#list spec.vm.disks as disk> - <#if disk.volumeClaimTemplate??> - - name: ${ disk.generatedDiskName } - persistentVolumeClaim: - claimName: ${ disk.generatedPvcName } - - - hostNetwork: true - terminationGracePeriodSeconds: ${ (spec.vm.powerdownTimeout + 5)?c } - <#if spec.nodeName??> - nodeName: ${ spec.nodeName } - - <#if spec.nodeSelector??> - nodeSelector: ${ toJson(spec.nodeSelector) } - - <#if spec.affinity??> - affinity: ${ toJson(spec.affinity) } - - serviceAccountName: vm-runner diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml new file mode 100644 index 0000000..2e5712b --- /dev/null +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml @@ -0,0 +1,188 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + namespace: ${ cr.metadata.namespace.asString } + name: ${ cr.metadata.name.asString } + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } + annotations: + vmoperator.jdrupes.org/version: ${ managerVersion } + ownerReferences: + - apiVersion: ${ cr.apiVersion.asString } + kind: ${ constants.VM_OP_KIND_VM } + name: ${ cr.metadata.name.asString } + uid: ${ cr.metadata.uid.asString } + blockOwnerDeletion: true + controller: false + +spec: + selector: + matchLabels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + replicas: ${ (cr.spec.vm.state.asString == "Running")?then(1, 0) } + updateStrategy: + type: OnDelete + template: + metadata: + namespace: ${ cr.metadata.namespace.asString } + name: ${ cr.metadata.name.asString } + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/component: ${ constants.APP_NAME } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } + annotations: + # Triggers update of config map mounted in pod + # See https://ahmet.im/blog/kubernetes-secret-volumes-delay/ + vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion.asString }" + vmoperator.jdrupes.org/version: ${ managerVersion } + spec: + containers: + - name: ${ cr.metadata.name.asString } + <#assign image = cr.spec.image> + <#if image.source??> + image: ${ image.source.asString } + <#else> + image: ${ image.repository.asString }/${ image.path.asString }<#if image.version??>:${ image.version.asString } + + <#if image.pullPolicy??> + imagePullPolicy: ${ image.pullPolicy.asString } + + <#if cr.spec.vm.display.spice??> + ports: + <#if cr.spec.vm.display.spice??> + - name: spice + containerPort: ${ cr.spec.vm.display.spice.port.asInt?c } + protocol: TCP + + + volumeMounts: + # Not needed because pod is priviledged: + # - mountPath: /dev/kvm + # name: dev-kvm + # - mountPath: /dev/net/tun + # name: dev-tun + # - mountPath: /sys/fs/cgroup + # name: cgroup + - name: config + mountPath: /etc/opt/vmrunner + - name: runner-data + mountPath: /var/local/vm-data + - name: vmop-image-repository + mountPath: ${ constants.IMAGE_REPO_PATH } + volumeDevices: + <#assign diskCounter = 0/> + <#list cr.spec.vm.disks.asList() as disk> + <#if disk.volumeClaimTemplate??> + <#if disk.volumeClaimTemplate.metadata?? + && disk.volumeClaimTemplate.metadata.name??> + <#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk"> + <#else> + <#assign diskName = "disk-" + diskCounter> + + - name: ${ diskName } + devicePath: /dev/${ diskName } + <#assign diskCounter = diskCounter + 1/> + + + securityContext: + privileged: true + <#if cr.spec.resources??> + resources: ${ cr.spec.resources.toString() } + <#else> + <#if cr.spec.vm.currentCpus?? || cr.spec.vm.currentRam?? > + resources: + requests: + <#if cr.spec.vm.currentCpus?? > + <#assign factor = 2.0 /> + <#if reconciler.cpuOvercommit??> + <#assign factor = reconciler.cpuOvercommit * 1.0 /> + + cpu: ${ (parseQuantity(cr.spec.vm.currentCpus.asString) / factor)?c } + + <#if cr.spec.vm.currentRam?? > + <#assign factor = 1.25 /> + <#if reconciler.ramOvercommit??> + <#assign factor = reconciler.ramOvercommit * 1.0 /> + + memory: ${ (parseQuantity(cr.spec.vm.currentRam.asString) / factor)?floor?c } + + + + volumes: + # Not needed because pod is priviledged: + # - name: dev-kvm + # hostPath: + # path: /dev/kvm + # type: CharDevice + # - hostPath: + # path: /dev/net/tun + # type: CharDevice + # name: dev-tun + # - name: cgroup + # hostPath: + # path: /sys/fs/cgroup + - name: config + configMap: + name: ${ cr.metadata.name.asString } + - name: vmop-image-repository + persistentVolumeClaim: + claimName: vmop-image-repository + hostNetwork: true + terminationGracePeriodSeconds: ${ (cr.spec.vm.powerdownTimeout.asInt + 5)?c } + <#if cr.spec.nodeName??> + nodeName: ${ cr.spec.nodeName.asString } + + <#if cr.spec.nodeSelector??> + nodeSelector: ${ cr.spec.nodeSelector.toString() } + + <#if cr.spec.affinity??> + affinity: ${ cr.spec.affinity.toString() } + + serviceAccountName: vm-runner + volumeClaimTemplates: + - metadata: + namespace: ${ cr.metadata.namespace.asString } + name: runner-data + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } + spec: + accessModes: + - ReadWriteOnce + <#if reconciler.runnerDataPvc?? && reconciler.runnerDataPvc.storageClassName??> + storageClassName: ${ reconciler.runnerDataPvc.storageClassName } + + resources: + requests: + storage: 1Mi + <#assign diskCounter = 0/> + <#list cr.spec.vm.disks.asList() as disk> + <#if disk.volumeClaimTemplate??> + <#if disk.volumeClaimTemplate.metadata?? + && disk.volumeClaimTemplate.metadata.name??> + <#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk"> + <#else> + <#assign diskName = "disk-" + diskCounter> + + - metadata: + namespace: ${ cr.metadata.namespace.asString } + name: ${ diskName } + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } + <#if disk.volumeClaimTemplate.metadata?? + && disk.volumeClaimTemplate.metadata.annotations??> + annotations: + ${ disk.volumeClaimTemplate.metadata.annotations.toString() } + + spec: + ${ disk.volumeClaimTemplate.spec.toString() } + <#assign diskCounter = diskCounter + 1/> + + diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AbstractMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AbstractMonitor.java deleted file mode 100644 index c10752e..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AbstractMonitor.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.common.KubernetesListObject; -import io.kubernetes.client.common.KubernetesObject; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.Watch.Response; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jdrupes.vmoperator.manager.events.Exit; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Component; -import org.jgrapes.core.Components; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Start; -import org.jgrapes.core.events.Stop; -import org.jgrapes.util.events.ConfigurationUpdate; - -/** - * A base class for monitoring VM related resources. When started, - * it creates observers for all versions of the the {@link APIResource} - * configured by {@link #context(APIResource)}. The APIResource is not - * passed to the constructor because in some cases it has to be - * evaluated lazily. - * - * @param the object type for the context - * @param the object list type for the context - */ -public abstract class AbstractMonitor extends Component { - - private final Class objectClass; - private final Class objectListClass; - private K8sClient client; - private APIResource context; - private String namespace; - private ListOptions options = new ListOptions(); - private final AtomicInteger observerCounter = new AtomicInteger(0); - - /** - * Initializes the instance. - * - * @param componentChannel the component channel - * @param objectClass the class of the Kubernetes object to watch - * @param objectListClass the class of the list of Kubernetes objects - * to watch - */ - protected AbstractMonitor(Channel componentChannel, - Class objectClass, Class objectListClass) { - super(componentChannel); - this.objectClass = objectClass; - this.objectListClass = objectListClass; - } - - /** - * Return the client. - * - * @return the client - */ - public K8sClient client() { - return client; - } - - /** - * Sets the client to be used. - * - * @param client the client - * @return the abstract monitor - */ - public AbstractMonitor client(K8sClient client) { - this.client = client; - return this; - } - - /** - * Return the observed namespace. - * - * @return the namespace - */ - public String namespace() { - return namespace; - } - - /** - * Sets the namespace to be observed. - * - * @param namespace the namespaceToWatch to set - * @return the abstract monitor - */ - public AbstractMonitor namespace(String namespace) { - this.namespace = namespace; - return this; - } - - /** - * Returns the options for selecting the objects to observe. - * - * @return the options - */ - public ListOptions options() { - return options; - } - - /** - * Sets the options for selecting the objects to observe. - * - * @param options the options to set - * @return the abstract monitor - */ - public AbstractMonitor options(ListOptions options) { - this.options = options; - return this; - } - - /** - * Returns the observed context. - * - * @return the context - */ - public APIResource context() { - return context; - } - - /** - * Sets the context to observe. - * - * @param context the context - * @return the abstract monitor - */ - public AbstractMonitor context(APIResource context) { - this.context = context; - return this; - } - - /** - * Looks for a key "namespace" in the configuration and, if found, - * sets the namespace to its value. - * - * @param event the event - */ - @Handler - public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured(Components.manager(parent()).componentPath()) - .ifPresent(c -> { - if (c.containsKey("namespace")) { - namespace = (String) c.get("namespace"); - } - }); - } - - /** - * Handle the start event. Configures the namespace, invokes - * {@link #prepareMonitoring()} and starts the observers. - * - * @param event the event - */ - @Handler(priority = 10) - public void onStart(Start event) { - try { - // Get namespace - if (namespace == null) { - var path = Path - .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); - if (Files.isReadable(path)) { - namespace - = Files.lines(path).findFirst().orElse(null); - } - } - - // Additional preparations by derived class - prepareMonitoring(); - assert client != null; - assert context != null; - assert namespace != null; - - // Monitor all versions - for (var version : context.getVersions()) { - createObserver(version); - } - registerAsGenerator(); - } catch (IOException | ApiException e) { - logger.log(Level.SEVERE, e, - () -> "Cannot watch VMs, terminating."); - event.cancel(true); - fire(new Exit(1)); - } - } - - private void createObserver(String version) { - observerCounter.incrementAndGet(); - new K8sObserver<>(objectClass, objectListClass, client, - K8s.preferred(context, version), namespace, options) - .handler(this::handleChange).onTerminated((o, t) -> { - if (observerCounter.decrementAndGet() == 0) { - unregisterAsGenerator(); - } - // Exception has been logged already - if (t != null) { - fire(new Stop()); - } - }).start(); - } - - /** - * Invoked by {@link #onStart(Start)} after the namespace has - * been configured and before starting the observer. This is - * the last opportunity to invoke {@link #context(APIResource)}. - * - * @throws IOException Signals that an I/O exception has occurred. - * @throws ApiException the api exception - */ - @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") - protected void prepareMonitoring() throws IOException, ApiException { - // To be overridden by derived class. - } - - /** - * Handle an observed change. The method is invoked by the observer - * thread(s). It is the responsibility of the implementing class to - * fire derived events on the appropriate event pipeline. - * - * @param client the client - * @param change the change - */ - protected abstract void handleChange(K8sClient client, Response change); -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java index 912b623..000a21e 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java @@ -18,17 +18,11 @@ package org.jdrupes.vmoperator.manager; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Collections; -import java.util.ResourceBundle; -import java.util.stream.Collectors; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; import org.jgrapes.core.annotation.Handler; -import org.jgrapes.webconlet.markdowndisplay.MarkdownDisplayConlet; -import org.jgrapes.webconsole.base.Conlet.RenderMode; +import org.jgrapes.webconsole.base.Conlet; import org.jgrapes.webconsole.base.ConsoleConnection; import org.jgrapes.webconsole.base.events.AddConletRequest; import org.jgrapes.webconsole.base.events.ConsoleConfigured; @@ -69,13 +63,10 @@ public class AvoidEmptyPolicy extends Component { * @param event the event * @param connection the connection */ - @Handler(priority = 100) + @Handler public void onRenderConlet(RenderConlet event, ConsoleConnection connection) { - if (event.renderAs().contains(RenderMode.Preview) - || event.renderAs().contains(RenderMode.View)) { - connection.session().put(renderedFlagName, true); - } + connection.session().put(renderedFlagName, true); } /** @@ -85,42 +76,18 @@ public class AvoidEmptyPolicy extends Component { * @param connection the console connection * @throws InterruptedException the interrupted exception */ - @Handler(priority = -100) + @Handler public void onConsoleConfigured(ConsoleConfigured event, ConsoleConnection connection) throws InterruptedException, IOException { - if ((Boolean) connection.session().getOrDefault(renderedFlagName, - false)) { + if ((Boolean) connection.session().getOrDefault( + renderedFlagName, false)) { return; } - var resourceBundle = ResourceBundle.getBundle( - getClass().getPackage().getName() + ".l10n", connection.locale(), - getClass().getClassLoader(), - ResourceBundle.Control.getNoFallbackControl( - ResourceBundle.Control.FORMAT_DEFAULT)); - var locale = resourceBundle.getLocale().toString(); - String shortDesc; - try (BufferedReader shortDescReader - = new BufferedReader(new InputStreamReader( - AvoidEmptyPolicy.class.getResourceAsStream( - "ManagerIntro-Preview" + (locale.isEmpty() ? "" - : "_" + locale) + ".md"), - "utf-8"))) { - shortDesc - = shortDescReader.lines().collect(Collectors.joining("\n")); - } fire(new AddConletRequest(event.event().event().renderSupport(), - MarkdownDisplayConlet.class.getName(), - RenderMode.asSet(RenderMode.Preview)) - .addProperty(MarkdownDisplayConlet.CONLET_ID, - getClass().getName()) - .addProperty(MarkdownDisplayConlet.TITLE, - resourceBundle.getString("consoleTitle")) - .addProperty(MarkdownDisplayConlet.PREVIEW_SOURCE, - shortDesc) - .addProperty(MarkdownDisplayConlet.DELETABLE, true) - .addProperty(MarkdownDisplayConlet.EDITABLE_BY, - Collections.EMPTY_SET), + "org.jdrupes.vmoperator.vmconlet.VmConlet", + Conlet.RenderMode + .asSet(Conlet.RenderMode.Preview, Conlet.RenderMode.View)), connection); } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java index 0ca6312..0fbb3a7 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java @@ -18,18 +18,11 @@ package org.jdrupes.vmoperator.manager; -import com.google.gson.JsonObject; -import freemarker.template.AdapterTemplateModel; import freemarker.template.Configuration; import freemarker.template.TemplateException; -import freemarker.template.TemplateMethodModelEx; -import freemarker.template.TemplateModel; -import freemarker.template.TemplateModelException; -import freemarker.template.utility.DeepUnwrap; import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import io.kubernetes.client.util.generic.dynamic.Dynamics; @@ -37,18 +30,13 @@ import io.kubernetes.client.util.generic.options.ListOptions; import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.io.StringWriter; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.logging.Logger; import org.jdrupes.vmoperator.common.K8s; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.util.DataPath; -import org.jdrupes.vmoperator.util.GsonPtr; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -56,6 +44,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; /** * Delegee for reconciling the config map */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") /* default */ class ConfigMapReconciler { protected final Logger logger = Logger.getLogger(getClass().getName()); @@ -73,72 +62,34 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; /** * Reconcile. * + * @param event the event * @param model the model * @param channel the channel - * @param modelChanged the model has changed + * @return the dynamic kubernetes object * @throws IOException Signals that an I/O exception has occurred. * @throws TemplateException the template exception - * @throws ApiException the API exception + * @throws ApiException the api exception */ - public void reconcile(Map model, VmChannel channel, - boolean modelChanged) + public DynamicKubernetesObject reconcile(VmDefChanged event, + Map model, VmChannel channel) throws IOException, TemplateException, ApiException { - // Check if an update is needed - var prevData = channel.associated(PrevData.class) - .orElseGet(() -> new PrevData(null, new HashMap<>())); - Object newInputs = model.get("loginRequestedFor"); - if (!modelChanged && Objects.equals(prevData.inputs, newInputs)) { - // Make added data available in new model - model.putAll(prevData.added); - return; - } - prevData = new PrevData(newInputs, prevData.added); - channel.setAssociated(PrevData.class, prevData); + // Get API + DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", + "configmaps", channel.client()); // Combine template and data and parse result - logger.fine(() -> "Create/update configmap " - + DataPath. get(model, "cr", "name").orElse("unknown")); - model.put("adjustCloudInitMeta", adjustCloudInitMetaModel); - prevData.added.put("adjustCloudInitMeta", adjustCloudInitMetaModel); var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml"); StringWriter out = new StringWriter(); fmTemplate.process(model, out); // Avoid Yaml.load due to // https://github.com/kubernetes-client/java/issues/2741 - var newCm = Dynamics.newFromYaml( + var mapDef = Dynamics.newFromYaml( new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); - // Maybe override logging.properties from reconciler configuration. - DataPath. get(model, "reconciler", "loggingProperties") - .ifPresent(props -> { - GsonPtr.to(newCm.getRaw()).getAs(JsonObject.class, "data") - .get().addProperty("logging.properties", props); - }); - - // Maybe override logging.properties from VM definition. - DataPath. get(model, "cr", "spec", "loggingProperties") - .ifPresent(props -> { - GsonPtr.to(newCm.getRaw()).getAs(JsonObject.class, "data") - .get().addProperty("logging.properties", props); - }); - - // Get API and update - DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", - "configmaps", channel.client()); - // Apply and maybe force pod update - var updatedCm = K8s.apply(cmApi, newCm, newCm.getRaw().toString()); - maybeForceUpdate(channel.client(), updatedCm); - model.put("configMapResourceVersion", - updatedCm.getMetadata().getResourceVersion()); - prevData.added.put("configMapResourceVersion", - updatedCm.getMetadata().getResourceVersion()); - } - - /** - * Key for association. - */ - private record PrevData(Object inputs, Map added) { + var newState = K8s.apply(cmApi, mapDef, out.toString()); + maybeForceUpdate(channel.client(), newState); + return newState; } /** @@ -153,16 +104,14 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; ListOptions listOpts = new ListOptions(); listOpts.setLabelSelector( "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," - + "app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + newCm.getMetadata() - .getLabels().get("app.kubernetes.io/instance")); + + "app.kubernetes.io/name=" + APP_NAME); // Get pod, selected by label var podApi = new DynamicKubernetesApi("", "v1", "pods", client); var pods = podApi .list(newCm.getMetadata().getNamespace(), listOpts).getObject(); // If the VM is being created, the pod may not exist yet. - if (pods == null || pods.getItems().isEmpty()) { + if (pods == null || pods.getItems().size() == 0) { return; } var pod = pods.getItems().get(0); @@ -184,27 +133,4 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; } } - private final TemplateMethodModelEx adjustCloudInitMetaModel - = new TemplateMethodModelEx() { - @Override - public Object exec(@SuppressWarnings("rawtypes") List arguments) - throws TemplateModelException { - @SuppressWarnings("unchecked") - var res = new HashMap<>((Map) DeepUnwrap - .unwrap((TemplateModel) arguments.get(0))); - var metadata - = (V1ObjectMeta) ((AdapterTemplateModel) arguments.get(1)) - .getAdaptedObject(Object.class); - if (!res.containsKey("instance-id")) { - res.put("instance-id", - Optional.ofNullable(metadata.getGeneration()) - .map(s -> "v" + s).orElse("v1")); - } - if (!res.containsKey("local-hostname")) { - res.put("local-hostname", metadata.getName()); - } - return res; - } - }; - } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Containerfile b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Containerfile index 08c4bff..c212945 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Containerfile +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Containerfile @@ -1,4 +1,4 @@ -FROM docker.io/eclipse-temurin:21-jre-alpine +FROM docker.io/eclipse-temurin:17-jre-alpine COPY build/install/vm-manager /opt/vmmanager diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java index ce14488..589affc 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023, 2025 Michael N. Lipp + * Copyright (C) 2023 Michael N. Lipp * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,39 +18,23 @@ package org.jdrupes.vmoperator.manager; -import com.google.gson.JsonObject; -import io.kubernetes.client.apimachinery.GroupVersionKind; +import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.util.Config; +import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Instant; -import java.util.Comparator; -import java.util.Optional; import java.util.logging.Level; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; -import org.jdrupes.vmoperator.common.VmDefinition.Assignment; -import org.jdrupes.vmoperator.common.VmDefinitionStub; -import org.jdrupes.vmoperator.common.VmPool; -import org.jdrupes.vmoperator.manager.events.AssignVm; -import org.jdrupes.vmoperator.manager.events.ChannelManager; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; +import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.manager.events.Exit; -import org.jdrupes.vmoperator.manager.events.GetPools; -import org.jdrupes.vmoperator.manager.events.GetVms; -import org.jdrupes.vmoperator.manager.events.GetVms.VmData; import org.jdrupes.vmoperator.manager.events.ModifyVm; -import org.jdrupes.vmoperator.manager.events.PodChanged; -import org.jdrupes.vmoperator.manager.events.UpdateAssignment; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmPoolChanged; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; -import org.jgrapes.core.EventPipeline; import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.events.HandlingError; import org.jgrapes.core.events.Start; @@ -61,8 +45,8 @@ import org.jgrapes.util.events.ConfigurationUpdate; * [Operator Whitepaper](https://github.com/cncf/tag-app-delivery/blob/eece8f7307f2970f46f100f51932db106db46968/operator-wg/whitepaper/Operator-WhitePaper_v1-0.md#operator-components-in-kubernetes). * * The implementation splits the controller in two components. The - * {@link VmMonitor} and the {@link Reconciler}. The former watches - * the VM definitions (CRs) and generates {@link VmResourceChanged} events + * {@link VmWatcher} and the {@link Reconciler}. The former watches + * the VM definitions (CRs) and generates {@link VmDefChanged} events * when they change. The latter handles the changes and reconciles the * resources in the cluster. * @@ -95,33 +79,15 @@ import org.jgrapes.util.events.ConfigurationUpdate; public class Controller extends Component { private String namespace; - private final ChannelManager chanMgr; /** * Creates a new instance. */ - @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") public Controller(Channel componentChannel) { super(componentChannel); // Prepare component tree - chanMgr = new ChannelManager<>(name -> { - try { - return new VmChannel(channel(), newEventPipeline(), - new K8sClient()); - } catch (IOException e) { - logger.log(Level.SEVERE, e, () -> "Failed to create client" - + " for handling changes: " + e.getMessage()); - return null; - } - }); - attach(new VmMonitor(channel(), chanMgr)); - attach(new DisplaySecretMonitor(channel(), chanMgr)); - // Currently, we don't use the IP assigned by the load balancer - // to access the VM's console. Might change in the future. - // attach(new ServiceMonitor(channel()).channelManager(chanMgr)); + attach(new VmWatcher(channel())); attach(new Reconciler(channel())); - attach(new PoolMonitor(channel())); - attach(new PodMonitor(channel(), chanMgr)); } /** @@ -183,146 +149,46 @@ public class Controller extends Component { fire(new Exit(2)); return; } - logger.config(() -> "Controlling namespace \"" + namespace + "\"."); + logger.fine(() -> "Controlling namespace \"" + namespace + "\"."); } /** - * Returns the VM data. - * - * @param event the event - */ - @Handler - public void onGetVms(GetVms event) { - event.setResult(chanMgr.channels().stream() - .filter(c -> event.name().isEmpty() - || c.vmDefinition().name().equals(event.name().get())) - .filter(c -> event.user().isEmpty() && event.roles().isEmpty() - || !c.vmDefinition().permissionsFor(event.user().orElse(null), - event.roles()).isEmpty()) - .filter(c -> event.fromPool().isEmpty() - || c.vmDefinition().assignment().map(Assignment::pool) - .map(p -> p.equals(event.fromPool().get())).orElse(false)) - .filter(c -> event.toUser().isEmpty() - || c.vmDefinition().assignment().map(Assignment::user) - .map(u -> u.equals(event.toUser().get())).orElse(false)) - .map(c -> new VmData(c.vmDefinition(), c)) - .toList()); - } - - /** - * Assign a VM if not already assigned. + * On modify vm. * * @param event the event * @throws ApiException the api exception - * @throws InterruptedException + * @throws IOException Signals that an I/O exception has occurred. */ @Handler - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - public void onAssignVm(AssignVm event) - throws ApiException, InterruptedException { - while (true) { - // Search for existing assignment. - var vmQuery = chanMgr.channels().stream() - .filter(c -> c.vmDefinition().assignment().map(Assignment::pool) - .map(p -> p.equals(event.fromPool())).orElse(false)) - .filter(c -> c.vmDefinition().assignment().map(Assignment::user) - .map(u -> u.equals(event.toUser())).orElse(false)) - .findFirst(); - if (vmQuery.isPresent()) { - var vmDef = vmQuery.get().vmDefinition(); - event.setResult(new VmData(vmDef, vmQuery.get())); - return; - } - - // Get the pool definition for checking possible assignment - VmPool vmPool = newEventPipeline().fire(new GetPools() - .withName(event.fromPool())).get().stream().findFirst() - .orElse(null); - if (vmPool == null) { - return; - } - - // Find available VM. - vmQuery = chanMgr.channels().stream() - .filter(c -> vmPool.isAssignable(c.vmDefinition())) - .sorted(Comparator.comparing((VmChannel c) -> c.vmDefinition() - .assignment().map(Assignment::lastUsed) - .orElse(Instant.ofEpochSecond(0))) - .thenComparing(preferRunning)) - .findFirst(); - - // None found - if (vmQuery.isEmpty()) { - return; - } - - // Assign to user - var chosenVm = vmQuery.get(); - if (Optional.ofNullable(chosenVm.fire(new UpdateAssignment( - vmPool, event.toUser())).get()).orElse(false)) { - var vmDef = chosenVm.vmDefinition(); - event.setResult(new VmData(vmDef, chosenVm)); - - // Make sure that a newly assigned VM is running. - chosenVm.fire(new ModifyVm(vmDef.name(), "state", "Running")); - return; - } - } + public void onModigyVm(ModifyVm event) throws ApiException, IOException { + patchVmSpec(event.name(), event.path(), event.value()); } - private static Comparator preferRunning - = new Comparator<>() { - @Override - public int compare(VmChannel ch1, VmChannel ch2) { - if (ch1.vmDefinition().conditionStatus("Running").orElse(false) - && !ch2.vmDefinition().conditionStatus("Running") - .orElse(false)) { - return -1; - } - return 0; - } - }; - - /** - * When s pool is deleted, remove all related assignments. - * - * @param event the event - * @throws InterruptedException - */ - @Handler - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - public void onPoolChanged(VmPoolChanged event) throws InterruptedException { - if (!event.deleted()) { + private void patchVmSpec(String name, String path, Object value) + throws ApiException, IOException { + var crApi = K8s.crApi(Config.defaultClient(), VM_OP_GROUP, + VM_OP_KIND_VM, namespace, name); + if (crApi.isEmpty()) { + logger.warning(() -> "Trying to patch " + namespace + "/" + name + + " which does not exist."); return; } - var vms = newEventPipeline() - .fire(new GetVms().assignedFrom(event.vmPool().name())).get(); - for (var vm : vms) { - vm.channel().fire(new UpdateAssignment(event.vmPool(), null)); - } - } - /** - * Remove runner version from status when pod is deleted - * - * @param event the event - * @param channel the channel - * @throws ApiException the api exception - */ - @Handler - public void onPodChange(PodChanged event, VmChannel channel) - throws ApiException { - if (event.type() == ResponseType.DELETED) { - // Remove runner info from status - var vmDef = channel.vmDefinition(); - var vmStub = VmDefinitionStub.get(channel.client(), - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - vmDef.namespace(), vmDef.name()); - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.remove(Status.RUNNER_VERSION); - return status; - }); + // Patch running + PatchOptions patchOpts = new PatchOptions(); + patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); + String valueAsText = value instanceof String + ? "\"" + value + "\"" + : value.toString(); + var res = crApi.get().patch(namespace, name, + V1Patch.PATCH_FORMAT_JSON_PATCH, + new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/" + + path + "\", \"value\": " + valueAsText + "}]"), + patchOpts); + if (!res.isSuccess()) { + logger.warning( + () -> "Cannot patch pod annotations: " + res.getStatus()); } + } } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java deleted file mode 100644 index b094b79..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import io.kubernetes.client.custom.V1Patch; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1Secret; -import io.kubernetes.client.openapi.models.V1SecretList; -import io.kubernetes.client.util.Watch.Response; -import io.kubernetes.client.util.generic.options.ListOptions; -import io.kubernetes.client.util.generic.options.PatchOptions; -import java.io.IOException; -import java.util.logging.Level; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sV1PodStub; -import org.jdrupes.vmoperator.common.K8sV1SecretStub; -import org.jdrupes.vmoperator.manager.events.ChannelDictionary; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jgrapes.core.Channel; - -/** - * Watches for changes of display secrets. Updates an artifical attribute - * of the pod running the VM in response to force an update of the files - * in the pod that reflect the information from the secret. - */ -public class DisplaySecretMonitor - extends AbstractMonitor { - - private final ChannelDictionary channelDictionary; - - /** - * Instantiates a new display secrets monitor. - * - * @param componentChannel the component channel - * @param channelDictionary the channel dictionary - */ - public DisplaySecretMonitor(Channel componentChannel, - ChannelDictionary channelDictionary) { - super(componentChannel, V1Secret.class, V1SecretList.class); - this.channelDictionary = channelDictionary; - context(K8sV1SecretStub.CONTEXT); - ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME); - options(options); - } - - @Override - protected void prepareMonitoring() throws IOException, ApiException { - client(new K8sClient()); - } - - @Override - protected void handleChange(K8sClient client, Response change) { - String vmName = change.object.getMetadata().getLabels() - .get("app.kubernetes.io/instance"); - if (vmName == null) { - return; - } - var channel = channelDictionary.channel(vmName).orElse(null); - if (channel == null || channel.vmDefinition() == null) { - return; - } - - try { - patchPod(client, change); - } catch (ApiException e) { - logger.log(Level.WARNING, e, - () -> "Cannot patch pod annotations: " + e.getMessage()); - } - } - - private void patchPod(K8sClient client, Response change) - throws ApiException { - // Force update for pod - ListOptions listOpts = new ListOptions(); - listOpts.setLabelSelector( - "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," - + "app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + change.object.getMetadata() - .getLabels().get("app.kubernetes.io/instance")); - // Get pod, selected by label - var pods = K8sV1PodStub.list(client, namespace(), listOpts); - - // If the VM is being created, the pod may not exist yet. - if (pods.isEmpty()) { - return; - } - var pod = pods.iterator().next(); - - // Patch pod annotation - PatchOptions patchOpts = new PatchOptions(); - patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); - pod.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, - new V1Patch("[{\"op\": \"replace\", \"path\": " - + "\"/metadata/annotations/vmrunner.jdrupes.org~1dpVersion\", " - + "\"value\": \"" - + change.object.getMetadata().getResourceVersion() - + "\"}]"), - patchOpts); - } -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java deleted file mode 100644 index 1e3eb0f..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import com.google.gson.JsonObject; -import freemarker.template.TemplateException; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1ObjectMeta; -import io.kubernetes.client.openapi.models.V1Secret; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.io.IOException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.time.Instant; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Scanner; -import java.util.logging.Logger; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.K8sV1SecretStub; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinitionStub; -import org.jdrupes.vmoperator.manager.events.GetDisplaySecret; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; -import org.jdrupes.vmoperator.util.DataPath; -import org.jgrapes.core.Channel; -import org.jgrapes.core.CompletionLock; -import org.jgrapes.core.Component; -import org.jgrapes.core.Event; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.util.events.ConfigurationUpdate; -import org.jose4j.base64url.Base64; - -/** - * The properties of the display secret do not only depend on the - * VM definition, but also on events that occur during runtime. - * The reconciler for the display secret is therefore a separate - * component. - * - * The reconciler supports the following configuration properties: - * - * * `passwordValidity`: the validity of the random password in seconds. - * Used to calculate the password expiry time in the generated secret. - */ -public class DisplaySecretReconciler extends Component { - - protected final Logger logger = Logger.getLogger(getClass().getName()); - private int passwordValidity = 10; - private final List pendingPrepares - = Collections.synchronizedList(new LinkedList<>()); - - /** - * Instantiates a new display secret reconciler. - * - * @param componentChannel the component channel - */ - public DisplaySecretReconciler(Channel componentChannel) { - super(componentChannel); - } - - /** - * On configuration update. - * - * @param event the event - */ - @Handler - public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured(componentPath()) - // for backward compatibility - .or(() -> { - var oldConfig = event - .structured("/Manager/Controller/DisplaySecretMonitor"); - if (oldConfig.isPresent()) { - logger.warning(() -> "Using configuration with old " - + "path '/Manager/Controller/DisplaySecretMonitor' " - + "for `passwordValidity`, please update " - + "the configuration."); - } - return oldConfig; - }).ifPresent(c -> { - try { - Optional.ofNullable(c.get("passwordValidity")) - .map(p -> p instanceof Integer ? (Integer) p - : Integer.valueOf((String) p)) - .ifPresent(p -> { - passwordValidity = p; - }); - } catch (NumberFormatException e) { - logger.warning( - () -> "Malformed configuration: " + e.getMessage()); - } - }); - } - - /** - * Reconcile. If the configuration prevents generating a secret - * or the secret already exists, do nothing. Else generate a new - * secret with a random password and immediate expiration, thus - * preventing access to the display. - * - * @param vmDef the VM definition - * @param model the model - * @param channel the channel - * @param specChanged the spec changed - * @throws IOException Signals that an I/O exception has occurred. - * @throws TemplateException the template exception - * @throws ApiException the api exception - */ - public void reconcile(VmDefinition vmDef, Map model, - VmChannel channel, boolean specChanged) - throws IOException, TemplateException, ApiException { - // Nothing to do unless spec changed - if (!specChanged) { - return; - } - - // Secret needed at all? - var display = vmDef.fromVm("display").get(); - if (!DataPath. get(display, "spice", "generateSecret") - .orElse(true)) { - return; - } - - // Check if exists - ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME + "," - + "app.kubernetes.io/instance=" + vmDef.name()); - var stubs = K8sV1SecretStub.list(channel.client(), vmDef.namespace(), - options); - if (!stubs.isEmpty()) { - return; - } - - // Create secret - var secretName = vmDef.name() + "-" + DisplaySecret.NAME; - logger.fine(() -> "Create/update secret " + secretName); - var secret = new V1Secret(); - secret.setMetadata(new V1ObjectMeta().namespace(vmDef.namespace()) - .name(secretName) - .putLabelsItem("app.kubernetes.io/name", APP_NAME) - .putLabelsItem("app.kubernetes.io/component", DisplaySecret.NAME) - .putLabelsItem("app.kubernetes.io/instance", vmDef.name())); - secret.setType("Opaque"); - SecureRandom random = null; - try { - random = SecureRandom.getInstanceStrong(); - } catch (NoSuchAlgorithmException e) { // NOPMD - // "Every implementation of the Java platform is required - // to support at least one strong SecureRandom implementation." - } - byte[] bytes = new byte[16]; - random.nextBytes(bytes); - var password = Base64.encode(bytes); - secret.setStringData(Map.of(DisplaySecret.PASSWORD, password, - DisplaySecret.EXPIRY, "now")); - K8sV1SecretStub.create(channel.client(), secret); - } - - /** - * Prepares access to the console for the user from the event. - * Generates a new password and sends it to the runner. - * Requests the VM (via the runner) to login the user if specified - * in the event. - * - * @param event the event - * @param channel the channel - * @throws ApiException the api exception - */ - @Handler - public void onGetDisplaySecret(GetDisplaySecret event, VmChannel channel) - throws ApiException { - // Get VM definition and check if running - var vmStub = VmDefinitionStub.get(channel.client(), - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - event.vmDefinition().namespace(), event.vmDefinition().name()); - var vmDef = vmStub.model().orElse(null); - if (vmDef == null || !vmDef.conditionStatus("Running").orElse(false)) { - return; - } - - // Update console user in status - vmDef = vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.CONSOLE_USER, event.user()); - return status; - }).get(); - - // Get secret and update password in secret - var stub = getSecretStub(event, channel, vmDef); - if (stub == null) { - return; - } - var secret = stub.model().get(); - if (!updatePassword(secret, event)) { - return; - } - - // Register wait for confirmation (by VM status change, - // after secret update) - var pending = new PendingRequest(event, - event.vmDefinition().displayPasswordSerial().orElse(0L) + 1, - new CompletionLock(event, 1500)); - pendingPrepares.add(pending); - Event.onCompletion(event, e -> { - pendingPrepares.remove(pending); - }); - - // Update, will (eventually) trigger confirmation - stub.update(secret).getObject(); - } - - private K8sV1SecretStub getSecretStub(GetDisplaySecret event, - VmChannel channel, VmDefinition vmDef) throws ApiException { - // Look for secret - ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME + "," - + "app.kubernetes.io/instance=" + vmDef.name()); - var stubs = K8sV1SecretStub.list(channel.client(), vmDef.namespace(), - options); - if (stubs.isEmpty()) { - // No secret means no password for this VM wanted - event.setResult(null); - return null; - } - return stubs.iterator().next(); - } - - private boolean updatePassword(V1Secret secret, GetDisplaySecret event) { - var expiry = Optional.ofNullable(secret.getData() - .get(DisplaySecret.EXPIRY)).map(b -> new String(b)).orElse(null); - if (secret.getData().get(DisplaySecret.PASSWORD) != null - && stillValid(expiry)) { - // Fixed secret, don't touch - event.setResult( - new String(secret.getData().get(DisplaySecret.PASSWORD))); - return false; - } - - // Generate password and set expiry - SecureRandom random = null; - try { - random = SecureRandom.getInstanceStrong(); - } catch (NoSuchAlgorithmException e) { // NOPMD - // "Every implementation of the Java platform is required - // to support at least one strong SecureRandom implementation." - } - byte[] bytes = new byte[16]; - random.nextBytes(bytes); - var password = Base64.encode(bytes); - secret.setStringData(Map.of(DisplaySecret.PASSWORD, password, - DisplaySecret.EXPIRY, - Long.toString(Instant.now().getEpochSecond() + passwordValidity))); - event.setResult(password); - return true; - } - - private boolean stillValid(String expiry) { - if (expiry == null || "never".equals(expiry)) { - return true; - } - @SuppressWarnings({ "PMD.CloseResource", "resource" }) - var scanner = new Scanner(expiry); - if (!scanner.hasNextLong()) { - return false; - } - long expTime = scanner.nextLong(); - return expTime > Instant.now().getEpochSecond() + passwordValidity; - } - - /** - * On vm def changed. - * - * @param event the event - * @param channel the channel - */ - @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onVmResourceChanged(VmResourceChanged event, Channel channel) { - synchronized (pendingPrepares) { - String vmName = event.vmDefinition().name(); - for (var pending : pendingPrepares) { - if (pending.event.vmDefinition().name().equals(vmName) - && event.vmDefinition().displayPasswordSerial() - .map(s -> s >= pending.expectedSerial).orElse(false)) { - pending.lock.remove(); - // pending will be removed from pendingGest by - // waiting thread, see updatePassword - continue; - } - } - } - } - - /** - * The Class PendingGet. - */ - private static class PendingRequest { - public final GetDisplaySecret event; - public final long expectedSerial; - public final CompletionLock lock; - - /** - * Instantiates a new pending get. - * - * @param event the event - * @param expectedSerial the expected serial - */ - public PendingRequest(GetDisplaySecret event, long expectedSerial, - CompletionLock lock) { - super(); - this.event = event; - this.expectedSerial = expectedSerial; - this.lock = lock; - } - } -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java index a66b432..efa95f4 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java @@ -18,25 +18,24 @@ package org.jdrupes.vmoperator.manager; -import com.google.gson.Gson; +import com.google.gson.JsonObject; import freemarker.template.Configuration; import freemarker.template.TemplateException; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.models.V1APIService; import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import io.kubernetes.client.util.generic.dynamic.Dynamics; import java.io.IOException; import java.io.StringWriter; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.logging.Logger; -import org.jdrupes.vmoperator.common.K8sV1ServiceStub; -import org.jdrupes.vmoperator.common.VmDefinition; +import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.util.DataPath; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.util.GsonPtr; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; @@ -45,6 +44,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; /** * Delegee for reconciling the service */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") /* default */ class LoadBalancerReconciler { private static final String LOAD_BALANCER_SERVICE = "loadBalancerService"; @@ -68,47 +68,32 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; /** * Reconcile. * - * @param vmDef the VM definition + * @param event the event * @param model the model * @param channel the channel - * @param specChanged the spec changed * @throws IOException Signals that an I/O exception has occurred. * @throws TemplateException the template exception * @throws ApiException the api exception */ - public void reconcile(VmDefinition vmDef, Map model, - VmChannel channel, boolean specChanged) + public void reconcile(VmDefChanged event, + Map model, VmChannel channel) throws IOException, TemplateException, ApiException { - // Nothing to do unless spec changed - if (!specChanged) { - return; - } - // Check if to be generated - @SuppressWarnings({ "unchecked" }) - var lbsDef = Optional.of(model) + @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" }) + var lbs = Optional.of(model) .map(m -> (Map) m.get("reconciler")) .map(c -> c.get(LOAD_BALANCER_SERVICE)).orElse(Boolean.FALSE); - if (!(lbsDef instanceof Map) && !(lbsDef instanceof Boolean)) { + if (lbs instanceof Boolean isOn && !isOn) { + return; + } + if (!(lbs instanceof Map)) { logger.warning(() -> "\"" + LOAD_BALANCER_SERVICE + "\" in configuration must be boolean or mapping but is " - + lbsDef.getClass() + "."); - return; - } - if (lbsDef instanceof Boolean isOn && !isOn) { - return; - } - - // Load balancer can also be turned off for VM - if (vmDef - .>> fromSpec(LOAD_BALANCER_SERVICE) - .map(m -> m.isEmpty()).orElse(false)) { + + lbs.getClass() + "."); return; } // Combine template and data and parse result - logger.fine(() -> "Create/update load balancer service for " - + DataPath. get(model, "cr", "name").orElse("unknown")); var fmTemplate = fmConfig.getTemplate("runnerLoadBalancer.ftl.yaml"); StringWriter out = new StringWriter(); fmTemplate.process(model, out); @@ -116,78 +101,63 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; // https://github.com/kubernetes-client/java/issues/2741 var svcDef = Dynamics.newFromYaml( new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); - @SuppressWarnings("unchecked") - var defaults = lbsDef instanceof Map - ? (Map>) lbsDef - : null; - var client = channel.client(); - mergeMetadata(client.getJSON().getGson(), svcDef, defaults, vmDef); + mergeMetadata(svcDef, lbs, channel); // Apply - var svcStub = K8sV1ServiceStub - .get(client, vmDef.namespace(), vmDef.name()); - if (svcStub.apply(svcDef).isEmpty()) { - logger.warning( - () -> "Could not patch service for " + svcStub.name()); - } + DynamicKubernetesApi svcApi = new DynamicKubernetesApi("", "v1", + "services", channel.client()); + K8s.apply(svcApi, svcDef, svcDef.getRaw().toString()); } - private void mergeMetadata(Gson gson, DynamicKubernetesObject svcDef, - Map> defaults, - VmDefinition vmDefinition) { - // Get specific load balancer metadata from VM definition - var vmLbMeta = vmDefinition - .>> fromSpec(LOAD_BALANCER_SERVICE) - .orElse(Collections.emptyMap()); + @SuppressWarnings("unchecked") + private void mergeMetadata(DynamicKubernetesObject svcDef, + Object lbsConfig, VmChannel channel) { + // Get metadata from config + Map asmData = Collections.emptyMap(); + if (lbsConfig instanceof Map config) { + asmData = (Map) config; + } + var json = channel.client().getJSON(); + JsonObject cfgMeta + = json.deserialize(json.serialize(asmData), JsonObject.class); - // Merge - var svcMeta = svcDef.getMetadata(); - var svcJsonMeta = GsonPtr.to(svcDef.getRaw()).to(METADATA); - Optional.ofNullable(mergeIfAbsent(svcMeta.getLabels(), - mergeReplace(defaults.get(LABELS), vmLbMeta.get(LABELS)))) - .ifPresent(lbls -> svcJsonMeta.set(LABELS, gson.toJsonTree(lbls))); - Optional.ofNullable(mergeIfAbsent(svcMeta.getAnnotations(), - mergeReplace(defaults.get(ANNOTATIONS), vmLbMeta.get(ANNOTATIONS)))) - .ifPresent(as -> svcJsonMeta.set(ANNOTATIONS, gson.toJsonTree(as))); + // Get metadata from VM definition + var vmMeta = GsonPtr.to(channel.vmDefinition().getRaw()).to("spec") + .get(JsonObject.class, LOAD_BALANCER_SERVICE) + .map(JsonObject::deepCopy).orElseGet(() -> new JsonObject()); + + // Merge Data from VM definition into config data + mergeReplace(GsonPtr.to(cfgMeta).to(LABELS).get(JsonObject.class), + GsonPtr.to(vmMeta).to(LABELS).get(JsonObject.class)); + mergeReplace( + GsonPtr.to(cfgMeta).to(ANNOTATIONS).get(JsonObject.class), + GsonPtr.to(vmMeta).to(ANNOTATIONS).get(JsonObject.class)); + + // Merge additional data into service definition + var svcMeta = GsonPtr.to(svcDef.getRaw()).to(METADATA); + mergeIfAbsent(svcMeta.to(LABELS).get(JsonObject.class), + GsonPtr.to(cfgMeta).to(LABELS).get(JsonObject.class)); + mergeIfAbsent(svcMeta.to(ANNOTATIONS).get(JsonObject.class), + GsonPtr.to(cfgMeta).to(ANNOTATIONS).get(JsonObject.class)); } - private Map mergeReplace(Map dest, - Map src) { - if (src == null) { - return dest; - } - if (dest == null) { - dest = new LinkedHashMap<>(); - } else { - dest = new LinkedHashMap<>(dest); - } + private void mergeReplace(JsonObject dest, JsonObject src) { for (var e : src.entrySet()) { - if (e.getValue() == null) { + if (e.getValue().isJsonNull()) { dest.remove(e.getKey()); continue; } - dest.put(e.getKey(), e.getValue()); + dest.add(e.getKey(), e.getValue()); } - return dest; } - private Map mergeIfAbsent(Map dest, - Map src) { - if (src == null) { - return dest; - } - if (dest == null) { - dest = new LinkedHashMap<>(); - } else { - dest = new LinkedHashMap<>(dest); - } + private void mergeIfAbsent(JsonObject dest, JsonObject src) { for (var e : src.entrySet()) { - if (dest.containsKey(e.getKey())) { + if (dest.has(e.getKey())) { continue; } - dest.put(e.getKey(), e.getValue()); + dest.add(e.getKey(), e.getValue()); } - return dest; } } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java index f431c9d..8c8e0fd 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java @@ -50,23 +50,19 @@ import org.jgrapes.core.NamedChannel; import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.events.HandlingError; import org.jgrapes.core.events.Stop; -import org.jgrapes.http.HttpConnector; import org.jgrapes.http.HttpServer; import org.jgrapes.http.InMemorySessionManager; import org.jgrapes.http.LanguageSelector; import org.jgrapes.http.events.Request; import org.jgrapes.io.NioDispatcher; import org.jgrapes.io.util.PermitsPool; -import org.jgrapes.net.SocketConnector; import org.jgrapes.net.SocketServer; -import org.jgrapes.net.SslCodec; import org.jgrapes.util.ComponentCollector; import org.jgrapes.util.FileSystemWatcher; import org.jgrapes.util.YamlConfigurationStore; import org.jgrapes.util.events.ConfigurationUpdate; import org.jgrapes.util.events.WatchFile; -import org.jgrapes.webconlet.oidclogin.LoginConlet; -import org.jgrapes.webconlet.oidclogin.OidcClient; +import org.jgrapes.webconlet.locallogin.LoginConlet; import org.jgrapes.webconsole.base.BrowserLocalBackedKVStore; import org.jgrapes.webconsole.base.ConletComponentFactory; import org.jgrapes.webconsole.base.ConsoleWeblet; @@ -75,13 +71,12 @@ import org.jgrapes.webconsole.base.PageResourceProviderFactory; import org.jgrapes.webconsole.base.WebConsole; import org.jgrapes.webconsole.rbac.RoleConfigurator; import org.jgrapes.webconsole.rbac.RoleConletFilter; -import org.jgrapes.webconsole.rbac.UserLogger; import org.jgrapes.webconsole.vuejs.VueJsConsoleWeblet; /** * The application class. */ -@SuppressWarnings({ "PMD.ExcessiveImports" }) +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" }) public class Manager extends Component { private static String version; @@ -95,11 +90,10 @@ public class Manager extends Component { * @param cmdLine * * @throws IOException Signals that an I/O exception has occurred. - * @throws URISyntaxException */ - @SuppressWarnings({ "PMD.NcssCount", - "PMD.ConstructorCallsOverridableMethod" }) - public Manager(CommandLine cmdLine) throws IOException, URISyntaxException { + @SuppressWarnings({ "PMD.TooFewBranchesForASwitchStatement", + "PMD.NcssCount" }) + public Manager(CommandLine cmdLine) throws IOException { super(new NamedChannel("manager")); // Prepare component tree attach(new NioDispatcher()); @@ -126,19 +120,9 @@ public class Manager extends Component { .setServerAddress(new InetSocketAddress(8080)) .setName("GuiSocketServer")); - // Channel for HTTP application layer - Channel httpChannel = new NamedChannel("guiHttp"); - - // Create network channels for client requests. - Channel requestChannel = attach(new SocketConnector(SELF)); - Channel secReqChannel - = attach(new SslCodec(SELF, requestChannel, true)); - // Support for making HTTP requests - attach(new HttpConnector(httpChannel, requestChannel, - secReqChannel)); - // Create an HTTP server as converter between transport and application // layer. + Channel httpChannel = new NamedChannel("guiHttp"); HttpServer guiHttpServer = attach(new HttpServer(httpChannel, httpTransport, Request.In.Get.class, Request.In.Post.class)); guiHttpServer.setName("GuiHttpServer"); @@ -154,7 +138,7 @@ public class Manager extends Component { return; } ConsoleWeblet consoleWeblet = guiHttpServer - .attach(new VueJsConsoleWeblet(httpChannel, SELF, rootUri) { + .attach(new VueJsConsoleWeblet(httpChannel, Channel.SELF, rootUri) { @Override protected Map createConsoleBaseModel() { return augmentBaseModel(super.createConsoleBaseModel()); @@ -172,14 +156,9 @@ public class Manager extends Component { console.attach(new RoleConfigurator(console.channel())); console.attach(new RoleConletFilter(console.channel())); console.attach(new LoginConlet(console.channel())); - console.attach(new OidcClient(console.channel(), httpChannel, - httpChannel, new URI("/oauth/callback"), 1500)); - console.attach(new UserLogger(console.channel())); - // Add all available page resource providers console.attach(new ComponentCollector<>( PageResourceProviderFactory.class, console.channel())); - // Add all available conlets console.attach(new ComponentCollector<>( ConletComponentFactory.class, console.channel(), type -> { @@ -217,6 +196,7 @@ public class Manager extends Component { * @param event the event */ @Handler + @SuppressWarnings("PMD.DataflowAnomalyAnalysis") public void onConfigurationUpdate(ConfigurationUpdate event) { event.structured(componentPath()).ifPresent(c -> { if (c.containsKey("clusterName")) { @@ -263,7 +243,7 @@ public class Manager extends Component { */ @Handler(priority = -1000) public void onStop(Stop event) { - logger.info(() -> "Application stopped."); + logger.fine(() -> "Application stopped."); } static { @@ -290,6 +270,7 @@ public class Manager extends Component { * @param args the arguments * @throws Exception the exception */ + @SuppressWarnings("PMD.SignatureDeclareThrowsException") public static void main(String[] args) { try { // Instance logger is not available yet. @@ -318,7 +299,7 @@ public class Manager extends Component { try { app.fire(new Stop()); Components.awaitExhaustion(); - } catch (InterruptedException e) { // NOPMD + } catch (InterruptedException e) { // Cannot do anything about this. } })); @@ -329,7 +310,7 @@ public class Manager extends Component { // Wait for (regular) termination Components.awaitExhaustion(); System.exit(exitStatus); - } catch (IOException | InterruptedException | URISyntaxException + } catch (IOException | InterruptedException | org.apache.commons.cli.ParseException e) { Logger.getLogger(Manager.class.getName()).log(Level.SEVERE, e, () -> "Failed to start manager: " + e.getMessage()); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodMonitor.java deleted file mode 100644 index cfb49e5..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodMonitor.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1Pod; -import io.kubernetes.client.openapi.models.V1PodList; -import io.kubernetes.client.util.Watch.Response; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.io.IOException; -import java.time.Duration; -import java.time.Instant; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; -import org.jdrupes.vmoperator.common.K8sV1PodStub; -import org.jdrupes.vmoperator.manager.events.ChannelDictionary; -import org.jdrupes.vmoperator.manager.events.PodChanged; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; -import org.jgrapes.core.Channel; -import org.jgrapes.core.annotation.Handler; - -/** - * Watches for changes of pods that run VMs. - */ -public class PodMonitor extends AbstractMonitor { - - private final ChannelDictionary channelDictionary; - - private final Map pendingChanges - = new ConcurrentHashMap<>(); - - /** - * Instantiates a new pod monitor. - * - * @param componentChannel the component channel - * @param channelDictionary the channel dictionary - */ - public PodMonitor(Channel componentChannel, - ChannelDictionary channelDictionary) { - super(componentChannel, V1Pod.class, V1PodList.class); - this.channelDictionary = channelDictionary; - context(K8sV1PodStub.CONTEXT); - ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + APP_NAME + "," - + "app.kubernetes.io/managed-by=" + VM_OP_NAME); - options(options); - } - - @Override - protected void prepareMonitoring() throws IOException, ApiException { - client(new K8sClient()); - } - - @Override - protected void handleChange(K8sClient client, Response change) { - String vmName = change.object.getMetadata().getLabels() - .get("app.kubernetes.io/instance"); - if (vmName == null) { - return; - } - var channel = channelDictionary.channel(vmName).orElse(null); - var responseType = ResponseType.valueOf(change.type); - if (channel != null && channel.vmDefinition() != null) { - pendingChanges.remove(vmName); - channel.fire(new PodChanged(change.object, responseType)); - return; - } - - // VM definition not available yet, may happen during startup - if (responseType == ResponseType.DELETED) { - return; - } - purgePendingChanges(); - logger.finer(() -> "Add pending pod change for " + vmName); - pendingChanges.put(vmName, new PendingChange(Instant.now(), change)); - } - - private void purgePendingChanges() { - Instant tooOld = Instant.now().minus(Duration.ofMinutes(15)); - for (var itr = pendingChanges.entrySet().iterator(); itr.hasNext();) { - var change = itr.next(); - if (change.getValue().from().isBefore(tooOld)) { - itr.remove(); - logger.finer( - () -> "Cleaned pending pod change for " + change.getKey()); - } - } - } - - /** - * Check for pending changes. - * - * @param event the event - * @param channel the channel - */ - @Handler - public void onVmResourceChanged(VmResourceChanged event, - VmChannel channel) { - Optional.ofNullable(pendingChanges.remove(event.vmDefinition().name())) - .map(PendingChange::change).ifPresent(change -> { - logger.finer(() -> "Firing pending pod change for " - + event.vmDefinition().name()); - channel.fire(new PodChanged(change.object, - ResponseType.valueOf(change.type))); - if (logger.isLoggable(Level.FINER) - && pendingChanges.isEmpty()) { - logger.finer("No pending pod changes left."); - } - }); - } - - private record PendingChange(Instant from, Response change) { - } - -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodReconciler.java deleted file mode 100644 index 4733e73..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodReconciler.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import freemarker.template.Configuration; -import freemarker.template.TemplateException; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.generic.dynamic.Dynamics; -import io.kubernetes.client.util.generic.options.ListOptions; -import io.kubernetes.client.util.generic.options.PatchOptions; -import java.io.IOException; -import java.io.StringWriter; -import java.util.Map; -import java.util.logging.Logger; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sV1PodStub; -import org.jdrupes.vmoperator.common.K8sV1SecretStub; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinition.RequestedVmState; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.SafeConstructor; - -/** - * Delegee for reconciling the pod. - */ -/* default */ class PodReconciler { - - protected final Logger logger = Logger.getLogger(getClass().getName()); - private final Configuration fmConfig; - - /** - * Instantiates a new pod reconciler. - * - * @param fmConfig the fm config - */ - public PodReconciler(Configuration fmConfig) { - this.fmConfig = fmConfig; - } - - /** - * Reconcile the pod. - * - * @param vmDef the vm def - * @param model the model - * @param channel the channel - * @param specChanged the spec changed - * @throws IOException Signals that an I/O exception has occurred. - * @throws TemplateException the template exception - * @throws ApiException the api exception - */ - public void reconcile(VmDefinition vmDef, Map model, - VmChannel channel, boolean specChanged) - throws IOException, TemplateException, ApiException { - // Get pod stub. - var podStub = K8sV1PodStub.get(channel.client(), vmDef.namespace(), - vmDef.name()); - - // Nothing to do if exists and should be running - if (vmDef.vmState() == RequestedVmState.RUNNING - && podStub.model().isPresent()) { - return; - } - - // Delete if running but should be stopped - if (vmDef.vmState() == RequestedVmState.STOPPED) { - if (podStub.model().isPresent()) { - podStub.delete(); - } - return; - } - - // Create pod. First combine template and data and parse result - logger.fine(() -> "Create/update pod " + podStub.name()); - addDisplaySecret(channel.client(), model, vmDef); - var fmTemplate = fmConfig.getTemplate("runnerPod.ftl.yaml"); - StringWriter out = new StringWriter(); - fmTemplate.process(model, out); - // Avoid Yaml.load due to - // https://github.com/kubernetes-client/java/issues/2741 - var podDef = Dynamics.newFromYaml( - new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); - - // Do apply changes - PatchOptions opts = new PatchOptions(); - opts.setForce(true); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - if (podStub.apply(podDef).isEmpty()) { - logger.warning( - () -> "Could not patch pod for " + podStub.name()); - } - } - - private void addDisplaySecret(K8sClient client, Map model, - VmDefinition vmDef) throws ApiException { - ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME + "," - + "app.kubernetes.io/instance=" + vmDef.name()); - var dsStub = K8sV1SecretStub - .list(client, vmDef.namespace(), options).stream().findFirst(); - if (dsStub.isPresent()) { - dsStub.get().model().ifPresent(m -> { - model.put("displaySecret", m.getMetadata().getName()); - }); - } - } - -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java deleted file mode 100644 index e554d5a..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023,2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import com.google.gson.JsonObject; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.Watch; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sDynamicModel; -import org.jdrupes.vmoperator.common.K8sDynamicModels; -import org.jdrupes.vmoperator.common.K8sDynamicStub; -import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; -import org.jdrupes.vmoperator.common.VmDefinition.Assignment; -import org.jdrupes.vmoperator.common.VmDefinitionStub; -import org.jdrupes.vmoperator.common.VmPool; -import org.jdrupes.vmoperator.manager.events.GetPools; -import org.jdrupes.vmoperator.manager.events.VmPoolChanged; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; -import org.jdrupes.vmoperator.util.GsonPtr; -import org.jgrapes.core.Channel; -import org.jgrapes.core.EventPipeline; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Attached; - -/** - * Watches for changes of VM pools. Reports the changes using - * {@link VmPoolChanged} events fired on a special pipeline to - * avoid concurrent change informations. - */ -public class PoolMonitor extends - AbstractMonitor { - - private final Map pools = new ConcurrentHashMap<>(); - private EventPipeline poolPipeline; - - /** - * Instantiates a new VM pool manager. - * - * @param componentChannel the component channel - */ - public PoolMonitor(Channel componentChannel) { - super(componentChannel, K8sDynamicModel.class, - K8sDynamicModels.class); - } - - /** - * On attached. - * - * @param event the event - */ - @Handler - @SuppressWarnings("PMD.CompareObjectsWithEquals") - public void onAttached(Attached event) { - if (event.node() == this) { - poolPipeline = newEventPipeline(); - } - } - - @Override - protected void prepareMonitoring() throws IOException, ApiException { - client(new K8sClient()); - - // Get all our API versions - var ctx = K8s.context(client(), Crd.GROUP, "", Crd.KIND_VM_POOL); - if (ctx.isEmpty()) { - logger.severe(() -> "Cannot get CRD context."); - return; - } - context(ctx.get()); - } - - @Override - protected void handleChange(K8sClient client, - Watch.Response response) { - - var type = ResponseType.valueOf(response.type); - var poolName = response.object.metadata().getName(); - - // When pool is deleted, save VMs in pending - if (type == ResponseType.DELETED) { - Optional.ofNullable(pools.get(poolName)).ifPresent(pool -> { - pool.setUndefined(); - if (pool.vms().isEmpty()) { - pools.remove(poolName); - } - poolPipeline.fire(new VmPoolChanged(pool, true)); - }); - return; - } - - // Get full definition - var poolModel = response.object; - if (poolModel.data() == null) { - // ADDED event does not provide data, see - // https://github.com/kubernetes-client/java/issues/3215 - try { - poolModel = K8sDynamicStub.get(client(), context(), namespace(), - poolModel.metadata().getName()).model().orElse(null); - } catch (ApiException e) { - return; - } - } - - // Get pool and merge changes - var vmPool = pools.computeIfAbsent(poolName, k -> new VmPool(poolName)); - vmPool.defineFrom(client().getJSON().getGson().fromJson( - GsonPtr.to(poolModel.data()).to("spec").get(), VmPool.class)); - poolPipeline.fire(new VmPoolChanged(vmPool)); - } - - /** - * Track VM definition changes. - * - * @param event the event - * @throws ApiException - */ - @Handler - public void onVmResourceChanged(VmResourceChanged event) - throws ApiException { - final var vmDef = event.vmDefinition(); - final String vmName = vmDef.name(); - switch (event.type()) { - case ADDED: - vmDef.> fromSpec("pools") - .orElse(Collections.emptyList()).stream().forEach(p -> { - pools.computeIfAbsent(p, k -> new VmPool(p)) - .vms().add(vmName); - poolPipeline.fire(new VmPoolChanged(pools.get(p))); - }); - break; - case DELETED: - pools.values().stream().forEach(p -> { - if (p.vms().remove(vmName)) { - poolPipeline.fire(new VmPoolChanged(p)); - } - }); - return; - default: - break; - } - - // Sync last usage to console state change if user matches - if (vmDef.assignment().map(Assignment::user) - .map(at -> at.equals(vmDef.consoleUser().orElse(null))) - .orElse(true)) { - return; - } - - var ccChange = vmDef.condition("ConsoleConnected") - .map(cc -> cc.getLastTransitionTime().toInstant()); - if (ccChange - .map(tt -> vmDef.assignment().map(Assignment::lastUsed) - .map(alu -> alu.isAfter(tt)).orElse(true)) - .orElse(true)) { - return; - } - var vmStub = VmDefinitionStub.get(client(), - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - vmDef.namespace(), vmDef.name()); - vmStub.updateStatus(from -> { - // TODO - JsonObject status = from.statusJson(); - var assignment = GsonPtr.to(status).to(Status.ASSIGNMENT); - assignment.set("lastUsed", ccChange.get().toString()); - return status; - }); - } - - /** - * Return the requested pools. - * - * @param event the event - */ - @Handler - public void onGetPools(GetPools event) { - event.setResult(pools.values().stream().filter(VmPool::isDefined) - .filter(p -> event.name().isEmpty() - || p.name().equals(event.name().get())) - .filter(p -> event.forUser().isEmpty() && event.forRoles().isEmpty() - || !p.permissionsFor(event.forUser().orElse(null), - event.forRoles()).isEmpty()) - .toList()); - } -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PvcReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PvcReconciler.java deleted file mode 100644 index 515bfc9..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PvcReconciler.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import freemarker.core.ParseException; -import freemarker.template.Configuration; -import freemarker.template.MalformedTemplateNameException; -import freemarker.template.TemplateException; -import freemarker.template.TemplateNotFoundException; -import io.kubernetes.client.custom.V1Patch; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.generic.dynamic.Dynamics; -import io.kubernetes.client.util.generic.options.ListOptions; -import io.kubernetes.client.util.generic.options.PatchOptions; -import java.io.IOException; -import java.io.StringWriter; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; -import org.jdrupes.vmoperator.common.K8sV1PvcStub; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.util.DataPath; -import org.jdrupes.vmoperator.util.GsonPtr; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.SafeConstructor; - -/** - * Delegee for reconciling the stateful set (effectively the pod). - */ -/* default */ class PvcReconciler { - - protected final Logger logger = Logger.getLogger(getClass().getName()); - private final Configuration fmConfig; - - /** - * Instantiates a new pvc reconciler. - * - * @param fmConfig the fm config - */ - public PvcReconciler(Configuration fmConfig) { - this.fmConfig = fmConfig; - } - - /** - * Reconcile the PVCs. - * - * @param vmDef the VM definition - * @param model the model - * @param channel the channel - * @param specChanged the spec changed - * @throws IOException Signals that an I/O exception has occurred. - * @throws TemplateException the template exception - * @throws ApiException the api exception - */ - @SuppressWarnings({ "unchecked" }) - public void reconcile(VmDefinition vmDef, Map model, - VmChannel channel, boolean specChanged) - throws IOException, TemplateException, ApiException { - Set knownPvcs; - if (!specChanged && channel.associated(this, Set.class).isPresent()) { - knownPvcs = (Set) channel.associated(this, Set.class).get(); - } else { - ListOptions listOpts = new ListOptions(); - listOpts.setLabelSelector( - "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," - + "app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + vmDef.name()); - knownPvcs = K8sV1PvcStub.list(channel.client(), - vmDef.namespace(), listOpts).stream().map(K8sV1PvcStub::name) - .collect(Collectors.toSet()); - channel.setAssociated(this, knownPvcs); - } - - // Reconcile runner data pvc - reconcileRunnerDataPvc(vmDef, model, channel, knownPvcs, specChanged); - - // Reconcile pvcs for defined disks - var diskDefs = vmDef.>> fromVm("disks") - .orElse(List.of()); - var diskCounter = 0; - for (var diskDef : diskDefs) { - if (!diskDef.containsKey("volumeClaimTemplate")) { - continue; - } - var diskName = DataPath.get(diskDef, "volumeClaimTemplate", - "metadata", "name").map(name -> name + "-disk") - .orElse("disk-" + diskCounter); - diskCounter += 1; - diskDef.put("generatedDiskName", diskName); - - // Don't do anything if pvc with old (sts generated) name exists. - var stsDiskPvcName = diskName + "-" + vmDef.name() + "-0"; - if (knownPvcs.contains(stsDiskPvcName)) { - diskDef.put("generatedPvcName", stsDiskPvcName); - continue; - } - - // Update PVC - reconcileRunnerDiskPvc(vmDef, model, channel, specChanged, diskDef); - } - } - - private void reconcileRunnerDataPvc(VmDefinition vmDef, - Map model, VmChannel channel, - Set knownPvcs, boolean specChanged) - throws TemplateNotFoundException, MalformedTemplateNameException, - ParseException, IOException, TemplateException, ApiException { - - // Look for old (sts generated) name. - var stsRunnerDataPvcName - = "runner-data" + "-" + vmDef.name() + "-0"; - if (knownPvcs.contains(stsRunnerDataPvcName)) { - model.put("runnerDataPvcName", stsRunnerDataPvcName); - return; - } - - // Generate PVC - var runnerDataPvcName = vmDef.name() + "-runner-data"; - logger.fine(() -> "Create/update pvc " + runnerDataPvcName); - model.put("runnerDataPvcName", runnerDataPvcName); - if (!specChanged) { - // Augmenting the model is all we have to do - return; - } - var fmTemplate = fmConfig.getTemplate("runnerDataPvc.ftl.yaml"); - StringWriter out = new StringWriter(); - fmTemplate.process(model, out); - // Avoid Yaml.load due to - // https://github.com/kubernetes-client/java/issues/2741 - var pvcDef = Dynamics.newFromYaml( - new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); - - // Do apply changes - var pvcStub = K8sV1PvcStub.get(channel.client(), - vmDef.namespace(), (String) model.get("runnerDataPvcName")); - PatchOptions opts = new PatchOptions(); - opts.setForce(true); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - if (pvcStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML, - new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts) - .isEmpty()) { - logger.warning( - () -> "Could not patch pvc for " + pvcStub.name()); - } - } - - private void reconcileRunnerDiskPvc(VmDefinition vmDef, - Map model, VmChannel channel, boolean specChanged, - Map diskDef) - throws TemplateNotFoundException, MalformedTemplateNameException, - ParseException, IOException, TemplateException, ApiException { - // Generate PVC - var pvcName = vmDef.name() + "-" + diskDef.get("generatedDiskName"); - diskDef.put("generatedPvcName", pvcName); - if (!specChanged) { - // Augmenting the model is all we have to do - return; - } - - // Generate PVC - logger.fine(() -> "Create/update pvc " + pvcName); - model.put("disk", diskDef); - var fmTemplate = fmConfig.getTemplate("runnerDiskPvc.ftl.yaml"); - StringWriter out = new StringWriter(); - fmTemplate.process(model, out); - model.remove("disk"); - // Avoid Yaml.load due to - // https://github.com/kubernetes-client/java/issues/2741 - var pvcDef = Dynamics.newFromYaml( - new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); - - // Apply changes - var pvcStub - = K8sV1PvcStub.get(channel.client(), vmDef.namespace(), pvcName); - var pvc = pvcStub.model(); - if (pvc.isEmpty() - || !"Bound".equals(pvc.get().getStatus().getPhase())) { - // Does not exist or isn't bound, use apply - PatchOptions opts = new PatchOptions(); - opts.setForce(true); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - if (pvcStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML, - new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts) - .isEmpty()) { - logger.warning( - () -> "Could not patch pvc for " + pvcStub.name()); - } - return; - } - - // If bound, use json merge, omitting immutable fields - var spec = GsonPtr.to(pvcDef.getRaw()).to("spec"); - spec.removeExcept("volumeAttributesClassName", "resources"); - spec.get("resources").ifPresent(p -> p.removeExcept("requests")); - PatchOptions opts = new PatchOptions(); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - if (pvcStub.patch(V1Patch.PATCH_FORMAT_JSON_MERGE_PATCH, - new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts) - .isEmpty()) { - logger.warning( - () -> "Could not patch pvc for " + pvcStub.name()); - } - } -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java index e580c48..2adb843 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java @@ -18,40 +18,37 @@ package org.jdrupes.vmoperator.manager; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import freemarker.template.AdapterTemplateModel; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import freemarker.core.ParseException; import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapperBuilder; +import freemarker.template.MalformedTemplateNameException; import freemarker.template.SimpleNumber; -import freemarker.template.SimpleScalar; import freemarker.template.TemplateException; import freemarker.template.TemplateExceptionHandler; +import freemarker.template.TemplateHashModel; import freemarker.template.TemplateMethodModelEx; import freemarker.template.TemplateModelException; +import freemarker.template.TemplateNotFoundException; import io.kubernetes.client.custom.Quantity; import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import java.io.IOException; -import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.logging.Level; import org.jdrupes.vmoperator.common.Convertions; -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinition.Assignment; -import org.jdrupes.vmoperator.common.VmPool; -import org.jdrupes.vmoperator.manager.events.GetPools; -import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; +import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type; import org.jdrupes.vmoperator.util.ExtendedObjectWrapper; +import org.jdrupes.vmoperator.util.GsonPtr; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; import org.jgrapes.core.annotation.Handler; @@ -68,25 +65,20 @@ import org.jgrapes.util.events.ConfigurationUpdate; * * * A [`ConfigMap`](https://kubernetes.io/docs/concepts/configuration/configmap/) * that defines the configuration file for the runner. - * - * * A [`PVC`](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) - * for 1 MiB of persistent storage used by the Runner (referred to as the - * "runnerDataPvc") - * - * * The PVCs for the VM's disks. - * - * * A [`Pod`](https://kubernetes.io/docs/concepts/workloads/pods/) with the - * runner instance[^oldSts]. - * + * + * * A [`StatefulSet`](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) + * that creates + * * the [`Pod`](https://kubernetes.io/docs/concepts/workloads/pods/) + * with the Runner instance, + * * a PVC for 1 MiB of persistent storage used by the Runner + * (referred to as the "runnerDataPvc") and + * * the PVCs for the VM's disks. + * * * (Optional) A load balancer * [`Service`](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/) * that allows the user to access a VM's console without knowing which * node it runs on. * - * [^oldSts]: Before version 3.4, the operator created a - * [`StatefulSet`](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) - * that created the pod. - * * The reconciler is part of the {@link Controller} component. It's * configuration properties are therefore defined in * ```yaml @@ -129,26 +121,15 @@ import org.jgrapes.util.events.ConfigurationUpdate; * ``` * This makes all VM consoles available at IP address 192.168.168.1 * with the port numbers from the VM definitions. - * - * * `loggingProperties`: If defined, specifies the default logging - * properties to be used by the runners managed by the controller. - * This property is a string that holds the content of - * a logging.properties file. - * - * @see org.jdrupes.vmoperator.manager.DisplaySecretReconciler */ -@SuppressWarnings({ "PMD.AvoidDuplicateLiterals" }) +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", + "PMD.AvoidDuplicateLiterals" }) public class Reconciler extends Component { - /** The Constant mapper. */ - @SuppressWarnings("PMD.FieldNamingConventions") - protected static final ObjectMapper mapper = new ObjectMapper(); - + @SuppressWarnings("PMD.SingularField") private final Configuration fmConfig; private final ConfigMapReconciler cmReconciler; - private final DisplaySecretReconciler dsReconciler; - private final PvcReconciler pvcReconciler; - private final PodReconciler podReconciler; + private final StatefulSetReconciler stsReconciler; private final LoadBalancerReconciler lbReconciler; @SuppressWarnings("PMD.UseConcurrentHashMap") private final Map config = new HashMap<>(); @@ -158,7 +139,6 @@ public class Reconciler extends Component { * * @param componentChannel the component channel */ - @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") public Reconciler(Channel componentChannel) { super(componentChannel); @@ -173,9 +153,7 @@ public class Reconciler extends Component { fmConfig.setClassForTemplateLoading(Reconciler.class, ""); cmReconciler = new ConfigMapReconciler(fmConfig); - dsReconciler = attach(new DisplaySecretReconciler(componentChannel)); - pvcReconciler = new PvcReconciler(fmConfig); - podReconciler = new PodReconciler(fmConfig); + stsReconciler = new StatefulSetReconciler(fmConfig); lbReconciler = new LoadBalancerReconciler(fmConfig); } @@ -197,131 +175,117 @@ public class Reconciler extends Component { * @param event the event * @param channel the channel * @throws ApiException the api exception - * @throws TemplateException the template exception - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onVmResourceChanged(VmResourceChanged event, VmChannel channel) - throws ApiException, TemplateException, IOException { - // Ownership relationships takes care of deletions - if (event.type() == K8sObserver.ResponseType.DELETED) { - return; - } - - // Create model for processing templates - var vmDef = event.vmDefinition(); - Map model = prepareModel(vmDef); - cmReconciler.reconcile(model, channel, event.specChanged()); - - // The remaining reconcilers depend only on changes of the spec part - // or the pod state. - if (!event.specChanged() && !event.podChanged()) { - return; - } - dsReconciler.reconcile(vmDef, model, channel, event.specChanged()); - pvcReconciler.reconcile(vmDef, model, channel, event.specChanged()); - podReconciler.reconcile(vmDef, model, channel, event.specChanged()); - lbReconciler.reconcile(vmDef, model, channel, event.specChanged()); - } - - /** - * Reset the VM by incrementing the reset count and doing a - * partial reconcile (configmap only). - * - * @param event the event - * @param channel the channel * @throws IOException - * @throws ApiException + * @throws ParseException + * @throws MalformedTemplateNameException + * @throws TemplateNotFoundException * @throws TemplateException + * @throws KubectlException */ @Handler - public void onResetVm(ResetVm event, VmChannel channel) - throws ApiException, IOException, TemplateException { - var vmDef = channel.vmDefinition(); - var extra = vmDef.extra(); - extra.resetCount(extra.resetCount() + 1); - Map model - = prepareModel(channel.vmDefinition()); - cmReconciler.reconcile(model, channel, true); + @SuppressWarnings("PMD.ConfusingTernary") + public void onVmDefChanged(VmDefChanged event, VmChannel channel) + throws ApiException, TemplateException, IOException { + // We're only interested in "spec" changes. + if (!event.specChanged()) { + return; + } + + // Ownership relationships takes care of deletions + var defMeta = event.vmDefinition().getMetadata(); + if (event.type() == Type.DELETED) { + logger.fine(() -> "VM \"" + defMeta.getName() + "\" deleted"); + return; + } + + // Reconcile, use "augmented" vm definition for model + Map model = prepareModel(patchCr(event.vmDefinition())); + var configMap = cmReconciler.reconcile(event, model, channel); + model.put("cm", configMap.getRaw()); + stsReconciler.reconcile(event, model, channel); + lbReconciler.reconcile(event, model, channel); } - private Map prepareModel(VmDefinition vmDef) - throws TemplateModelException, ApiException { + private DynamicKubernetesObject patchCr(DynamicKubernetesObject vmDef) { + var json = vmDef.getRaw().deepCopy(); + // Adjust cdromImage path + adjustCdRomPaths(json); + + // Adjust cloud-init data + adjustCloudInitData(json); + + return new DynamicKubernetesObject(json); + } + + private void adjustCdRomPaths(JsonObject json) { + var disks + = GsonPtr.to(json).to("spec", "vm", "disks").get(JsonArray.class); + for (var disk : disks) { + var cdrom = (JsonObject) ((JsonObject) disk).get("cdrom"); + if (cdrom == null) { + continue; + } + String image = cdrom.get("image").getAsString(); + if (image.isEmpty()) { + continue; + } + try { + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var imageUri = new URI("file://" + Constants.IMAGE_REPO_PATH + + "/").resolve(image); + if ("file".equals(imageUri.getScheme())) { + cdrom.addProperty("image", imageUri.getPath()); + } else { + cdrom.addProperty("image", imageUri.toString()); + } + } catch (URISyntaxException e) { + logger.warning(() -> "Invalid CDROM image: " + image); + } + } + } + + private void adjustCloudInitData(JsonObject json) { + var spec = GsonPtr.to(json).to("spec").get(JsonObject.class); + if (!spec.has("cloudInit")) { + return; + } + var metaData = GsonPtr.to(spec).to("cloudInit", "metaData"); + if (metaData.getAsString("instance-id").isEmpty()) { + metaData.set("instance-id", + GsonPtr.to(json).getAsString("metadata", "resourceVersion") + .map(s -> "v" + s).orElse("v1")); + } + if (metaData.getAsString("local-hostname").isEmpty()) { + metaData.set("local-hostname", + GsonPtr.to(json).getAsString("metadata", "name").get()); + } + } + + @SuppressWarnings("PMD.CognitiveComplexity") + private Map prepareModel(DynamicKubernetesObject vmDef) + throws TemplateModelException { @SuppressWarnings("PMD.UseConcurrentHashMap") Map model = new HashMap<>(); model.put("managerVersion", Optional.ofNullable(Reconciler.class.getPackage() .getImplementationVersion()).orElse("(Unknown)")); - model.put("cr", vmDef); + model.put("cr", vmDef.getRaw()); + model.put("constants", + (TemplateHashModel) new DefaultObjectWrapperBuilder( + Configuration.VERSION_2_3_32) + .build().getStaticModels() + .get(Constants.class.getName())); model.put("reconciler", config); - model.put("constants", constantsMap(Constants.class)); - addLoginRequestedFor(model, vmDef); // Methods - model.put("parseQuantity", parseQuantityModel); - model.put("formatMemory", formatMemoryModel); - model.put("imageLocation", imgageLocationModel); - model.put("toJson", toJsonModel); - return model; - } - - /** - * Creates a map with constants. Needed because freemarker doesn't support - * nested classes with its static models. - * - * @param clazz the clazz - * @return the map - */ - @SuppressWarnings("PMD.EmptyCatchBlock") - private Map constantsMap(Class clazz) { - @SuppressWarnings("PMD.UseConcurrentHashMap") - Map result = new HashMap<>(); - Arrays.stream(clazz.getFields()).filter(f -> { - var modifiers = f.getModifiers(); - return Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers) - && f.getType() == String.class; - }).forEach(f -> { - try { - result.put(f.getName(), f.get(null)); - } catch (IllegalArgumentException | IllegalAccessException e) { - // Should not happen, ignore - } - }); - Arrays.stream(clazz.getClasses()).filter(c -> { - var modifiers = c.getModifiers(); - return Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers); - }).forEach(c -> { - result.put(c.getSimpleName(), constantsMap(c)); - }); - return result; - } - - private void addLoginRequestedFor(Map model, - VmDefinition vmDef) { - vmDef.assignment().filter(a -> { - try { - return newEventPipeline() - .fire(new GetPools().withName(a.pool())).get() - .stream().findFirst().map(VmPool::loginOnAssignment) - .orElse(false); - } catch (InterruptedException e) { - logger.log(Level.WARNING, e, e::getMessage); - } - return false; - }).map(Assignment::user) - .or(() -> vmDef.fromSpec("vm", "display", "loggedInUser")) - .ifPresent(u -> model.put("loginRequestedFor", u)); - } - - private final TemplateMethodModelEx parseQuantityModel - = new TemplateMethodModelEx() { + model.put("parseQuantity", new TemplateMethodModelEx() { @Override @SuppressWarnings("PMD.PreserveStackTrace") public Object exec(@SuppressWarnings("rawtypes") List arguments) throws TemplateModelException { var arg = arguments.get(0); - if (arg instanceof SimpleNumber number) { - return number.getAsNumber(); + if (arg instanceof Number number) { + return number; } try { return Quantity.fromString(arg.toString()).getNumber(); @@ -330,11 +294,10 @@ public class Reconciler extends Component { + "specified as \"" + arg + "\": " + e.getMessage()); } } - }; - - private final TemplateMethodModelEx formatMemoryModel - = new TemplateMethodModelEx() { + }); + model.put("formatMemory", new TemplateMethodModelEx() { @Override + @SuppressWarnings("PMD.PreserveStackTrace") public Object exec(@SuppressWarnings("rawtypes") List arguments) throws TemplateModelException { var arg = arguments.get(0); @@ -359,45 +322,7 @@ public class Reconciler extends Component { } return Convertions.formatMemory(bigInt); } - }; - - private final TemplateMethodModelEx imgageLocationModel - = new TemplateMethodModelEx() { - @Override - @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" }) - public Object exec(@SuppressWarnings("rawtypes") List arguments) - throws TemplateModelException { - var image = ((SimpleScalar) arguments.get(0)).getAsString(); - if (image.isEmpty()) { - return ""; - } - try { - var imageUri - = new URI("file://" + Constants.IMAGE_REPO_PATH + "/") - .resolve(image); - if ("file".equals(imageUri.getScheme())) { - return imageUri.getPath(); - } - return imageUri.toString(); - } catch (URISyntaxException e) { - logger.warning(() -> "Invalid CDROM image: " + image); - } - return image; - } - }; - - private final TemplateMethodModelEx toJsonModel - = new TemplateMethodModelEx() { - @Override - public Object exec(@SuppressWarnings("rawtypes") List arguments) - throws TemplateModelException { - try { - return mapper.writeValueAsString( - ((AdapterTemplateModel) arguments.get(0)) - .getAdaptedObject(Object.class)); - } catch (JsonProcessingException e) { - return "{}"; - } - } - }; + }); + return model; + } } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java new file mode 100644 index 0000000..3cd6a39 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java @@ -0,0 +1,107 @@ +/* + * VM-Operator + * Copyright (C) 2023 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.manager; + +import freemarker.template.Configuration; +import freemarker.template.TemplateException; +import io.kubernetes.client.custom.V1Patch; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.Dynamics; +import io.kubernetes.client.util.generic.options.PatchOptions; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Map; +import java.util.logging.Logger; +import org.jdrupes.vmoperator.common.K8s; +import org.jdrupes.vmoperator.manager.events.VmChannel; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; +import org.jdrupes.vmoperator.util.GsonPtr; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +/** + * Delegee for reconciling the stateful set (effectively the pod). + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +/* default */ class StatefulSetReconciler { + + protected final Logger logger = Logger.getLogger(getClass().getName()); + private final Configuration fmConfig; + + /** + * Instantiates a new config map reconciler. + * + * @param fmConfig the fm config + */ + public StatefulSetReconciler(Configuration fmConfig) { + this.fmConfig = fmConfig; + } + + /** + * Reconcile stateful set. + * + * @param event the event + * @param model the model + * @param channel the channel + * @throws IOException Signals that an I/O exception has occurred. + * @throws TemplateException the template exception + * @throws ApiException the api exception + */ + public void reconcile(VmDefChanged event, Map model, + VmChannel channel) + throws IOException, TemplateException, ApiException { + DynamicKubernetesApi stsApi = new DynamicKubernetesApi("apps", "v1", + "statefulsets", channel.client()); + var metadata = event.vmDefinition().getMetadata(); + + // Combine template and data and parse result + var fmTemplate = fmConfig.getTemplate("runnerSts.ftl.yaml"); + StringWriter out = new StringWriter(); + fmTemplate.process(model, out); + // Avoid Yaml.load due to + // https://github.com/kubernetes-client/java/issues/2741 + var stsDef = Dynamics.newFromYaml( + new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); + + // If exists apply changes only when transitioning state + // or not running. + var existing = K8s.get(stsApi, metadata); + if (existing.isPresent()) { + var current = GsonPtr.to(existing.get().getRaw()) + .to("spec").getAsInt("replicas").orElse(1); + var desired = GsonPtr.to(stsDef.getRaw()) + .to("spec").getAsInt("replicas").orElse(1); + if (current == 1 && desired == 1) { + return; + } + } + + // Do apply changes + PatchOptions opts = new PatchOptions(); + opts.setForce(true); + opts.setFieldManager("kubernetes-java-kubectl-apply"); + stsApi.patch(stsDef.getMetadata().getNamespace(), + stsDef.getMetadata().getName(), V1Patch.PATCH_FORMAT_APPLY_YAML, + new V1Patch(channel.client().getJSON().serialize(stsDef)), + opts).throwsApiException(); + } + +} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java deleted file mode 100644 index 22f083c..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import com.google.gson.JsonObject; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.custom.V1Patch; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.Watch; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sDynamicStub; -import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; -import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub; -import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinitionStub; -import org.jdrupes.vmoperator.common.VmDefinitions; -import org.jdrupes.vmoperator.common.VmExtraData; -import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; -import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; -import org.jdrupes.vmoperator.manager.events.ChannelManager; -import org.jdrupes.vmoperator.manager.events.ModifyVm; -import org.jdrupes.vmoperator.manager.events.PodChanged; -import org.jdrupes.vmoperator.manager.events.UpdateAssignment; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; -import org.jdrupes.vmoperator.util.GsonPtr; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Event; -import org.jgrapes.core.EventPipeline; -import org.jgrapes.core.annotation.Handler; - -/** - * Watches for changes of VM definitions. When a VM definition (CR) - * becomes known, is is registered with a {@link ChannelManager} and thus - * gets an associated {@link VmChannel} and an associated - * {@link EventPipeline}. - * - * The {@link EventPipeline} is used for submitting an action that processes - * the change data from kubernetes, eventually transforming it to a - * {@link VmResourceChanged} event that is handled by another - * {@link EventPipeline} associated with the {@link VmChannel}. This - * event pipeline should be used for all events related to changes of - * a particular VM. - */ -public class VmMonitor extends - AbstractMonitor { - - private final ChannelManager channelManager; - - /** - * Instantiates a new VM definition watcher. - * - * @param componentChannel the component channel - * @param channelManager the channel manager - */ - public VmMonitor(Channel componentChannel, - ChannelManager channelManager) { - super(componentChannel, VmDefinition.class, - VmDefinitions.class); - this.channelManager = channelManager; - } - - @Override - protected void prepareMonitoring() throws IOException, ApiException { - client(new K8sClient()); - - // Get all our API versions - var ctx = K8s.context(client(), Crd.GROUP, "", Crd.KIND_VM); - if (ctx.isEmpty()) { - logger.severe(() -> "Cannot get CRD context."); - return; - } - context(ctx.get()); - - // Remove left over resources - purge(); - } - - private void purge() throws ApiException { - // Get existing CRs (VMs) - var known = K8sDynamicStub.list(client(), context(), namespace()) - .stream().map(stub -> stub.name()).collect(Collectors.toSet()); - ListOptions opts = new ListOptions(); - opts.setLabelSelector( - "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," - + "app.kubernetes.io/name=" + APP_NAME); - for (var context : Set.of(K8sV1StatefulSetStub.CONTEXT, - K8sV1ConfigMapStub.CONTEXT)) { - for (var resStub : K8sDynamicStub.list(client(), context, - namespace(), opts)) { - String instance = resStub.model() - .map(m -> m.metadata().getName()).orElse("(unknown)"); - if (!known.contains(instance)) { - resStub.delete(); - } - } - } - } - - @Override - protected void handleChange(K8sClient client, - Watch.Response response) { - var name = response.object.getMetadata().getName(); - - // Process the response data on a VM specific pipeline to - // increase concurrency when e.g. starting many VMs. - var preparing = channelManager.associated(name) - .orElseGet(() -> newEventPipeline()); - preparing.submit("VmChange[" + name + "]", - () -> processChange(client, response, preparing)); - } - - private void processChange(K8sClient client, - Watch.Response response, EventPipeline preparing) { - // Get full definition and associate with channel as backup - var vmDef = response.object; - if (vmDef.data() == null) { - // ADDED event does not provide data, see - // https://github.com/kubernetes-client/java/issues/3215 - vmDef = getModel(client, vmDef); - } - var name = response.object.getMetadata().getName(); - var channel = channelManager.channel(name) - .orElseGet(() -> channelManager.createChannel(name)); - if (vmDef.data() != null) { - // New data, augment and save - addExtraData(vmDef, channel.vmDefinition()); - channel.setVmDefinition(vmDef); - } else { - // Reuse cached (e.g. if deleted) - vmDef = channel.vmDefinition(); - } - if (vmDef == null) { - logger.warning(() -> "Cannot get defintion for " - + response.object.getMetadata()); - return; - } - channelManager.put(name, channel, preparing); - - // Create and fire changed event. Remove channel from channel - // manager on completion. - VmResourceChanged chgEvt - = new VmResourceChanged(ResponseType.valueOf(response.type), vmDef, - channel.setGeneration(response.object.getMetadata() - .getGeneration()), - false); - if (ResponseType.valueOf(response.type) == ResponseType.DELETED) { - chgEvt = Event.onCompletion(chgEvt, - e -> channelManager.remove(e.vmDefinition().name())); - } - channel.fire(chgEvt); - } - - private VmDefinition getModel(K8sClient client, VmDefinition vmDef) { - try { - return VmDefinitionStub.get(client, context(), namespace(), - vmDef.metadata().getName()).model().orElse(null); - } catch (ApiException e) { - return null; - } - } - - private void addExtraData(VmDefinition vmDef, VmDefinition prevState) { - var extra = new VmExtraData(vmDef); - var prevExtra = Optional.ofNullable(prevState).map(VmDefinition::extra); - - // Maintain (or initialize) the resetCount - extra.resetCount(prevExtra.map(VmExtraData::resetCount).orElse(0L)); - - // Maintain node info - prevExtra - .ifPresent(e -> extra.nodeInfo(e.nodeName(), e.nodeAddresses())); - } - - /** - * On pod changed. - * - * @param event the event - * @param channel the channel - */ - @Handler - public void onPodChanged(PodChanged event, VmChannel channel) { - var vmDef = channel.vmDefinition(); - - // Make sure that this is properly sync'd with VM CR changes. - channelManager.associated(vmDef.name()) - .orElseGet(() -> activeEventPipeline()) - .submit("NodeInfo[" + vmDef.name() + "]", - () -> { - updateNodeInfo(event, vmDef); - channel.fire(new VmResourceChanged(ResponseType.MODIFIED, - vmDef, false, true)); - }); - } - - private void updateNodeInfo(PodChanged event, VmDefinition vmDef) { - var extra = vmDef.extra(); - if (event.type() == ResponseType.DELETED) { - // The status of a deleted pod is the status before deletion, - // i.e. the node info is still cached and must be removed. - extra.nodeInfo("", Collections.emptyList()); - return; - } - - // Get current node info from pod - var pod = event.pod(); - var nodeName = Optional - .ofNullable(pod.getSpec().getNodeName()).orElse(""); - logger.finer(() -> "Adding node name " + nodeName - + " to VM info for " + vmDef.name()); - var addrs = new ArrayList(); - Optional.ofNullable(pod.getStatus().getPodIPs()) - .orElse(Collections.emptyList()).stream() - .map(ip -> ip.getIp()).forEach(addrs::add); - logger.finer(() -> "Adding node addresses " + addrs - + " to VM info for " + vmDef.name()); - extra.nodeInfo(nodeName, addrs); - } - - /** - * On modify vm. - * - * @param event the event - * @throws ApiException the api exception - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onModifyVm(ModifyVm event, VmChannel channel) - throws ApiException, IOException { - patchVmDef(channel.client(), event.name(), "spec/vm/" + event.path(), - event.value()); - } - - private void patchVmDef(K8sClient client, String name, String path, - Object value) throws ApiException, IOException { - var vmStub = K8sDynamicStub.get(client, - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), namespace(), - name); - - // Patch running - String valueAsText = value instanceof String - ? "\"" + value + "\"" - : value.toString(); - var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, - new V1Patch("[{\"op\": \"replace\", \"path\": \"/" - + path + "\", \"value\": " + valueAsText + "}]"), - client.defaultPatchOptions()); - if (!res.isPresent()) { - logger.warning( - () -> "Cannot patch definition for Vm " + vmStub.name()); - } - } - - /** - * Attempt to Update the assignment information in the status of the - * VM CR. Returns true if successful. The handler does not attempt - * retries, because in case of failure it will be necessary to - * re-evaluate the chosen VM. - * - * @param event the event - * @param channel the channel - * @throws ApiException the api exception - */ - @Handler - public void onUpdatedAssignment(UpdateAssignment event, VmChannel channel) - throws ApiException { - try { - var vmDef = channel.vmDefinition(); - var vmStub = VmDefinitionStub.get(channel.client(), - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - vmDef.namespace(), vmDef.name()); - if (vmStub.updateStatus(vmDef, from -> { - JsonObject status = from.statusJson(); - if (event.toUser() == null) { - ((JsonObject) GsonPtr.to(status).get()) - .remove(Status.ASSIGNMENT); - } else { - var assignment = GsonPtr.to(status).to(Status.ASSIGNMENT); - assignment.set("pool", event.fromPool().name()); - assignment.set("user", event.toUser()); - assignment.set("lastUsed", Instant.now().toString()); - } - return status; - }).isPresent()) { - event.setResult(true); - } - } catch (ApiException e) { - // Log exceptions except for conflict, which can be expected - if (HttpURLConnection.HTTP_CONFLICT != e.getCode()) { - throw e; - } - } - event.setResult(false); - } - -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java new file mode 100644 index 0000000..2d99727 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java @@ -0,0 +1,352 @@ +/* + * VM-Operator + * Copyright (C) 2023 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.manager; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.ApisApi; +import io.kubernetes.client.openapi.apis.CustomObjectsApi; +import io.kubernetes.client.openapi.models.V1APIGroup; +import io.kubernetes.client.openapi.models.V1APIResource; +import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery; +import io.kubernetes.client.openapi.models.V1Namespace; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.util.Config; +import io.kubernetes.client.util.Watch; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import io.kubernetes.client.util.generic.options.ListOptions; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; +import org.jdrupes.vmoperator.common.K8s; +import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; +import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM; +import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; +import org.jdrupes.vmoperator.manager.events.Exit; +import org.jdrupes.vmoperator.manager.events.VmChannel; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; +import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type; +import org.jdrupes.vmoperator.util.GsonPtr; +import org.jgrapes.core.Channel; +import org.jgrapes.core.Component; +import org.jgrapes.core.Components; +import org.jgrapes.core.annotation.Handler; +import org.jgrapes.core.events.Start; +import org.jgrapes.core.events.Stop; +import org.jgrapes.util.events.ConfigurationUpdate; + +/** + * Watches for changes of VM definitions. + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class VmWatcher extends Component { + + private String namespaceToWatch; + private final Map channels = new ConcurrentHashMap<>(); + + /** + * Instantiates a new VM definition watcher. + * + * @param componentChannel the component channel + */ + public VmWatcher(Channel componentChannel) { + super(componentChannel); + } + + /** + * Configure the component. + * + * @param event the event + */ + @Handler + public void onConfigurationUpdate(ConfigurationUpdate event) { + event.structured(Components.manager(parent()).componentPath()) + .ifPresent(c -> { + if (c.containsKey("namespace")) { + namespaceToWatch = (String) c.get("namespace"); + } + }); + } + + /** + * Handle the start event. + * + * @param event the event + * @throws IOException + * @throws ApiException + */ + @Handler(priority = 10) + public void onStart(Start event) { + try { + startWatching(); + } catch (IOException | ApiException e) { + logger.log(Level.SEVERE, e, + () -> "Cannot watch VMs, terminating."); + event.cancel(true); + fire(new Exit(1)); + } + } + + private void startWatching() throws IOException, ApiException { + // Get namespace + if (namespaceToWatch == null) { + var path = Path + .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); + if (Files.isReadable(path)) { + namespaceToWatch = Files.lines(path).findFirst().orElse(null); + } + } + // Availability already checked by Controller.onStart + logger.fine(() -> "Watching namespace \"" + namespaceToWatch + "\"."); + + // Get all our API versions + var client = Config.defaultClient(); + var apis = new ApisApi(client).getAPIVersions(); + var vmOpApiVersions = apis.getGroups().stream() + .filter(g -> g.getName().equals(VM_OP_GROUP)).findFirst() + .map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream()) + .map(V1GroupVersionForDiscovery::getVersion).toList(); + + // Remove left overs + var coa = new CustomObjectsApi(client); + purge(client, coa, vmOpApiVersions); + + // Start a watcher thread for each existing CRD version. + // The watcher will send us an "ADDED" for each existing VM. + for (var version : vmOpApiVersions) { + coa.getAPIResources(VM_OP_GROUP, version) + .getResources().stream() + .filter(r -> VM_OP_KIND_VM.equals(r.getKind())) + .findFirst() + .ifPresent(crd -> watchVmDefs(crd, version)); + } + } + + @SuppressWarnings("PMD.CognitiveComplexity") + private void purge(ApiClient client, CustomObjectsApi coa, + List vmOpApiVersions) throws ApiException { + // Get existing CRs (VMs) + Set known = new HashSet<>(); + for (var version : vmOpApiVersions) { + // Get all known CR instances. + coa.getAPIResources(VM_OP_GROUP, version) + .getResources().stream() + .filter(r -> VM_OP_KIND_VM.equals(r.getKind())) + .findFirst() + .ifPresent(crd -> known.addAll(getKnown(client, crd, version))); + } + + ListOptions opts = new ListOptions(); + opts.setLabelSelector( + "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," + + "app.kubernetes.io/name=" + APP_NAME); + for (String resource : List.of("apps/v1/statefulsets", + "v1/configmaps", "v1/secrets")) { + @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", + "PMD.AvoidDuplicateLiterals" }) + var resParts = new LinkedList<>(List.of(resource.split("/"))); + var group = resParts.size() == 3 ? resParts.poll() : ""; + var version = resParts.poll(); + var plural = resParts.poll(); + // Get resources, selected by label + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var api = new DynamicKubernetesApi(group, version, plural, client); + var listObj = api.list(namespaceToWatch, opts).getObject(); + if (listObj == null) { + continue; + } + for (var obj : listObj.getItems()) { + String instance = obj.getMetadata().getLabels() + .get("app.kubernetes.io/instance"); + if (!known.contains(instance)) { + var resName = obj.getMetadata().getName(); + var result = api.delete(namespaceToWatch, resName); + if (!result.isSuccess()) { + logger.warning(() -> "Cannot cleanup resource \"" + + resName + "\": " + result.toString()); + } + } + } + } + } + + private Set getKnown(ApiClient client, V1APIResource crd, + String version) { + Set result = new HashSet<>(); + var api = new DynamicKubernetesApi(VM_OP_GROUP, version, + crd.getName(), client); + for (var item : api.list(namespaceToWatch).getObject().getItems()) { + if (!VM_OP_KIND_VM.equals(item.getKind())) { + continue; + } + result.add(item.getMetadata().getName()); + } + return result; + } + + private void watchVmDefs(V1APIResource crd, String version) { + @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", + "PMD.AvoidCatchingThrowable", "PMD.AvoidCatchingGenericException" }) + var watcher = new Thread(() -> { + try { + logger.info(() -> "Watching objects created from " + + crd.getName() + "." + VM_OP_GROUP + "/" + version); + // Watch sometimes terminates without apparent reason. + while (true) { + Instant startedAt = Instant.now(); + var client = Config.defaultClient(); + var coa = new CustomObjectsApi(client); + var call = coa.listNamespacedCustomObjectCall(VM_OP_GROUP, + version, namespaceToWatch, crd.getName(), null, false, + null, null, null, null, null, null, null, true, null); + try (Watch watch + = Watch.createWatch(client, call, + new TypeToken>() { + }.getType())) { + for (Watch.Response item : watch) { + handleVmDefinitionChange(crd, item); + } + } catch (IOException | ApiException | RuntimeException e) { + logger.log(Level.FINE, e, () -> "Problem watching \"" + + crd.getName() + "\" (will retry): " + + e.getMessage()); + delayRestart(startedAt); + } + } + } catch (Throwable e) { + logger.log(Level.SEVERE, e, () -> "Probem watching: " + + e.getMessage()); + } + fire(new Stop()); + }); + watcher.setDaemon(true); + watcher.start(); + } + + @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") + private void delayRestart(Instant started) { + var runningFor = Duration + .between(started, Instant.now()).toMillis(); + if (runningFor < 5000) { + logger.log(Level.FINE, () -> "Waiting... "); + try { + Thread.sleep(5000 - runningFor); + } catch (InterruptedException e1) { // NOPMD + // Retry + } + logger.log(Level.FINE, () -> "Retrying"); + } + } + + private void handleVmDefinitionChange(V1APIResource vmsCrd, + Watch.Response vmDefStub) { + V1ObjectMeta metadata = vmDefStub.object.getMetadata(); + VmChannel channel = channels.computeIfAbsent(metadata.getName(), + k -> { + try { + return new VmChannel(channel(), newEventPipeline(), + Config.defaultClient()); + } catch (IOException e) { + logger.log(Level.SEVERE, e, () -> "Failed to create client" + + " for handling changes: " + e.getMessage()); + return null; + } + }); + if (channel == null) { + return; + } + + // Get full definition and associate with channel as backup + var apiVersion = K8s.version(vmDefStub.object.getApiVersion()); + DynamicKubernetesApi vmCrApi = new DynamicKubernetesApi(VM_OP_GROUP, + apiVersion, vmsCrd.getName(), channel.client()); + var curVmDef = K8s.get(vmCrApi, metadata); + curVmDef.ifPresent(def -> { + // Augment with "dynamic" data and associate with channel + addDynamicData(channel.client(), def); + channel.setVmDefinition(def); + }); + + // Get eventual definition to use + var vmDef = curVmDef.orElse(channel.vmDefinition()); + + // Create and fire event + channel.pipeline().fire(new VmDefChanged(VmDefChanged.Type + .valueOf(vmDefStub.type), + channel + .setGeneration(vmDefStub.object.getMetadata().getGeneration()), + vmsCrd, vmDef), channel); + } + + private void addDynamicData(ApiClient client, + DynamicKubernetesObject vmDef) { + var rootNode = GsonPtr.to(vmDef.getRaw()).get(JsonObject.class); + rootNode.addProperty("nodeName", ""); + + // VM definition status changes before the pod terminates. + // This results in pod information being shown for a stopped + // VM which is irritating. So check condition first. + var isRunning = GsonPtr.to(rootNode).to("status", "conditions") + .get(JsonArray.class) + .asList().stream().filter(el -> "Running" + .equals(((JsonObject) el).get("type").getAsString())) + .findFirst().map(el -> "True" + .equals(((JsonObject) el).get("status").getAsString())) + .orElse(false); + if (!isRunning) { + return; + } + var podSearch = new ListOptions(); + podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + + ",app.kubernetes.io/component=" + APP_NAME + + ",app.kubernetes.io/instance=" + vmDef.getMetadata().getName()); + var podList = K8s.podApi(client).list(namespaceToWatch, podSearch); + podList.getObject().getItems().stream().forEach(pod -> { + rootNode.addProperty("nodeName", pod.getSpec().getNodeName()); + }); + } + + /** + * Remove VM channel when VM is deleted. + * + * @param event the event + * @param channel the channel + */ + @Handler(priority = -10_000) + public void onVmDefChanged(VmDefChanged event, VmChannel channel) { + if (event.type() == Type.DELETED) { + channels.remove(event.vmDefinition().getMetadata().getName()); + } + } + +} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java index 1d05ec9..54d4efe 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp + * Copyright (C) 2023 Michael N. Lipp * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -57,7 +57,7 @@ * ``` * * Developers may also be interested in the usage of channels - * by the application's components: + * by the application's component: * * ![Main channels](app-channels.svg) * @@ -74,8 +74,6 @@ * * Component NioDispatcher as NioDispatcher <> * [Manager] *-up- [NioDispatcher] - * Component HttpConnector as HttpConnector <> - * [Manager] *-up- [HttpConnector] * Component FileSystemWatcher as FileSystemWatcher <> * [Manager] *-up- [FileSystemWatcher] * Component YamlConfigurationStore as YamlConfigurationStore <> @@ -83,18 +81,8 @@ * [YamlConfigurationStore] *-right[hidden]- [Controller] * * [Manager] *-- [Controller] - * Component VmMonitor as VmMonitor <> - * [Controller] *-- [VmMonitor] - * [VmMonitor] -right[hidden]- [PoolMonitor] - * Component PoolMonitor as PoolMonitor <> - * [Controller] *-- [PoolMonitor] - * Component PodMonitor as PodMonitor <> - * [Controller] *-- [PodMonitor] - * [PodMonitor] -up[hidden]- VmMonitor - * Component DisplaySecretMonitor as DisplaySecretMonitor <> - * [Controller] *-- [DisplaySecretMonitor] - * [DisplaySecretMonitor] -up[hidden]- VmMonitor - * [Controller] *-left- [Reconciler] + * [Controller] *-- [VmWatcher] + * [Controller] *-- [Reconciler] * [Controller] -right[hidden]- [GuiHttpServer] * * [Manager] *-down- [GuiSocketServer:8080] @@ -131,7 +119,6 @@ * [WebConsole] *-- [RoleConfigurator] * [WebConsole] *-- [RoleConletFilter] * [WebConsole] *-left- [LoginConlet] - * [WebConsole] *-right- [OidcClient] * * Component "ComponentCollector\nfor page resources" as cpr <> * [WebConsole] *-- [cpr] @@ -160,35 +147,21 @@ * () "guiTransport" as hT * hT .up. [GuiSocketServer:8080] * hT .down. [GuiHttpServer] - * hT .right[hidden]. [HttpConnector] * * [YamlConfigurationStore] -right[hidden]- hT * * () "guiHttp" as http * http .up. [GuiHttpServer] - * http .up. [HttpConnector] - * note top of [HttpConnector]: transport layer com-\nponents omitted * - * [PreferencesStore] .. http - * [OidcClient] .up. http - * [LanguageSelector] .left. http + * [PreferencesStore] .right. http * [InMemorySessionManager] .up. http + * [LanguageSelector] .up. http * * package "Conceptual WebConsole" { - * [ConsoleWeblet] .right. http + * [ConsoleWeblet] .left. http * [ConsoleWeblet] *-down- [WebConsole] * } * - * [Controller] .down[hidden]. [ConsoleWeblet] - * - * () "console" as console - * console .. WebConsole - * - * [OidcClient] .. console - * [LoginConlet] .right. console - * - * note right of console: More conlets\nconnect here - * * @enduml */ package org.jdrupes.vmoperator.manager; diff --git a/org.jdrupes.vmoperator.manager/test-resources/basic-vm.yaml b/org.jdrupes.vmoperator.manager/test-resources/basic-vm.yaml deleted file mode 100644 index 36054a2..0000000 --- a/org.jdrupes.vmoperator.manager/test-resources/basic-vm.yaml +++ /dev/null @@ -1,64 +0,0 @@ -apiVersion: "vmoperator.jdrupes.org/v1" -kind: VirtualMachine -metadata: - namespace: vmop-test - name: test-vm -spec: - image: - repository: docker-registry.lan.mnl.de - path: vmoperator/this.will.never.start - version: 0.0.0 - - cloudInit: - metaData: {} - - vm: - # state: Running - maximumRam: 4Gi - currentRam: 2Gi - maximumCpus: 4 - currentCpus: 2 - powerdownTimeout: 1 - - networks: - - user: {} - disks: - - cdrom: - image: https://test.com/test.iso - bootindex: 0 - - cdrom: - image: "image.iso" - - volumeClaimTemplate: - metadata: - name: system - annotations: - use_as: system-disk - spec: - storageClassName: local-path - resources: - requests: - storage: 1Gi - - volumeClaimTemplate: - spec: - storageClassName: local-path - resources: - requests: - storage: 1Gi - - display: - outputs: 2 - spice: - port: 5812 - usbRedirects: 2 - - resources: - requests: - cpu: 1 - memory: 2Gi - - loadBalancerService: - labels: - label2: replaced - label3: added - annotations: - anno1: added diff --git a/org.jdrupes.vmoperator.manager/test-resources/kustomization.yaml b/org.jdrupes.vmoperator.manager/test-resources/kustomization.yaml deleted file mode 100644 index 3a8451e..0000000 --- a/org.jdrupes.vmoperator.manager/test-resources/kustomization.yaml +++ /dev/null @@ -1,111 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- ../../deploy - -namespace: vmop-test - -patches: -- patch: |- - kind: PersistentVolumeClaim - apiVersion: v1 - metadata: - name: vmop-image-repository - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi - storageClassName: local-path - -- patch: |- - kind: ConfigMap - apiVersion: v1 - metadata: - name: vm-operator - data: - # Keep in sync with config.yaml - config.yaml: | - "/Manager": - # clusterName: "test" - "/Controller": - "/Reconciler": - runnerData: - storageClassName: null - loadBalancerService: - labels: - label1: label1 - label2: toBeReplaced - annotations: - metallb.universe.tf/loadBalancerIPs: 192.168.168.1 - metallb.universe.tf/ip-allocated-from-pool: single-common - metallb.universe.tf/allow-shared-ip: single-common - "/GuiSocketServer": - port: 8888 - "/GuiHttpServer": - # This configures the GUI - "/ConsoleWeblet": - "/WebConsole": - "/LoginConlet": - users: - - name: admin - fullName: Administrator - password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." - - name: test1 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test2 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test3 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - "/RoleConfigurator": - rolesByUser: - # User admin has role admin - admin: - - admin - test1: - - user - test2: - - user - test3: - - user - # All users have role other - "*": - - other - replace: false - "/RoleConletFilter": - conletTypesByRole: - # Admins can use all conlets - admin: - - "*" - user: - - org.jdrupes.vmoperator.vmviewer.VmViewer - # Others cannot use any conlet (except login conlet to log out) - other: - - org.jgrapes.webconlet.locallogin.LoginConlet - "/ComponentCollector": - "/VmAccess": - displayResource: - preferredIpVersion: ipv4 - syncPreviewsFor: - - role: user -- target: - group: apps - version: v1 - kind: Deployment - name: vm-operator - patch: |- - - op: replace - path: /spec/template/spec/containers/0/image - value: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.manager:test - - op: replace - path: /spec/template/spec/containers/0/imagePullPolicy - value: Always - - op: replace - path: /spec/replicas - value: 0 - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.manager/test-resources/unittest-vm.yaml b/org.jdrupes.vmoperator.manager/test-resources/unittest-vm.yaml new file mode 100644 index 0000000..0d395bd --- /dev/null +++ b/org.jdrupes.vmoperator.manager/test-resources/unittest-vm.yaml @@ -0,0 +1,35 @@ +apiVersion: "vmoperator.jdrupes.org/v1" +kind: VirtualMachine +metadata: + namespace: vmop-dev + name: unittest-vm +spec: + resources: + requests: + cpu: 1 + memory: 2Gi + + loadBalancerService: + labels: + test2: null + test3: added + + vm: + # state: Running + maximumRam: 4Gi + currentRam: 2Gi + maximumCpus: 4 + currentCpus: 2 + powerdownTimeout: 1 + + networks: + - user: {} + disks: + - 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: 5812 diff --git a/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java b/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java index d600d3c..26eb387 100644 --- a/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java +++ b/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java @@ -1,32 +1,13 @@ package org.jdrupes.vmoperator.manager; -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.custom.Quantity; -import io.kubernetes.client.custom.V1Patch; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.generic.options.ListOptions; -import io.kubernetes.client.util.generic.options.PatchOptions; -import java.io.FileReader; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext; import java.io.IOException; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; -import org.jdrupes.vmoperator.common.Constants; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; -import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sDynamicStub; -import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub; -import org.jdrupes.vmoperator.common.K8sV1DeploymentStub; -import org.jdrupes.vmoperator.common.K8sV1PodStub; -import org.jdrupes.vmoperator.common.K8sV1PvcStub; -import org.jdrupes.vmoperator.common.K8sV1SecretStub; -import org.jdrupes.vmoperator.common.K8sV1ServiceStub; -import org.jdrupes.vmoperator.util.DataPath; import org.junit.jupiter.api.AfterAll; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.BeforeAll; @@ -37,12 +18,8 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; class BasicTests { - private static K8sClient client; - private static APIResource vmsContext; - private static K8sV1DeploymentStub mgrDeployment; - private static K8sDynamicStub vmStub; - private static final String VM_NAME = "test-vm"; - private static final Object EXISTS = new Object(); + private static KubernetesClient client; + private static ResourceDefinitionContext vmsContext; @BeforeAll static void setUpBeforeClass() throws Exception { @@ -50,44 +27,29 @@ class BasicTests { assertNotNull(testCluster); // Get client - client = new K8sClient(); - - // Update manager pod by scaling deployment - mgrDeployment - = K8sV1DeploymentStub.get(client, "vmop-test", "vm-operator"); - mgrDeployment.scale(0); - mgrDeployment.scale(1); - waitForManager(); + client = new KubernetesClientBuilder() + .withConfig(Config.autoConfigure(testCluster)).build(); // Context for working with our CR - var apiRes = K8s.context(client, Crd.GROUP, null, Crd.KIND_VM); - assertTrue(apiRes.isPresent()); - vmsContext = apiRes.get(); + vmsContext = new ResourceDefinitionContext.Builder() + .withGroup("vmoperator.jdrupes.org").withKind("VirtualMachine") + .withPlural("vms").withNamespaced(true).withVersion("v1").build(); - // Cleanup existing VM - K8sDynamicStub.get(client, vmsContext, "vmop-test", VM_NAME) - .delete(); - ListOptions listOpts = new ListOptions(); - listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + VM_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME); - var secrets = K8sV1SecretStub.list(client, "vmop-test", listOpts); - for (var secret : secrets) { - secret.delete(); - } - deletePvcs(); + // Cleanup + var resourcesInNamespace = client.genericKubernetesResources(vmsContext) + .inNamespace("vmop-dev"); + resourcesInNamespace.withName("unittest-vm").delete(); - // Load from Yaml - var rdr = new FileReader("test-resources/basic-vm.yaml"); - vmStub = K8sDynamicStub.createFromYaml(client, vmsContext, rdr); - assertTrue(vmStub.model().isPresent()); - } + // Update manager pod by scaling deployment + client.apps().deployments().inNamespace("vmop-dev") + .withName("vm-operator").scale(0); + client.apps().deployments().inNamespace("vmop-dev") + .withName("vm-operator").scale(1); - private static void waitForManager() - throws ApiException, InterruptedException { // Wait until available for (int i = 0; i < 10; i++) { - if (mgrDeployment.model().get().getStatus().getConditions() + if (client.apps().deployments().inNamespace("vmop-dev") + .withName("vm-operator").get().getStatus().getConditions() .stream().filter(c -> "Available".equals(c.getType())).findAny() .isPresent()) { return; @@ -97,245 +59,63 @@ class BasicTests { fail("vm-operator not deployed."); } - private static void deletePvcs() throws ApiException { - ListOptions listOpts = new ListOptions(); - listOpts.setLabelSelector( - "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," - + "app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + VM_NAME); - var knownPvcs = K8sV1PvcStub.list(client, "vmop-test", listOpts); - for (var pvc : knownPvcs) { - pvc.delete(); - } - } - @AfterAll static void tearDownAfterClass() throws Exception { - // Cleanup - K8sDynamicStub.get(client, vmsContext, "vmop-test", VM_NAME) - .delete(); - deletePvcs(); - // Bring down manager - mgrDeployment.scale(0); + client.apps().deployments().inNamespace("vmop-dev") + .withName("vm-operator").scale(0); + client.close(); } @Test - void testConfigMap() - throws IOException, InterruptedException, ApiException { - K8sV1ConfigMapStub stub - = K8sV1ConfigMapStub.get(client, "vmop-test", VM_NAME); - for (int i = 0; i < 10; i++) { - if (stub.model().isPresent()) { - break; - } - Thread.sleep(1000); - } + void test() throws IOException, InterruptedException { + // Load from Yaml + var vm = client.genericKubernetesResources(vmsContext) + .load(Files + .newInputStream(Path.of("test-resources/unittest-vm.yaml"))); + // Create Custom Resource + vm.create(); + + // Wait for created resources + assertTrue(waitForConfigMap()); + assertTrue(waitForStatefulSet()); + // Check config map - var config = stub.model().get(); - Map, Object> toCheck = Map.of( - List.of("namespace"), "vmop-test", - List.of("name"), VM_NAME, - List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME, - List.of("annotations", "vmoperator.jdrupes.org/version"), EXISTS, - List.of("ownerReferences", 0, "apiVersion"), - vmsContext.getGroup() + "/" + vmsContext.getVersions().get(0), - List.of("ownerReferences", 0, "kind"), Crd.KIND_VM, - List.of("ownerReferences", 0, "name"), VM_NAME, - List.of("ownerReferences", 0, "uid"), EXISTS); - checkProps(config.getMetadata(), toCheck); + var config = client.configMaps().inNamespace("vmop-dev") + .withName("unittest-vm").get(); + var yaml = new Yaml(new SafeConstructor(new LoaderOptions())) + .load((String) config.getData().get("config.yaml")); + @SuppressWarnings("unchecked") + var currentRam = ((Map>>) yaml) + .get("/Runner").get("vm").get("maximumRam"); + assertEquals("4 GiB", currentRam); - toCheck = new LinkedHashMap<>(); - toCheck.put(List.of("/Runner", "guestShutdownStops"), false); - toCheck.put(List.of("/Runner", "cloudInit", "metaData", "instance-id"), - EXISTS); - toCheck.put( - List.of("/Runner", "cloudInit", "metaData", "local-hostname"), - VM_NAME); - toCheck.put(List.of("/Runner", "cloudInit", "userData"), Map.of()); - toCheck.put(List.of("/Runner", "vm", "maximumRam"), "4 GiB"); - toCheck.put(List.of("/Runner", "vm", "currentRam"), "2 GiB"); - toCheck.put(List.of("/Runner", "vm", "maximumCpus"), 4); - toCheck.put(List.of("/Runner", "vm", "currentCpus"), 2); - toCheck.put(List.of("/Runner", "vm", "powerdownTimeout"), 1); - toCheck.put(List.of("/Runner", "vm", "network", 0, "type"), "user"); - toCheck.put(List.of("/Runner", "vm", "drives", 0, "type"), "ide-cd"); - toCheck.put(List.of("/Runner", "vm", "drives", 0, "file"), - "https://test.com/test.iso"); - toCheck.put(List.of("/Runner", "vm", "drives", 0, "bootindex"), 0); - toCheck.put(List.of("/Runner", "vm", "drives", 1, "type"), "ide-cd"); - toCheck.put(List.of("/Runner", "vm", "drives", 1, "file"), - "/var/local/vmop-image-repository/image.iso"); - toCheck.put(List.of("/Runner", "vm", "drives", 2, "type"), "raw"); - toCheck.put(List.of("/Runner", "vm", "drives", 2, "resource"), - "/dev/system-disk"); - toCheck.put(List.of("/Runner", "vm", "drives", 3, "type"), "raw"); - toCheck.put(List.of("/Runner", "vm", "drives", 3, "resource"), - "/dev/disk-1"); - toCheck.put(List.of("/Runner", "vm", "display", "outputs"), 2); - toCheck.put(List.of("/Runner", "vm", "display", "spice", "port"), 5812); - toCheck.put( - List.of("/Runner", "vm", "display", "spice", "usbRedirects"), 2); - var cm = new Yaml(new SafeConstructor(new LoaderOptions())) - .load(config.getData().get("config.yaml")); - checkProps(cm, toCheck); + // Cleanup + var resourcesInNamespace = client.genericKubernetesResources(vmsContext) + .inNamespace("vmop-dev"); + resourcesInNamespace.withName("unittest-vm").delete(); } - @Test - void testDisplaySecret() throws ApiException, InterruptedException { - ListOptions listOpts = new ListOptions(); - listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + VM_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME); - Collection secrets = null; + private boolean waitForConfigMap() throws InterruptedException { for (int i = 0; i < 10; i++) { - secrets = K8sV1SecretStub.list(client, "vmop-test", listOpts); - if (secrets.size() > 0) { - break; + if (client.configMaps().inNamespace("vmop-dev") + .withName("unittest-vm").get() != null) { + return true; } Thread.sleep(1000); } - assertEquals(1, secrets.size()); - var secretData = secrets.iterator().next().model().get().getData(); - checkProps(secretData, Map.of( - List.of("display-password"), EXISTS)); - assertEquals("now", new String(secretData.get("password-expiry"))); + return false; } - @Test - void testRunnerPvc() throws ApiException, InterruptedException { - var stub - = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-runner-data"); + private boolean waitForStatefulSet() throws InterruptedException { for (int i = 0; i < 10; i++) { - if (stub.model().isPresent()) { - break; + if (client.apps().statefulSets().inNamespace("vmop-dev") + .withName("unittest-vm").get() != null) { + return true; } Thread.sleep(1000); } - var pvc = stub.model().get(); - checkProps(pvc.getMetadata(), Map.of( - List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME)); - checkProps(pvc.getSpec(), Map.of( - List.of("resources", "requests", "storage"), - Quantity.fromString("1Mi"))); - } - - @Test - void testSystemDiskPvc() throws ApiException, InterruptedException { - var stub - = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-system-disk"); - for (int i = 0; i < 10; i++) { - if (stub.model().isPresent()) { - break; - } - Thread.sleep(1000); - } - var pvc = stub.model().get(); - checkProps(pvc.getMetadata(), Map.of( - List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME, - List.of("annotations", "use_as"), "system-disk")); - checkProps(pvc.getSpec(), Map.of( - List.of("resources", "requests", "storage"), - Quantity.fromString("1Gi"))); - } - - @Test - void testDisk1Pvc() throws ApiException, InterruptedException { - var stub - = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-disk-1"); - for (int i = 0; i < 10; i++) { - if (stub.model().isPresent()) { - break; - } - Thread.sleep(1000); - } - var pvc = stub.model().get(); - checkProps(pvc.getMetadata(), Map.of( - List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME)); - checkProps(pvc.getSpec(), Map.of( - List.of("resources", "requests", "storage"), - Quantity.fromString("1Gi"))); - } - - @Test - void testPod() throws ApiException, InterruptedException { - PatchOptions opts = new PatchOptions(); - opts.setForce(true); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - assertTrue(vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, - new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state" - + "\", \"value\": \"Running\"}]"), - client.defaultPatchOptions()).isPresent()); - var stub = K8sV1PodStub.get(client, "vmop-test", VM_NAME); - for (int i = 0; i < 20; i++) { - if (stub.model().isPresent()) { - break; - } - Thread.sleep(1000); - } - var pod = stub.model().get(); - checkProps(pod.getMetadata(), Map.of( - List.of("labels", "app.kubernetes.io/name"), APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/component"), APP_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME, - List.of("annotations", "vmrunner.jdrupes.org/cmVersion"), EXISTS, - List.of("annotations", "vmoperator.jdrupes.org/version"), EXISTS, - List.of("ownerReferences", 0, "apiVersion"), - vmsContext.getGroup() + "/" + vmsContext.getVersions().get(0), - List.of("ownerReferences", 0, "kind"), Crd.KIND_VM, - List.of("ownerReferences", 0, "name"), VM_NAME, - List.of("ownerReferences", 0, "uid"), EXISTS)); - checkProps(pod.getSpec(), Map.of( - List.of("containers", 0, "image"), EXISTS, - List.of("containers", 0, "name"), VM_NAME, - List.of("containers", 0, "resources", "requests", "cpu"), - Quantity.fromString("1"))); - } - - @Test - public void testLoadBalancer() throws ApiException, InterruptedException { - var stub = K8sV1ServiceStub.get(client, "vmop-test", VM_NAME); - for (int i = 0; i < 10; i++) { - if (stub.model().isPresent()) { - break; - } - Thread.sleep(1000); - } - var svc = stub.model().get(); - checkProps(svc.getMetadata(), Map.of( - List.of("labels", "app.kubernetes.io/name"), APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME, - List.of("labels", "label1"), "label1", - List.of("labels", "label2"), "replaced", - List.of("labels", "label3"), "added", - List.of("annotations", "metallb.universe.tf/loadBalancerIPs"), - "192.168.168.1", - List.of("annotations", "anno1"), "added")); - } - - private void checkProps(Object obj, - Map, Object> toCheck) { - for (var entry : toCheck.entrySet()) { - var prop = DataPath.get(obj, entry.getKey().toArray()); - assertTrue(prop.isPresent(), () -> "Property " + entry.getKey() - + " not found in " + obj); - - // Check for existance only - if (entry.getValue() == EXISTS) { - continue; - } - assertEquals(entry.getValue(), prop.get()); - } + return false; } } diff --git a/org.jdrupes.vmoperator.runner.qemu/.eclipse-pmd b/org.jdrupes.vmoperator.runner.qemu/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/org.jdrupes.vmoperator.runner.qemu/.eclipse-pmd +++ b/org.jdrupes.vmoperator.runner.qemu/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.runner.qemu/build.gradle b/org.jdrupes.vmoperator.runner.qemu/build.gradle index 695c815..7179b8f 100644 --- a/org.jdrupes.vmoperator.runner.qemu/build.gradle +++ b/org.jdrupes.vmoperator.runner.qemu/build.gradle @@ -9,14 +9,14 @@ plugins { } dependencies { - implementation 'org.jgrapes:org.jgrapes.core:[1.22.1,2)' - implementation 'org.jgrapes:org.jgrapes.util:[1.38.1,2)' - implementation 'org.jgrapes:org.jgrapes.io:[2.12.1,3)' - implementation 'org.jgrapes:org.jgrapes.http:[3.5.0,4)' + implementation 'org.jgrapes:org.jgrapes.core:[1.19.0,2)' + implementation 'org.jgrapes:org.jgrapes.io:[2.7.0,3)' + implementation 'org.jgrapes:org.jgrapes.http:[3.1.0,4)' + implementation 'org.jgrapes:org.jgrapes.util:[1.31.0,2)' implementation project(':org.jdrupes.vmoperator.common') implementation 'commons-cli:commons-cli:1.5.0' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.16.1]' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.15.1,3]' runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)' } @@ -31,99 +31,116 @@ application { mainClass = 'org.jdrupes.vmoperator.runner.qemu.Runner' } -project.ext.gitBranch = grgit.branch.current.name.replace('/', '-') -def registry = "${project.rootProject.properties['docker.registry']}" -def rootVersion = rootProject.version - -task buildImageArch(type: Exec) { +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.gitBranch}",\ + '-t', "${project.name}-arch:${project.version}",\ '-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch', '.' } -task pushImageArch(type: Exec) { - dependsOn buildImageArch - - commandLine 'podman', 'push', '--tls-verify=false', \ - "${project.name}-arch:${project.gitBranch}", \ - "${registry}/${project.name}-arch:${project.gitBranch}" -} - -task tagWithVersionArch(type: Exec) { - dependsOn pushImageArch - - enabled = !rootVersion.contains("SNAPSHOT") +task tagLatestArchImage(type: Exec) { + dependsOn buildArchImage - commandLine 'podman', 'push', \ - "${project.name}-arch:${project.gitBranch}",\ - "${registry}/${project.name}-arch:${project.version}" -} - -task tagAsLatestArch(type: Exec) { - dependsOn tagWithVersionArch - - enabled = !rootVersion.contains("SNAPSHOT") - && !rootVersion.contains("alpha") \ - && !rootVersion.contains("beta") \ + 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', 'push', \ - "${project.name}-arch:${project.gitBranch}",\ - "${registry}/${project.name}-arch:latest" + commandLine 'podman', 'tag', "${project.name}-arch:${project.version}",\ + "${project.name}-arch:latest" } -task buildImageAlpine(type: Exec) { +task buildLatestArchImage { + dependsOn buildArchImage + dependsOn tagLatestArchImage +} + +task pushArchImage(type: Exec) { + dependsOn buildArchImage + + commandLine 'podman', 'push', '--tls-verify=false', \ + "localhost/${project.name}-arch:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}-arch:${project.version}" +} + +task pushArchLatestImage(type: Exec) { + dependsOn buildLatestArchImage + + 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', 'push', '--tls-verify=false', \ + "localhost/${project.name}-arch:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}-arch:latest" +} + +task buildAlpineImage(type: Exec) { dependsOn installDist inputs.files 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine' commandLine 'podman', 'build', '--pull', - '-t', "${project.name}-alpine:${project.gitBranch}",\ + '-t', "${project.name}-alpine:${project.version}",\ '-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine', '.' } -task pushImageAlpine(type: Exec) { - dependsOn buildImageAlpine - - commandLine 'podman', 'push', '--tls-verify=false', \ - "localhost/${project.name}-alpine:${project.gitBranch}", \ - "${registry}/${project.name}-alpine:${project.gitBranch}" -} - -task tagWithVersionAlpine(type: Exec) { - dependsOn pushImageAlpine - - enabled = !rootVersion.contains("SNAPSHOT") +task tagLatestAlpineImage(type: Exec) { + dependsOn buildAlpineImage - commandLine 'podman', 'push', \ - "${project.name}-alpine:${project.gitBranch}",\ - "${registry}/${project.name}-alpine:${project.version}" -} - -task tagAsLatestAlpine(type: Exec) { - dependsOn tagWithVersionAlpine - - enabled = !rootVersion.contains("SNAPSHOT") - && !rootVersion.contains("alpha") \ - && !rootVersion.contains("beta") \ + 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', 'push', \ - "${project.name}-alpine:${project.gitBranch}",\ - "${registry}/${project.name}-alpine:latest" + commandLine 'podman', 'tag', "${project.name}-alpine:${project.version}",\ + "${project.name}-alpine:latest" } -task publishImage { - dependsOn pushImageArch - dependsOn tagWithVersionArch - dependsOn tagAsLatestArch - dependsOn pushImageAlpine - dependsOn tagWithVersionAlpine - dependsOn tagAsLatestAlpine +task buildLatestAlpineImage { + dependsOn buildAlpineImage + dependsOn tagLatestAlpineImage } + +task pushAlpineImage(type: Exec) { + dependsOn buildAlpineImage + + commandLine 'podman', 'push', '--tls-verify=false', \ + "localhost/${project.name}-alpine:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}-alpine:${project.version}" +} + +task pushAlpineLatestImage(type: Exec) { + dependsOn buildLatestAlpineImage + + 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', 'push', '--tls-verify=false', \ + "localhost/${project.name}-alpine:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}-alpine:latest" +} + +task pushImages { + dependsOn pushArchImage + dependsOn pushArchLatestImage + dependsOn pushAlpineImage + dependsOn pushAlpineLatestImage +} + diff --git a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml index e23a2ec..c365a12 100644 --- a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml +++ b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml @@ -45,12 +45,6 @@ # property in the CRD. # "guestShutdownStops": # false - - # When incremented, the VM is reset. The value has no default value, - # i.e. if you start the VM without a value for this property, and - # decide to trigger a reset later, you have to first set the value - # and then inrement it. - # "resetCounter": 1 # Define the VM (required) "vm": diff --git a/org.jdrupes.vmoperator.runner.qemu/display-password b/org.jdrupes.vmoperator.runner.qemu/display-password deleted file mode 100644 index 97c1abb..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/display-password +++ /dev/null @@ -1 +0,0 @@ -test-vm \ No newline at end of file diff --git a/org.jdrupes.vmoperator.runner.qemu/password-expiry b/org.jdrupes.vmoperator.runner.qemu/password-expiry deleted file mode 100644 index 8a606d5..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/password-expiry +++ /dev/null @@ -1 +0,0 @@ -+1800 \ No newline at end of file diff --git a/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml b/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml index 600f0ad..9aadbf6 100644 --- a/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml +++ b/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml @@ -8,16 +8,10 @@ - "/usr/share/edk2/ovmf/OVMF_CODE.fd" - "/usr/share/edk2/x64/OVMF_CODE.fd" - "/usr/share/OVMF/OVMF_CODE.fd" - # Use 4M version as fallback (if smaller version not available) - - "/usr/share/edk2/ovmf-4m/OVMF_CODE.fd" - - "/usr/share/edk2/x64/OVMF_CODE.4m.fd" "vars": - "/usr/share/edk2/ovmf/OVMF_VARS.fd" - "/usr/share/edk2/x64/OVMF_VARS.fd" - "/usr/share/OVMF/OVMF_VARS.fd" - # Use 4M version as fallback (if smaller version not available) - - "/usr/share/edk2/ovmf-4m/OVMF_VARS.fd" - - "/usr/share/edk2/x64/OVMF_VARS.4m.fd" "uefi-4m": "rom": - "/usr/share/edk2/ovmf-4m/OVMF_CODE.fd" diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/AgentConnector.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/AgentConnector.java deleted file mode 100644 index 6303794..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/AgentConnector.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; -import org.jdrupes.vmoperator.runner.qemu.events.VserportChangeEvent; -import org.jgrapes.core.Channel; -import org.jgrapes.core.annotation.Handler; - -/** - * A component that handles the communication with an agent - * running in the VM. - * - * If the log level for this class is set to fine, the messages - * exchanged on the socket are logged. - */ -public abstract class AgentConnector extends QemuConnector { - - protected String channelId; - - /** - * Instantiates a new agent connector. - * - * @param componentChannel the component channel - * @throws IOException Signals that an I/O exception has occurred. - */ - public AgentConnector(Channel componentChannel) throws IOException { - super(componentChannel); - } - - /** - * Extracts the channel id and the socket path from the QEMU - * command line. - * - * @param command the command - * @param chardev the chardev - */ - @SuppressWarnings("PMD.CognitiveComplexity") - protected void configureConnection(List command, String chardev) { - Path socketPath = null; - for (var arg : command) { - if (arg.startsWith("virtserialport,") - && arg.contains("chardev=" + chardev)) { - for (var prop : arg.split(",")) { - if (prop.startsWith("id=")) { - channelId = prop.substring(3); - } - } - } - if (arg.startsWith("socket,") - && arg.contains("id=" + chardev)) { - for (var prop : arg.split(",")) { - if (prop.startsWith("path=")) { - socketPath = Path.of(prop.substring(5)); - } - } - } - } - if (channelId == null || socketPath == null) { - logger.warning(() -> "Definition of chardev " + chardev - + " missing in runner template."); - return; - } - logger.fine(() -> getClass().getSimpleName() + " configured with" - + " channelId=" + channelId); - super.configure(socketPath); - } - - /** - * When the virtual serial port with the configured channel id has - * been opened call {@link #agentConnected()}. - * - * @param event the event - */ - @Handler - public void onVserportChanged(VserportChangeEvent event) { - if (event.id().equals(channelId)) { - if (event.isOpen()) { - agentConnected(); - } else { - agentDisconnected(); - } - } - } - - /** - * Called when the agent in the VM opens the connection. The - * default implementation does nothing. - */ - @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") - protected void agentConnected() { - // Default is to do nothing. - } - - /** - * Called when the agent in the VM closes the connection. The - * default implementation does nothing. - */ - @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") - protected void agentDisconnected() { - // Default is to do nothing. - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CdMediaController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CdMediaController.java index c4ac871..b7c960a 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CdMediaController.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CdMediaController.java @@ -25,9 +25,9 @@ import java.util.concurrent.ConcurrentHashMap; import org.jdrupes.vmoperator.runner.qemu.commands.QmpChangeMedium; 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.RunState; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; import org.jdrupes.vmoperator.runner.qemu.events.TrayMovedEvent; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; @@ -36,6 +36,7 @@ import org.jgrapes.core.annotation.Handler; /** * The Class CdMediaController. */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class CdMediaController extends Component { /** @@ -54,6 +55,7 @@ public class CdMediaController extends Component { * * @param componentChannel the component channel */ + @SuppressWarnings("PMD.AssignmentToNonFinalStatic") public CdMediaController(Channel componentChannel) { super(componentChannel); } @@ -64,9 +66,10 @@ public class CdMediaController extends Component { * @param event the event */ @Handler - @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" }) - public void onConfigureQemu(ConfigureQemu event) { - if (event.runState() == RunState.TERMINATING) { + @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", + "PMD.AvoidInstantiatingObjectsInLoops" }) + public void onConfigureQemu(RunnerConfigurationUpdate event) { + if (event.state() == State.TERMINATING) { return; } @@ -79,7 +82,7 @@ public class CdMediaController extends Component { } var driveId = "cd" + cdCounter++; var newFile = Optional.ofNullable(drives[i].file).orElse(""); - if (event.runState() == RunState.STARTING) { + if (event.state() == State.STARTING) { current.put(driveId, newFile); continue; } @@ -113,8 +116,8 @@ public class CdMediaController extends Component { */ @Handler public void onTrayMovedEvent(TrayMovedEvent event) { - trayState.put(event.driveId(), event.trayState()); - if (event.trayState() == TrayState.OPEN + trayState.put(event.driveId(), event.state()); + if (event.state() == TrayState.OPEN && pending.containsKey(event.driveId())) { changeMedium(event.driveId()); } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java index 7aec209..9057606 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java @@ -69,9 +69,4 @@ class CommandDefinition { public String name() { return name; } - - @Override - public String toString() { - return "Command " + name + ": " + command; - } } \ No newline at end of file diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java index 87e8c76..7fc3f95 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java @@ -39,12 +39,14 @@ import org.jdrupes.vmoperator.util.FsdUtils; /** * The configuration information from the configuration file. */ +@SuppressWarnings("PMD.ExcessivePublicCount") public class Configuration implements Dto { private static final String CI_INSTANCE_ID = "instance-id"; + @SuppressWarnings("PMD.FieldNamingConventions") protected final Logger logger = Logger.getLogger(getClass().getName()); - /** Configuration timestamp. */ + /** Configuration timestamp */ public Instant asOf; /** The data dir. */ @@ -71,18 +73,12 @@ public class Configuration implements Dto { /** The firmware vars. */ public Path firmwareVars; - /** The display password. */ - public boolean hasDisplayPassword; - /** Optional cloud-init data. */ public CloudInit cloudInit; /** If guest shutdown changes CRD .vm.state to "Stopped". */ public boolean guestShutdownStops; - /** Increments of the reset counter trigger a reset of the VM. */ - public Integer resetCounter; - /** The vm. */ @SuppressWarnings("PMD.ShortVariable") public Vm vm; @@ -91,14 +87,11 @@ public class Configuration implements Dto { * Subsection "cloud-init". */ public static class CloudInit implements Dto { - - /** The meta data. */ + @SuppressWarnings("PMD.UseConcurrentHashMap") public Map metaData; - - /** The user data. */ + @SuppressWarnings("PMD.UseConcurrentHashMap") public Map userData; - - /** The network config. */ + @SuppressWarnings("PMD.UseConcurrentHashMap") public Map networkConfig; } @@ -106,7 +99,7 @@ public class Configuration implements Dto { * Subsection "vm". */ @SuppressWarnings({ "PMD.ShortClassName", "PMD.TooManyFields", - "PMD.DataClass", "PMD.AvoidDuplicateLiterals" }) + "PMD.DataClass" }) public static class Vm implements Dto { /** The name. */ @@ -194,7 +187,6 @@ public class Configuration implements Dto { /** * Subsection "network". */ - @SuppressWarnings("PMD.DataClass") public static class Network implements Dto { /** The type. */ @@ -216,7 +208,6 @@ public class Configuration implements Dto { /** * Subsection "drive". */ - @SuppressWarnings("PMD.DataClass") public static class Drive implements Dto { /** The type. */ @@ -239,21 +230,12 @@ public class Configuration implements Dto { * The Class Display. */ public static class Display implements Dto { - - /** The number of outputs. */ - public int outputs = 1; - - /** The logged in user. */ - public String loggedInUser; - - /** The spice. */ public Spice spice; } /** * Subsection "spice". */ - @SuppressWarnings("PMD.DataClass") public static class Spice implements Dto { /** The port. */ @@ -294,6 +276,7 @@ public class Configuration implements Dto { return true; } + @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") private void checkDrives() { for (Drive drive : vm.drives) { if (drive.file != null || drive.device != null @@ -313,6 +296,7 @@ public class Configuration implements Dto { } } + @SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts") private boolean checkRuntimeDir() { // Runtime directory (sockets etc.) if (runtimeDir == null) { @@ -348,6 +332,7 @@ public class Configuration implements Dto { return true; } + @SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts") private boolean checkDataDir() { // Data directory if (dataDir == null) { diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java deleted file mode 100644 index b50b481..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu; - -import com.google.gson.JsonObject; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.EventsV1Event; -import java.io.IOException; -import java.util.logging.Level; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.VmDefinitionStub; -import org.jdrupes.vmoperator.runner.qemu.events.Exit; -import org.jdrupes.vmoperator.runner.qemu.events.SpiceDisconnectedEvent; -import org.jdrupes.vmoperator.runner.qemu.events.SpiceInitializedEvent; -import org.jgrapes.core.Channel; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Start; - -/** - * A (sub)component that updates the console status in the CR status. - * Created as child of {@link StatusUpdater}. - */ -public class ConsoleTracker extends VmDefUpdater { - - private VmDefinitionStub vmStub; - private String mainChannelClientHost; - private long mainChannelClientPort; - - /** - * Instantiates a new status updater. - * - * @param componentChannel the component channel - */ - public ConsoleTracker(Channel componentChannel) { - super(componentChannel); - apiClient = (K8sClient) io.kubernetes.client.openapi.Configuration - .getDefaultApiClient(); - } - - /** - * Handle the start event. - * - * @param event the event - * @throws IOException - * @throws ApiException - */ - @Handler - public void onStart(Start event) { - if (namespace == null) { - return; - } - try { - vmStub = VmDefinitionStub.get(apiClient, - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - namespace, vmName); - } catch (ApiException e) { - logger.log(Level.SEVERE, e, - () -> "Cannot access VM object, terminating."); - event.cancel(true); - fire(new Exit(1)); - } - } - - /** - * On spice connected. - * - * @param event the event - * @throws ApiException the api exception - */ - @Handler - @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" }) - public void onSpiceInitialized(SpiceInitializedEvent event) - throws ApiException { - if (vmStub == null) { - return; - } - - // Only process connections using main channel. - if (event.channelType() != 1) { - return; - } - mainChannelClientHost = event.clientHost(); - mainChannelClientPort = event.clientPort(); - vmStub.updateStatus(from -> { - JsonObject status = updateCondition(from, "ConsoleConnected", true, - "Connected", "Connection from " + event.clientHost()); - status.addProperty(Status.CONSOLE_CLIENT, event.clientHost()); - return status; - }); - - // Log event - var evt = new EventsV1Event() - .reportingController(Crd.GROUP + "/" + APP_NAME) - .action("ConsoleConnectionUpdate") - .reason("Connection from " + event.clientHost()); - K8s.createEvent(apiClient, vmStub.model().get(), evt); - } - - /** - * On spice disconnected. - * - * @param event the event - * @throws ApiException the api exception - */ - @Handler - public void onSpiceDisconnected(SpiceDisconnectedEvent event) - throws ApiException { - if (vmStub == null) { - return; - } - - // Only process disconnects from main channel. - if (!event.clientHost().equals(mainChannelClientHost) - || event.clientPort() != mainChannelClientPort) { - return; - } - vmStub.updateStatus(from -> { - JsonObject status = updateCondition(from, "ConsoleConnected", false, - "Disconnected", event.clientHost() + " has disconnected"); - status.addProperty(Status.CONSOLE_CLIENT, ""); - return status; - }); - - // Log event - var evt = new EventsV1Event() - .reportingController(Crd.GROUP + "/" + APP_NAME) - .action("ConsoleConnectionUpdate") - .reason("Disconnected from " + event.clientHost()); - K8s.createEvent(apiClient, vmStub.model().get(), evt); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Constants.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Constants.java deleted file mode 100644 index eac05fa..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Constants.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu; - -/** - * Some constants. - */ -public class Constants extends org.jdrupes.vmoperator.common.Constants { - - /** - * Process names. - */ - public static class ProcessName { - - /** The Constant QEMU. */ - public static final String QEMU = "qemu"; - - /** The Constant SWTPM. */ - public static final String SWTPM = "swtpm"; - - /** The Constant CLOUD_INIT_IMG. */ - public static final String CLOUD_INIT_IMG = "cloudInitImg"; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine index d0104f3..b87049e 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine @@ -2,7 +2,7 @@ FROM docker.io/alpine RUN apk update -RUN apk add qemu-system-x86_64 qemu-modules ovmf swtpm openjdk21 mtools +RUN apk add qemu-system-x86_64 qemu-modules ovmf swtpm openjdk17 mtools RUN mkdir -p /etc/qemu && echo "allow all" > /etc/qemu/bridge.conf diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch index 0c2fd86..2ccb2f9 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch @@ -1,11 +1,11 @@ -FROM docker.io/archlinux/archlinux:latest +FROM 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 jre21-openjdk-headless \ + edk2-ovmf swtpm iproute2 bridge-utils jre17-openjdk-headless \ mtools \ && pacman -Scc --noconfirm diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java index 440da91..f0face4 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java @@ -22,18 +22,17 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.Set; import org.jdrupes.vmoperator.runner.qemu.commands.QmpAddCpu; import org.jdrupes.vmoperator.runner.qemu.commands.QmpDelCpu; import org.jdrupes.vmoperator.runner.qemu.commands.QmpQueryHotpluggableCpus; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; 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.RunState; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; import org.jgrapes.core.annotation.Handler; @@ -41,11 +40,12 @@ import org.jgrapes.core.annotation.Handler; /** * The Class CpuController. */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class CpuController extends Component { private Integer currentCpus; private Integer desiredCpus; - private ConfigureQemu suspendedConfigure; + private RunnerConfigurationUpdate suspendedConfigure; /** * Instantiates a new CPU controller. @@ -62,8 +62,8 @@ public class CpuController extends Component { * @param event the event */ @Handler - public void onConfigureQemu(ConfigureQemu event) { - if (event.runState() == RunState.TERMINATING) { + public void onConfigureQemu(RunnerConfigurationUpdate event) { + if (event.state() == State.TERMINATING) { return; } Optional.ofNullable(event.configuration().vm.currentCpus) @@ -170,7 +170,7 @@ public class CpuController extends Component { private void checkCpus() { if (suspendedConfigure != null && desiredCpus != null - && Objects.equals(currentCpus, desiredCpus)) { + && currentCpus == desiredCpus.intValue()) { suspendedConfigure.resumeHandling(); suspendedConfigure = null; } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java deleted file mode 100644 index c3bec93..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Objects; -import java.util.Optional; -import java.util.logging.Level; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -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.RunState; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogIn; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogOut; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Component; -import org.jgrapes.core.Event; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.util.events.FileChanged; -import org.jgrapes.util.events.WatchFile; - -/** - * The Class DisplayController. - */ -public class DisplayController extends Component { - - private String currentPassword; - private String protocol; - private final Path configDir; - private boolean canBeUpdated; - private boolean vmopAgentConnected; - private String loggedInUser; - - /** - * Instantiates a new Display controller. - * - * @param componentChannel the component channel - * @param configDir - */ - @SuppressWarnings({ "PMD.ConstructorCallsOverridableMethod" }) - public DisplayController(Channel componentChannel, Path configDir) { - super(componentChannel); - this.configDir = configDir; - fire(new WatchFile(configDir.resolve(DisplaySecret.PASSWORD))); - } - - /** - * On configure qemu. - * - * @param event the event - */ - @Handler - public void onConfigureQemu(ConfigureQemu event) { - if (event.runState() == RunState.TERMINATING) { - return; - } - protocol - = event.configuration().vm.display.spice != null ? "spice" : null; - loggedInUser = event.configuration().vm.display.loggedInUser; - configureLogin(); - if (event.runState() == RunState.STARTING) { - configurePassword(); - } - canBeUpdated = true; - } - - /** - * On vmop agent connected. - * - * @param event the event - */ - @Handler - public void onVmopAgentConnected(VmopAgentConnected event) { - vmopAgentConnected = true; - configureLogin(); - } - - private void configureLogin() { - if (!vmopAgentConnected) { - return; - } - Event evt = loggedInUser != null - ? new VmopAgentLogIn(loggedInUser) - : new VmopAgentLogOut(); - fire(evt); - } - - /** - * Watch for changes of the password file. - * - * @param event the event - */ - @Handler - public void onFileChanged(FileChanged event) { - if (event.path().equals(configDir.resolve(DisplaySecret.PASSWORD))) { - logger.fine(() -> "Display password updated"); - if (canBeUpdated) { - configurePassword(); - } - } - } - - private void configurePassword() { - if (protocol == null) { - return; - } - if (setDisplayPassword()) { - setPasswordExpiry(); - } - } - - private boolean setDisplayPassword() { - return readFromFile(DisplaySecret.PASSWORD).map(password -> { - if (Objects.equals(this.currentPassword, password)) { - return true; - } - this.currentPassword = password; - logger.fine(() -> "Updating display password"); - fire(new MonitorCommand( - new QmpSetDisplayPassword(protocol, password))); - return true; - }).orElse(false); - } - - private void setPasswordExpiry() { - readFromFile(DisplaySecret.EXPIRY).ifPresent(expiry -> { - logger.fine(() -> "Updating expiry time to " + expiry); - fire( - new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry))); - }); - } - - private Optional readFromFile(String dataItem) { - Path path = configDir.resolve(dataItem); - String label = dataItem.replace('-', ' '); - if (path.toFile().canRead()) { - logger.finer(() -> "Found " + label); - try { - return Optional.ofNullable(Files.readString(path)); - } catch (IOException e) { - logger.log(Level.WARNING, e, () -> "Cannot read " + label + ": " - + e.getMessage()); - return Optional.empty(); - } - } else { - logger.finer(() -> "No " + label); - return Optional.empty(); - } - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java deleted file mode 100644 index 45d2487..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.node.ObjectNode; -import java.io.IOException; -import java.time.Instant; -import java.util.LinkedList; -import java.util.Queue; -import java.util.logging.Level; -import org.jdrupes.vmoperator.runner.qemu.Constants.ProcessName; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpGuestGetOsinfo; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpGuestPowerdown; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; -import org.jdrupes.vmoperator.runner.qemu.events.GuestAgentCommand; -import org.jdrupes.vmoperator.runner.qemu.events.OsinfoEvent; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Components.Timer; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Stop; -import org.jgrapes.io.events.ProcessExited; - -/** - * A component that handles the communication with the guest agent. - * - * If the log level for this class is set to fine, the messages - * exchanged on the monitor socket are logged. - */ -public class GuestAgentClient extends AgentConnector { - - private boolean connected; - private Instant powerdownStartedAt; - private int powerdownTimeout; - private Timer powerdownTimer; - private final Queue executing = new LinkedList<>(); - private Stop suspendedStop; - - /** - * Instantiates a new guest agent client. - * - * @param componentChannel the component channel - * @throws IOException Signals that an I/O exception has occurred. - */ - public GuestAgentClient(Channel componentChannel) throws IOException { - super(componentChannel); - } - - /** - * When the agent has connected, request the OS information. - */ - @Override - protected void agentConnected() { - logger.fine(() -> "Guest agent connected"); - connected = true; - rep().fire(new GuestAgentCommand(new QmpGuestGetOsinfo())); - } - - @Override - protected void agentDisconnected() { - logger.fine(() -> "Guest agent disconnected"); - connected = false; - } - - /** - * Process agent input. - * - * @param line the line - * @throws IOException Signals that an I/O exception has occurred. - */ - @Override - protected void processInput(String line) throws IOException { - logger.finer(() -> "guest agent(in): " + line); - try { - var response = mapper.readValue(line, ObjectNode.class); - if (response.has("return") || response.has("error")) { - QmpCommand executed = executing.poll(); - logger.finer(() -> String.format("(Previous \"guest agent(in)\"" - + " is result from executing %s)", executed)); - if (executed instanceof QmpGuestGetOsinfo) { - var osInfo = new OsinfoEvent(response.get("return")); - logger.fine(() -> "Guest agent triggers: " + osInfo); - rep().fire(osInfo); - } - } - } catch (JsonProcessingException e) { - throw new IOException(e); - } - } - - /** - * On guest agent command. - * - * @param event the event - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - @SuppressWarnings({ "PMD.AvoidSynchronizedStatement", - "PMD.AvoidDuplicateLiterals" }) - public void onGuestAgentCommand(GuestAgentCommand event) - throws IOException { - if (qemuChannel() == null) { - return; - } - var command = event.command(); - logger.fine(() -> "Guest handles: " + event); - String asText; - try { - asText = command.asText(); - logger.finer(() -> "guest agent(out): " + asText); - } catch (JsonProcessingException e) { - logger.log(Level.SEVERE, e, - () -> "Cannot serialize Json: " + e.getMessage()); - return; - } - synchronized (executing) { - if (writer().isPresent()) { - executing.add(command); - sendCommand(asText); - } - } - } - - /** - * Shutdown the VM. - * - * @param event the event - */ - @Handler(priority = 200) - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onStop(Stop event) { - if (!connected) { - logger.fine(() -> "No guest agent connection," - + " cannot send shutdown command"); - return; - } - - // We have a connection to the guest agent attempt shutdown. - powerdownStartedAt = event.associated(Instant.class).orElseGet(() -> { - var now = Instant.now(); - event.setAssociated(Instant.class, now); - return now; - }); - var waitUntil = powerdownStartedAt.plusSeconds(powerdownTimeout); - if (waitUntil.isBefore(Instant.now())) { - return; - } - event.suspendHandling(); - suspendedStop = event; - logger.fine(() -> "Attempting shutdown through guest agent," - + " waiting for termination until " + waitUntil); - powerdownTimer = Components.schedule(t -> { - logger.fine(() -> "Powerdown timeout reached."); - synchronized (this) { - powerdownTimer = null; - if (suspendedStop != null) { - suspendedStop.resumeHandling(); - suspendedStop = null; - } - } - }, waitUntil); - rep().fire(new GuestAgentCommand(new QmpGuestPowerdown())); - } - - /** - * On process exited. - * - * @param event the event - */ - @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onProcessExited(ProcessExited event) { - if (!event.startedBy().associated(CommandDefinition.class) - .map(cd -> ProcessName.QEMU.equals(cd.name())).orElse(false)) { - return; - } - synchronized (this) { - if (powerdownTimer != null) { - powerdownTimer.cancel(); - } - if (suspendedStop != null) { - suspendedStop.resumeHandling(); - suspendedStop = null; - } - } - } - - /** - * On configure qemu. - * - * @param event the event - */ - @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onConfigureQemu(ConfigureQemu event) { - int newTimeout = event.configuration().vm.powerdownTimeout; - if (powerdownTimeout != newTimeout) { - powerdownTimeout = newTimeout; - synchronized (this) { - if (powerdownTimer != null) { - powerdownTimer - .reschedule(powerdownStartedAt.plusSeconds(newTimeout)); - } - - } - } - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuConnector.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuConnector.java deleted file mode 100644 index 777478e..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuConnector.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu; - -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.io.Writer; -import java.lang.reflect.UndeclaredThrowableException; -import java.net.UnixDomainSocketAddress; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Component; -import org.jgrapes.core.EventPipeline; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Start; -import org.jgrapes.core.events.Stop; -import org.jgrapes.io.events.Closed; -import org.jgrapes.io.events.ConnectError; -import org.jgrapes.io.events.Input; -import org.jgrapes.io.events.OpenSocketConnection; -import org.jgrapes.io.util.ByteBufferWriter; -import org.jgrapes.io.util.LineCollector; -import org.jgrapes.net.SocketIOChannel; -import org.jgrapes.net.events.ClientConnected; -import org.jgrapes.util.events.ConfigurationUpdate; -import org.jgrapes.util.events.FileChanged; -import org.jgrapes.util.events.WatchFile; - -/** - * A component that handles the communication with QEMU over a socket. - * - * Derived classes should log the messages exchanged on the socket - * if the log level is set to fine. - */ -public abstract class QemuConnector extends Component { - - @SuppressWarnings("PMD.FieldNamingConventions") - protected static final ObjectMapper mapper = new ObjectMapper(); - - private EventPipeline rep; - private Path socketPath; - private SocketIOChannel qemuChannel; - - /** - * Instantiates a new QEMU connector. - * - * @param componentChannel the component channel - * @throws IOException Signals that an I/O exception has occurred. - */ - public QemuConnector(Channel componentChannel) throws IOException { - super(componentChannel); - } - - /** - * As the initial configuration of this component depends on the - * configuration of the {@link Runner}, it doesn't have a handler - * for the {@link ConfigurationUpdate} event. The values are - * forwarded from the {@link Runner} instead. - * - * @param socketPath the socket path - */ - /* default */ void configure(Path socketPath) { - this.socketPath = socketPath; - logger.fine(() -> getClass().getSimpleName() - + " configured with socketPath=" + socketPath); - } - - /** - * Note the runner's event processor and delete the socket. - * - * @param event the event - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onStart(Start event) throws IOException { - rep = event.associated(EventPipeline.class).get(); - if (socketPath == null) { - return; - } - Files.deleteIfExists(socketPath); - fire(new WatchFile(socketPath)); - } - - /** - * Return the runner's event pipeline. - * - * @return the event pipeline - */ - protected EventPipeline rep() { - return rep; - } - - /** - * Watch for the creation of the swtpm socket and start the - * qemu process if it has been created. - * - * @param event the event - */ - @Handler - public void onFileChanged(FileChanged event) { - if (event.change() == FileChanged.Kind.CREATED - && event.path().equals(socketPath)) { - // qemu running, open socket - fire(new OpenSocketConnection( - UnixDomainSocketAddress.of(socketPath)) - .setAssociated(this, this)); - } - } - - /** - * Check if this is from opening the agent socket and if true, - * save the socket in the context and associate the channel with - * the context. - * - * @param event the event - * @param channel the channel - */ - @SuppressWarnings("resource") - @Handler - public void onClientConnected(ClientConnected event, - SocketIOChannel channel) { - event.openEvent().associated(this, getClass()).ifPresent(qc -> { - qemuChannel = channel; - channel.setAssociated(this, this); - channel.setAssociated(Writer.class, new ByteBufferWriter( - channel).nativeCharset()); - channel.setAssociated(LineCollector.class, - new LineCollector() - .consumer(line -> { - try { - qc.processInput(line); - } catch (IOException e) { - throw new UndeclaredThrowableException(e); - } - })); - qc.socketConnected(); - }); - } - - /** - * Return the QEMU channel if the connection has been established. - * - * @return the socket IO channel - */ - protected Optional qemuChannel() { - return Optional.ofNullable(qemuChannel); - } - - /** - * Return the {@link Writer} for the connection if the connection - * has been established. - * - * @return the optional - */ - protected Optional writer() { - return qemuChannel().flatMap(c -> c.associated(Writer.class)); - } - - /** - * Send the given command to QEMU. A newline is appended to the - * command automatically. - * - * @param command the command - * @return true, if successful - * @throws IOException Signals that an I/O exception has occurred. - */ - protected boolean sendCommand(String command) throws IOException { - if (writer().isEmpty()) { - return false; - } - writer().get().append(command).append('\n').flush(); - return true; - } - - /** - * Called when the connector has been connected to the socket. - */ - @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") - protected void socketConnected() { - // Default is to do nothing. - } - - /** - * Called when a connection attempt fails. - * - * @param event the event - */ - @Handler - public void onConnectError(ConnectError event) { - event.event().associated(this, getClass()).ifPresent(qc -> { - rep.fire(new Stop()); - }); - } - - /** - * Handle data from the socket connection. - * - * @param event the event - * @param channel the channel - */ - @Handler - public void onInput(Input event, SocketIOChannel channel) { - if (channel.associated(this, getClass()).isEmpty()) { - return; - } - channel.associated(LineCollector.class).ifPresent(collector -> { - collector.feed(event); - }); - } - - /** - * Process agent input. - * - * @param line the line - * @throws IOException Signals that an I/O exception has occurred. - */ - protected abstract void processInput(String line) throws IOException; - - /** - * On closed. - * - * @param event the event - * @param channel the channel - */ - @Handler - public void onClosed(Closed event, SocketIOChannel channel) { - channel.associated(this, getClass()).ifPresent(qc -> { - qemuChannel = null; - }); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java index feeb76a..3d22b26 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java @@ -19,33 +19,47 @@ package org.jdrupes.vmoperator.runner.qemu; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.UndeclaredThrowableException; +import java.net.UnixDomainSocketAddress; +import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.time.Instant; import java.util.LinkedList; import java.util.Queue; import java.util.logging.Level; -import org.jdrupes.vmoperator.runner.qemu.Constants.ProcessName; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCapabilities; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; import org.jdrupes.vmoperator.runner.qemu.commands.QmpPowerdown; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; import org.jdrupes.vmoperator.runner.qemu.events.MonitorEvent; import org.jdrupes.vmoperator.runner.qemu.events.MonitorReady; import org.jdrupes.vmoperator.runner.qemu.events.MonitorResult; import org.jdrupes.vmoperator.runner.qemu.events.PowerdownEvent; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; import org.jgrapes.core.Channel; +import org.jgrapes.core.Component; import org.jgrapes.core.Components; import org.jgrapes.core.Components.Timer; +import org.jgrapes.core.EventPipeline; import org.jgrapes.core.annotation.Handler; +import org.jgrapes.core.events.Start; import org.jgrapes.core.events.Stop; import org.jgrapes.io.events.Closed; -import org.jgrapes.io.events.ProcessExited; +import org.jgrapes.io.events.ConnectError; +import org.jgrapes.io.events.Input; +import org.jgrapes.io.events.OpenSocketConnection; +import org.jgrapes.io.util.ByteBufferWriter; +import org.jgrapes.io.util.LineCollector; import org.jgrapes.net.SocketIOChannel; +import org.jgrapes.net.events.ClientConnected; import org.jgrapes.util.events.ConfigurationUpdate; +import org.jgrapes.util.events.FileChanged; +import org.jgrapes.util.events.WatchFile; /** * A component that handles the communication over the Qemu monitor @@ -54,29 +68,32 @@ import org.jgrapes.util.events.ConfigurationUpdate; * If the log level for this class is set to fine, the messages * exchanged on the monitor socket are logged. */ -public class QemuMonitor extends QemuConnector { +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class QemuMonitor extends Component { + private static ObjectMapper mapper = new ObjectMapper(); + + private EventPipeline rep; + private Path socketPath; private int powerdownTimeout; + private SocketIOChannel monitorChannel; private final Queue executing = new LinkedList<>(); private Instant powerdownStartedAt; private Stop suspendedStop; private Timer powerdownTimer; private boolean powerdownConfirmed; - private boolean monitorReady; /** - * Instantiates a new QEMU monitor. + * Instantiates a new qemu monitor. * * @param componentChannel the component channel - * @param configDir the config dir * @throws IOException Signals that an I/O exception has occurred. */ - public QemuMonitor(Channel componentChannel, Path configDir) - throws IOException { + @SuppressWarnings("PMD.AssignmentToNonFinalStatic") + public QemuMonitor(Channel componentChannel) throws IOException { super(componentChannel); attach(new RamController(channel())); attach(new CpuController(channel())); - attach(new DisplayController(channel(), configDir)); attach(new CdMediaController(channel())); } @@ -90,45 +107,120 @@ public class QemuMonitor extends QemuConnector { * @param powerdownTimeout */ /* default */ void configure(Path socketPath, int powerdownTimeout) { - super.configure(socketPath); + this.socketPath = socketPath; this.powerdownTimeout = powerdownTimeout; } /** - * When the socket is connected, send the capabilities command. + * Handle the start event. + * + * @param event the event + * @throws IOException Signals that an I/O exception has occurred. */ - @Override - protected void socketConnected() { - rep().fire(new MonitorCommand(new QmpCapabilities())); + @Handler + public void onStart(Start event) throws IOException { + rep = event.associated(EventPipeline.class).get(); + if (socketPath == null) { + return; + } + Files.deleteIfExists(socketPath); + fire(new WatchFile(socketPath)); } - @Override - protected void processInput(String line) + /** + * Watch for the creation of the swtpm socket and start the + * qemu process if it has been created. + * + * @param event the event + */ + @Handler + public void onFileChanged(FileChanged event) { + if (event.change() == FileChanged.Kind.CREATED + && event.path().equals(socketPath)) { + // qemu running, open socket + fire(new OpenSocketConnection( + UnixDomainSocketAddress.of(socketPath)) + .setAssociated(QemuMonitor.class, this)); + } + } + + /** + * Check if this is from opening the monitor socket and if true, + * save the socket in the context and associate the channel with + * the context. Then send the initial message to the socket. + * + * @param event the event + * @param channel the channel + */ + @Handler + public void onClientConnected(ClientConnected event, + SocketIOChannel channel) { + event.openEvent().associated(QemuMonitor.class).ifPresent(qm -> { + monitorChannel = channel; + channel.setAssociated(QemuMonitor.class, this); + channel.setAssociated(Writer.class, new ByteBufferWriter( + channel).nativeCharset()); + channel.setAssociated(LineCollector.class, + new LineCollector() + .consumer(line -> { + try { + processMonitorInput(line); + } catch (IOException e) { + throw new UndeclaredThrowableException(e); + } + })); + fire(new MonitorCommand(new QmpCapabilities())); + }); + } + + /** + * Called when a connection attempt fails. + * + * @param event the event + * @param channel the channel + */ + @Handler + public void onConnectError(ConnectError event, SocketIOChannel channel) { + event.event().associated(QemuMonitor.class).ifPresent(qm -> { + rep.fire(new Stop()); + }); + } + + /** + * Handle data from qemu monitor connection. + * + * @param event the event + * @param channel the channel + */ + @Handler + public void onInput(Input event, SocketIOChannel channel) { + if (channel.associated(QemuMonitor.class).isEmpty()) { + return; + } + channel.associated(LineCollector.class).ifPresent(collector -> { + collector.feed(event); + }); + } + + private void processMonitorInput(String line) throws IOException { - logger.finer(() -> "monitor(in): " + line); + logger.fine(() -> "monitor(in): " + line); try { var response = mapper.readValue(line, ObjectNode.class); if (response.has("QMP")) { - monitorReady = true; - logger.fine(() -> "QMP connection ready"); - rep().fire(new MonitorReady()); + rep.fire(new MonitorReady()); return; } if (response.has("return") || response.has("error")) { QmpCommand executed = executing.poll(); - logger.finer( + logger.fine( () -> String.format("(Previous \"monitor(in)\" is result " + "from executing %s)", executed)); - var monRes = MonitorResult.from(executed, response); - logger.fine(() -> "QMP triggers: " + monRes); - rep().fire(monRes); + rep.fire(MonitorResult.from(executed, response)); return; } if (response.has("event")) { - MonitorEvent.from(response).ifPresent(me -> { - logger.fine(() -> "QMP triggers: " + me); - rep().fire(me); - }); + MonitorEvent.from(response).ifPresent(rep::fire); } } catch (JsonProcessingException e) { throw new IOException(e); @@ -142,10 +234,17 @@ public class QemuMonitor extends QemuConnector { */ @Handler public void onClosed(Closed event, SocketIOChannel channel) { - channel.associated(this, getClass()).ifPresent(qm -> { - super.onClosed(event, channel); - logger.fine(() -> "QMP connection closed."); - monitorReady = false; + channel.associated(QemuMonitor.class).ifPresent(qm -> { + monitorChannel = null; + synchronized (this) { + if (powerdownTimer != null) { + powerdownTimer.cancel(); + } + if (suspendedStop != null) { + suspendedStop.resumeHandling(); + suspendedStop = null; + } + } }); } @@ -153,37 +252,29 @@ public class QemuMonitor extends QemuConnector { * On monitor command. * * @param event the event - * @throws IOException */ @Handler - @SuppressWarnings({ "PMD.AvoidSynchronizedStatement", - "PMD.AvoidDuplicateLiterals" }) - public void onMonitorCommand(MonitorCommand event) throws IOException { - // Check prerequisites - if (!monitorReady && !(event.command() instanceof QmpCapabilities)) { - logger.severe(() -> "Premature QMP command (not ready): " - + event.command()); - rep().fire(new Stop()); - return; - } - - // Send the command + public void onExecQmpCommand(MonitorCommand event) { var command = event.command(); - logger.fine(() -> "QMP handles: " + event.toString()); String asText; try { - asText = command.asText(); - logger.finer(() -> "monitor(out): " + asText); + asText = mapper.writeValueAsString(command.toJson()); } catch (JsonProcessingException e) { logger.log(Level.SEVERE, e, () -> "Cannot serialize Json: " + e.getMessage()); return; } + logger.fine(() -> "monitor(out): " + asText); synchronized (executing) { - if (writer().isPresent()) { - executing.add(command); - sendCommand(asText); - } + monitorChannel.associated(Writer.class).ifPresent(writer -> { + try { + executing.add(command); + writer.append(asText).append('\n').flush(); + } catch (IOException e) { + // Cannot happen, but... + logger.log(Level.WARNING, e, () -> e.getMessage()); + } + }); } } @@ -193,51 +284,37 @@ public class QemuMonitor extends QemuConnector { * @param event the event */ @Handler(priority = 100) - @SuppressWarnings("PMD.AvoidSynchronizedStatement") public void onStop(Stop event) { - if (!monitorReady) { - logger.fine(() -> "Not sending QMP powerdown command" - + " because QMP connection is closed"); - return; - } + if (monitorChannel != null) { + // We have a connection to Qemu, attempt ACPI shutdown. + event.suspendHandling(); + suspendedStop = event; - // We have a connection to Qemu, attempt ACPI shutdown if time left - powerdownStartedAt = event.associated(Instant.class).orElseGet(() -> { - var now = Instant.now(); - event.setAssociated(Instant.class, now); - return now; - }); - if (powerdownStartedAt.plusSeconds(powerdownTimeout) - .isBefore(Instant.now())) { - return; - } - event.suspendHandling(); - suspendedStop = event; - - // Send command. If not confirmed, assume "hanging" qemu process. - powerdownTimer = Components.schedule(t -> { - logger.fine(() -> "QMP powerdown command not confirmed"); - synchronized (this) { - powerdownTimer = null; - if (suspendedStop != null) { - suspendedStop.resumeHandling(); - suspendedStop = null; + // Attempt powerdown command. If not confirmed, assume + // "hanging" qemu process. + powerdownTimer = Components.schedule(t -> { + // Powerdown not confirmed + logger.fine(() -> "QMP powerdown command has not effect."); + synchronized (this) { + powerdownTimer = null; + if (suspendedStop != null) { + suspendedStop.resumeHandling(); + suspendedStop = null; + } } - } - }, Duration.ofSeconds(5)); - logger.fine(() -> "Attempting QMP (ACPI) powerdown."); - rep().fire(new MonitorCommand(new QmpPowerdown())); + }, Duration.ofSeconds(1)); + logger.fine(() -> "Attempting QMP powerdown."); + powerdownStartedAt = Instant.now(); + fire(new MonitorCommand(new QmpPowerdown())); + } } /** - * When the powerdown event is confirmed, wait for termination - * or timeout. Termination is detected by the qemu process exiting - * (see {@link #onProcessExited(ProcessExited)}). + * On powerdown event. * * @param event the event */ @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") public void onPowerdownEvent(PowerdownEvent event) { synchronized (this) { // Cancel confirmation timeout @@ -246,54 +323,27 @@ public class QemuMonitor extends QemuConnector { } // (Re-)schedule timer as fallback - var waitUntil = powerdownStartedAt.plusSeconds(powerdownTimeout); - logger.fine(() -> "QMP powerdown confirmed, waiting for" - + " termination until " + waitUntil); + logger.fine(() -> "QMP powerdown confirmed, waiting..."); powerdownTimer = Components.schedule(t -> { logger.fine(() -> "Powerdown timeout reached."); synchronized (this) { - powerdownTimer = null; if (suspendedStop != null) { suspendedStop.resumeHandling(); suspendedStop = null; } } - }, waitUntil); + }, powerdownStartedAt.plusSeconds(powerdownTimeout)); powerdownConfirmed = true; } } - /** - * On process exited. - * - * @param event the event - */ - @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onProcessExited(ProcessExited event) { - if (!event.startedBy().associated(CommandDefinition.class) - .map(cd -> ProcessName.QEMU.equals(cd.name())).orElse(false)) { - return; - } - synchronized (this) { - if (powerdownTimer != null) { - powerdownTimer.cancel(); - } - if (suspendedStop != null) { - suspendedStop.resumeHandling(); - suspendedStop = null; - } - } - } - /** * On configure qemu. * * @param event the event */ @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onConfigureQemu(ConfigureQemu event) { + public void onConfigureQemu(RunnerConfigurationUpdate event) { int newTimeout = event.configuration().vm.powerdownTimeout; if (powerdownTimeout != newTimeout) { powerdownTimeout = newTimeout; diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/RamController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/RamController.java index 81a10f9..05fdde6 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/RamController.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/RamController.java @@ -21,8 +21,8 @@ package org.jdrupes.vmoperator.runner.qemu; import java.math.BigInteger; import java.util.Optional; import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetBalloon; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; import org.jgrapes.core.annotation.Handler; @@ -39,6 +39,7 @@ public class RamController extends Component { * * @param componentChannel the component channel */ + @SuppressWarnings("PMD.AssignmentToNonFinalStatic") public RamController(Channel componentChannel) { super(componentChannel); } @@ -49,7 +50,7 @@ public class RamController extends Component { * @param event the event */ @Handler - public void onConfigureQemu(ConfigureQemu event) { + public void onConfigureQemu(RunnerConfigurationUpdate event) { Optional.ofNullable(event.configuration().vm.currentRam) .ifPresent(cr -> { if (currentRam != null && currentRam.equals(cr)) { diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java index 4819dcd..922f2af 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp + * Copyright (C) 2023 Michael N. Lipp * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -41,32 +41,26 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; import java.util.Comparator; -import java.util.EnumSet; import java.util.HashMap; +import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -import org.jdrupes.vmoperator.runner.qemu.Constants.ProcessName; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCont; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpReset; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.Exit; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; -import org.jdrupes.vmoperator.runner.qemu.events.OsinfoEvent; import org.jdrupes.vmoperator.runner.qemu.events.QmpConfigured; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange; -import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; import org.jdrupes.vmoperator.util.ExtendedObjectWrapper; import org.jdrupes.vmoperator.util.FsdUtils; import org.jgrapes.core.Channel; @@ -149,23 +143,14 @@ import org.jgrapes.util.events.WatchFile; * waitForConfigured: entry/fire QmpCapabilities * waitForConfigured --> configure: QmpConfigured * - * configure: entry/fire ConfigureQemu - * configure --> success: ConfigureQemu (last handler)/fire cont command + * configure: entry/fire RunnerConfigurationUpdate + * configure --> success: RunnerConfigurationUpdate (last handler)/fire cont command * } * * Initializing --> prepFork: Started * * success --> Running * - * state Running { - * state Booting - * state Booted - * - * [*] -right-> Booting - * Booting -down-> Booting: VserportChanged[guest agent connected]/fire GetOsinfo - * Booting --> Booted: Osinfo - * } - * * state Terminating { * state terminate <> * state qemuRunning <> @@ -201,9 +186,12 @@ import org.jgrapes.util.events.WatchFile; * */ @SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace", - "PMD.TooManyMethods", "PMD.CouplingBetweenObjects" }) + "PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods" }) public class Runner extends Component { + private static final String QEMU = "qemu"; + private static final String SWTPM = "swtpm"; + private static final String CLOUD_INIT_IMG = "cloudInitImg"; private static final String TEMPLATE_DIR = "/opt/" + APP_NAME.replace("-", "") + "/templates"; private static final String DEFAULT_TEMPLATE @@ -212,25 +200,20 @@ public class Runner extends Component { private static final String FW_VARS = "fw-vars.fd"; private static int exitStatus; - private final EventPipeline rep = newEventPipeline(); + private EventPipeline rep; private final ObjectMapper yamlMapper = new ObjectMapper(YAMLFactory .builder().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) .build()); private final JsonNode defaults; + @SuppressWarnings("PMD.UseConcurrentHashMap") private final File configFile; - private final Path configDir; - private Configuration initialConfig; - private Configuration pendingConfig; + private Configuration config = new Configuration(); private final freemarker.template.Configuration fmConfig; private CommandDefinition swtpmDefinition; private CommandDefinition cloudInitImgDefinition; private CommandDefinition qemuDefinition; private final QemuMonitor qemuMonitor; - private boolean qmpConfigured; - private final GuestAgentClient guestAgentClient; - private final VmopAgentClient vmopAgentClient; - private Integer resetCounter; - private RunState state = RunState.INITIALIZING; + private State state = State.INITIALIZING; /** Preparatory actions for QEMU start */ @SuppressWarnings("PMD.FieldNamingConventions") @@ -240,7 +223,7 @@ public class Runner extends Component { CloudInit } - private final Set qemuLatch = EnumSet.noneOf(QemuPreps.class); + private final Set qemuLatch = new HashSet<>(); /** * Instantiates a new runner. @@ -248,7 +231,7 @@ public class Runner extends Component { * @param cmdLine the cmd line * @throws IOException Signals that an I/O exception has occurred. */ - @SuppressWarnings({ "PMD.ConstructorCallsOverridableMethod" }) + @SuppressWarnings("PMD.SystemPrintln") public Runner(CommandLine cmdLine) throws IOException { yamlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); @@ -257,17 +240,6 @@ public class Runner extends Component { defaults = yamlMapper.readValue( Runner.class.getResourceAsStream("defaults.yaml"), JsonNode.class); - // Get the config - configFile = new File(cmdLine.getOptionValue('c', - "/etc/opt/" + APP_NAME.replace("-", "") + "/config.yaml")); - // Don't rely on night config to produce a good exception - // for this simple case - if (!Files.isReadable(configFile.toPath())) { - throw new IOException( - "Cannot read configuration file " + configFile); - } - configDir = configFile.getParentFile().toPath().toRealPath(); - // Configure freemarker library fmConfig = new freemarker.template.Configuration( freemarker.template.Configuration.VERSION_2_3_32); @@ -284,10 +256,17 @@ public class Runner extends Component { attach(new FileSystemWatcher(channel())); attach(new ProcessManager(channel())); attach(new SocketConnector(channel())); - attach(qemuMonitor = new QemuMonitor(channel(), configDir)); - attach(guestAgentClient = new GuestAgentClient(channel())); - attach(vmopAgentClient = new VmopAgentClient(channel())); + attach(qemuMonitor = new QemuMonitor(channel())); attach(new StatusUpdater(channel())); + + configFile = new File(cmdLine.getOptionValue('c', + "/etc/opt/" + APP_NAME.replace("-", "") + "/config.yaml")); + // Don't rely on night config to produce a good exception + // for this simple case + if (!Files.isReadable(configFile.toPath())) { + throw new IOException( + "Cannot read configuration file " + configFile); + } attach(new YamlConfigurationStore(channel(), configFile, false)); fire(new WatchFile(configFile.toPath())); } @@ -307,84 +286,60 @@ public class Runner extends Component { } /** - * Process the initial configuration. The initial configuration - * and any subsequent updates will be forwarded to other components - * only when the QMP connection is ready - * (see @link #onQmpConfigured(QmpConfigured)). + * On configuration update. * * @param event the event */ @Handler public void onConfigurationUpdate(ConfigurationUpdate event) { event.structured(componentPath()).ifPresent(c -> { - logger.fine(() -> "Runner configuratation updated"); var newConf = yamlMapper.convertValue(c, Configuration.class); - - // Add some values from other sources to configuration newConf.asOf = Instant.ofEpochSecond(configFile.lastModified()); - Path dsPath = configDir.resolve(DisplaySecret.PASSWORD); - newConf.hasDisplayPassword = dsPath.toFile().canRead(); - - // Special actions for initial configuration (startup) if (event instanceof InitialConfiguration) { processInitialConfiguration(newConf); + return; } - - // Check if to be sent immediately or later - if (qmpConfigured) { - rep.fire(new ConfigureQemu(newConf, state)); - } else { - pendingConfig = newConf; - } + logger.fine(() -> "Updating configuration"); + rep.fire(new RunnerConfigurationUpdate(newConf, state)); }); } - @SuppressWarnings("PMD.LambdaCanBeMethodReference") private void processInitialConfiguration(Configuration newConfig) { try { - if (!newConfig.check()) { + config = newConfig; + if (!config.check()) { // Invalid configuration, not used, problems already logged. - return; + config = null; } // Prepare firmware files and add to config - setFirmwarePaths(newConfig); + setFirmwarePaths(); // Obtain more context data from template - var tplData = dataFromTemplate(newConfig); - initialConfig = newConfig; - - // Configure - swtpmDefinition - = Optional.ofNullable(tplData.get(ProcessName.SWTPM)) - .map(d -> new CommandDefinition(ProcessName.SWTPM, d)) - .orElse(null); - logger.finest(() -> swtpmDefinition.toString()); - qemuDefinition = Optional.ofNullable(tplData.get(ProcessName.QEMU)) - .map(d -> new CommandDefinition(ProcessName.QEMU, d)) - .orElse(null); - logger.finest(() -> qemuDefinition.toString()); + var tplData = dataFromTemplate(); + swtpmDefinition = Optional.ofNullable(tplData.get(SWTPM)) + .map(d -> new CommandDefinition(SWTPM, d)).orElse(null); + qemuDefinition = Optional.ofNullable(tplData.get(QEMU)) + .map(d -> new CommandDefinition(QEMU, d)).orElse(null); cloudInitImgDefinition - = Optional.ofNullable(tplData.get(ProcessName.CLOUD_INIT_IMG)) - .map(d -> new CommandDefinition(ProcessName.CLOUD_INIT_IMG, - d)) + = Optional.ofNullable(tplData.get(CLOUD_INIT_IMG)) + .map(d -> new CommandDefinition(CLOUD_INIT_IMG, d)) .orElse(null); - logger.finest(() -> cloudInitImgDefinition.toString()); // Forward some values to child components - qemuMonitor.configure(initialConfig.monitorSocket, - initialConfig.vm.powerdownTimeout); - guestAgentClient.configureConnection(qemuDefinition.command, - "guest-agent-socket"); - vmopAgentClient.configureConnection(qemuDefinition.command, - "vmop-agent-socket"); + qemuMonitor.configure(config.monitorSocket, + config.vm.powerdownTimeout); } catch (IllegalArgumentException | IOException | TemplateException e) { logger.log(Level.SEVERE, e, () -> "Invalid configuration: " + e.getMessage()); + // Don't use default configuration + config = null; } } - private void setFirmwarePaths(Configuration config) throws IOException { + @SuppressWarnings({ "PMD.CognitiveComplexity", + "PMD.DataflowAnomalyAnalysis" }) + private void setFirmwarePaths() throws IOException { JsonNode firmware = defaults.path("firmware").path(config.vm.firmware); // Get file for firmware ROM JsonNode codePaths = firmware.path("rom"); @@ -395,12 +350,6 @@ public class Runner extends Component { break; } } - if (codePaths.iterator().hasNext() && config.firmwareRom == null) { - throw new IllegalArgumentException("No ROM found, candidates were: " - + StreamSupport.stream(codePaths.spliterator(), false) - .map(JsonNode::asText).collect(Collectors.joining(", "))); - } - // Get file for firmware vars, if necessary config.firmwareVars = config.dataDir.resolve(FW_VARS); if (!Files.exists(config.firmwareVars)) { @@ -414,7 +363,7 @@ public class Runner extends Component { } } - private JsonNode dataFromTemplate(Configuration config) + private JsonNode dataFromTemplate() throws IOException, TemplateNotFoundException, MalformedTemplateNameException, ParseException, TemplateException, JsonProcessingException, JsonMappingException { @@ -439,35 +388,21 @@ public class Runner extends Component { .map(Object::toString).orElse(null)); model.put("firmwareVars", Optional.ofNullable(config.firmwareVars) .map(Object::toString).orElse(null)); - model.put("hasDisplayPassword", config.hasDisplayPassword); model.put("cloudInit", config.cloudInit); model.put("vm", config.vm); - logger.finest(() -> "Processing template with model: " + model); + if (Optional.ofNullable(config.vm.display) + .map(d -> d.spice).map(s -> s.ticket).isPresent()) { + model.put("ticketPath", config.runtimeDir.resolve("ticket.txt")); + } // Combine template and data and parse result // (tempting, but no need to use a pipe here) var fmTemplate = fmConfig.getTemplate(templatePath.toString()); StringWriter out = new StringWriter(); fmTemplate.process(model, out); - logger.finest(() -> "Result of processing template: " + out); return yamlMapper.readValue(out.toString(), JsonNode.class); } - /** - * Note ready state and send a {@link ConfigureQemu} event for - * any pending configuration (initial or change). - * - * @param event the event - */ - @Handler - public void onQmpConfigured(QmpConfigured event) { - qmpConfigured = true; - if (pendingConfig != null) { - rep.fire(new ConfigureQemu(pendingConfig, state)); - pendingConfig = null; - } - } - /** * Handle the start event. * @@ -475,7 +410,7 @@ public class Runner extends Component { */ @Handler(priority = 100) public void onStart(Start event) { - if (initialConfig == null) { + if (config == null) { // Missing configuration, fail event.cancel(true); fire(new Stop()); @@ -486,24 +421,25 @@ public class Runner extends Component { // https://github.com/kubernetes-client/java/issues/100 io.kubernetes.client.openapi.Configuration.setDefaultApiClient(null); - // Provide specific event pipeline to avoid concurrency. + // Prepare specific event pipeline to avoid concurrency. + rep = newEventPipeline(); event.setAssociated(EventPipeline.class, rep); try { // Store process id try (var pidFile = Files.newBufferedWriter( - initialConfig.runtimeDir.resolve("runner.pid"))) { + config.runtimeDir.resolve("runner.pid"))) { pidFile.write(ProcessHandle.current().pid() + "\n"); } // Files to watch for - Files.deleteIfExists(initialConfig.swtpmSocket); - fire(new WatchFile(initialConfig.swtpmSocket)); + Files.deleteIfExists(config.swtpmSocket); + fire(new WatchFile(config.swtpmSocket)); // Helper files - var ticket = Optional.ofNullable(initialConfig.vm.display) + var ticket = Optional.ofNullable(config.vm.display) .map(d -> d.spice).map(s -> s.ticket); if (ticket.isPresent()) { - Files.write(initialConfig.runtimeDir.resolve("ticket.txt"), + Files.write(config.runtimeDir.resolve("ticket.txt"), ticket.get().getBytes()); } } catch (IOException e) { @@ -520,23 +456,22 @@ public class Runner extends Component { */ @Handler public void onStarted(Started event) { - state = RunState.STARTING; + state = State.STARTING; rep.fire(new RunnerStateChange(state, "RunnerStarted", "Runner has been started")); // Start first process(es) qemuLatch.add(QemuPreps.Config); - if (initialConfig.vm.useTpm && swtpmDefinition != null) { + if (config.vm.useTpm && swtpmDefinition != null) { startProcess(swtpmDefinition); qemuLatch.add(QemuPreps.Tpm); } - if (initialConfig.cloudInit != null) { - generateCloudInitImg(initialConfig); + if (config.cloudInit != null) { + generateCloudInitImg(); qemuLatch.add(QemuPreps.CloudInit); } mayBeStartQemu(QemuPreps.Config); } - @SuppressWarnings("PMD.AvoidSynchronizedStatement") private void mayBeStartQemu(QemuPreps done) { synchronized (qemuLatch) { if (qemuLatch.isEmpty()) { @@ -549,31 +484,31 @@ public class Runner extends Component { } } - private void generateCloudInitImg(Configuration config) { + private void generateCloudInitImg() { try { var cloudInitDir = config.dataDir.resolve("cloud-init"); cloudInitDir.toFile().mkdir(); - try (var metaOut - = Files.newBufferedWriter(cloudInitDir.resolve("meta-data"))) { - if (config.cloudInit.metaData != null) { - yamlMapper.writer().writeValue(metaOut, - config.cloudInit.metaData); - } + var metaOut + = Files.newBufferedWriter(cloudInitDir.resolve("meta-data")); + if (config.cloudInit.metaData != null) { + yamlMapper.writer().writeValue(metaOut, + config.cloudInit.metaData); } - try (var userOut - = Files.newBufferedWriter(cloudInitDir.resolve("user-data"))) { - userOut.write("#cloud-config\n"); - if (config.cloudInit.userData != null) { - yamlMapper.writer().writeValue(userOut, - config.cloudInit.userData); - } + metaOut.close(); + var userOut + = Files.newBufferedWriter(cloudInitDir.resolve("user-data")); + userOut.write("#cloud-config\n"); + if (config.cloudInit.userData != null) { + yamlMapper.writer().writeValue(userOut, + config.cloudInit.userData); } + userOut.close(); if (config.cloudInit.networkConfig != null) { - try (var networkConfig = Files.newBufferedWriter( - cloudInitDir.resolve("network-config"))) { - yamlMapper.writer().writeValue(networkConfig, - config.cloudInit.networkConfig); - } + var networkConfig = Files.newBufferedWriter( + cloudInitDir.resolve("network-config")); + yamlMapper.writer().writeValue(networkConfig, + config.cloudInit.networkConfig); + networkConfig.close(); } startProcess(cloudInitImgDefinition); } catch (IOException e) { @@ -586,7 +521,7 @@ public class Runner extends Component { private boolean startProcess(CommandDefinition toStart) { logger.info( () -> "Starting process: " + String.join(" ", toStart.command)); - rep.fire(new StartProcess(toStart.command) + fire(new StartProcess(toStart.command) .setAssociated(CommandDefinition.class, toStart)); return true; } @@ -600,9 +535,10 @@ public class Runner extends Component { @Handler public void onFileChanged(FileChanged event) { if (event.change() == Kind.CREATED - && event.path().equals(initialConfig.swtpmSocket)) { + && event.path().equals(config.swtpmSocket)) { // swtpm running, maybe start qemu mayBeStartQemu(QemuPreps.Tpm); + return; } } @@ -615,13 +551,15 @@ public class Runner extends Component { * @throws InterruptedException the interrupted exception */ @Handler + @SuppressWarnings({ "PMD.SwitchStmtsShouldHaveDefault", + "PMD.TooFewBranchesForASwitchStatement" }) public void onProcessStarted(ProcessStarted event, ProcessChannel channel) throws InterruptedException { event.startEvent().associated(CommandDefinition.class) .ifPresent(procDef -> { channel.setAssociated(CommandDefinition.class, procDef); try (var pidFile = Files.newBufferedWriter( - initialConfig.runtimeDir.resolve(procDef.name + ".pid"))) { + config.runtimeDir.resolve(procDef.name + ".pid"))) { pidFile.write(channel.process().toHandle().pid() + "\n"); } catch (IOException e) { throw new UndeclaredThrowableException(e); @@ -654,53 +592,30 @@ public class Runner extends Component { } /** - * Whenever a new QEMU configuration is available, check if it - * is supposed to trigger a reset. + * On monitor ready. * * @param event the event */ @Handler - public void onConfigureQemu(ConfigureQemu event) { - if (state.vmActive()) { - if (resetCounter != null - && event.configuration().resetCounter != null - && event.configuration().resetCounter > resetCounter) { - fire(new MonitorCommand(new QmpReset())); - } - resetCounter = event.configuration().resetCounter; - } + public void onQmpConfigured(QmpConfigured event) { + rep.fire(new RunnerConfigurationUpdate(config, state)); } /** - * As last step when handling a new configuration, check if - * QEMU is suspended after startup and should be continued. - * + * On configure qemu. + * * @param event the event */ @Handler(priority = -1000) - public void onConfigureQemuFinal(ConfigureQemu event) { - if (state == RunState.STARTING) { - state = RunState.BOOTING; + public void onConfigureQemu(RunnerConfigurationUpdate event) { + if (state == State.STARTING) { fire(new MonitorCommand(new QmpCont())); + state = State.RUNNING; rep.fire(new RunnerStateChange(state, "VmStarted", "Qemu has been configured and is continuing")); } } - /** - * Receiving the OSinfo means that the OS has been booted. - * - * @param event the event - */ - @Handler - public void onOsinfo(OsinfoEvent event) { - if (state == RunState.BOOTING) { - state = RunState.BOOTED; - rep.fire(new RunnerStateChange(state, "VmBooted", - "The VM has started the guest agent.")); - } - } - /** * On process exited. * @@ -716,18 +631,15 @@ public class Runner extends Component { mayBeStartQemu(QemuPreps.CloudInit); return; } - // No other process(es) may exit during startup - if (state == RunState.STARTING) { + if (state == State.STARTING) { logger.severe(() -> "Process " + procDef.name + " has exited with value " + event.exitValue() + " during startup."); rep.fire(new Stop()); return; } - - // No processes may exit while the VM is running normally - if (procDef.equals(qemuDefinition) && state.vmActive()) { + if (procDef.equals(qemuDefinition) && state == State.RUNNING) { rep.fire(new Exit(event.exitValue())); } logger.info(() -> "Process " + procDef.name @@ -754,7 +666,7 @@ public class Runner extends Component { */ @Handler(priority = 10_000) public void onStopFirst(Stop event) { - state = RunState.TERMINATING; + state = State.TERMINATING; rep.fire(new RunnerStateChange(state, "VmTerminating", "The VM is being shut down", exitStatus != 0)); } @@ -766,13 +678,13 @@ public class Runner extends Component { */ @Handler(priority = -10_000) public void onStopLast(Stop event) { - state = RunState.STOPPED; + state = State.STOPPED; rep.fire(new RunnerStateChange(state, "VmStopped", "The VM has been shut down")); } private void shutdown() { - if (!Set.of(RunState.TERMINATING, RunState.STOPPED).contains(state)) { + if (!Set.of(State.TERMINATING, State.STOPPED).contains(state)) { fire(new Stop()); } try { @@ -781,7 +693,7 @@ public class Runner extends Component { logger.log(Level.WARNING, e, () -> "Proper shutdown failed."); } - Optional.ofNullable(initialConfig).map(c -> c.runtimeDir) + Optional.ofNullable(config).map(c -> c.runtimeDir) .ifPresent(runtimeDir -> { try { Files.walk(runtimeDir).sorted(Comparator.reverseOrder()) @@ -805,10 +717,6 @@ public class Runner extends Component { props = Runner.class.getResourceAsStream("logging.properties"); } LogManager.getLogManager().readConfiguration(props); - Logger.getLogger(Runner.class.getName()).log(Level.CONFIG, - () -> path.isPresent() - ? "Using logging configuration from " + path.get() - : "Using default logging configuration"); } catch (IOException e) { e.printStackTrace(); } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java index 127c070..1cb5e74 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp + * Copyright (C) 2023 Michael N. Lipp * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,71 +18,68 @@ package org.jdrupes.vmoperator.runner.qemu; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.google.gson.Gson; import com.google.gson.JsonObject; -import io.kubernetes.client.apimachinery.GroupVersionKind; import io.kubernetes.client.custom.Quantity; import io.kubernetes.client.custom.Quantity.Format; import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.ApisApi; +import io.kubernetes.client.openapi.apis.CustomObjectsApi; +import io.kubernetes.client.openapi.apis.EventsV1Api; import io.kubernetes.client.openapi.models.EventsV1Event; +import io.kubernetes.client.openapi.models.V1APIGroup; +import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.util.Config; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.math.BigDecimal; -import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Instant; +import java.time.OffsetDateTime; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.Constants.Status.Condition; -import org.jdrupes.vmoperator.common.Constants.Status.Condition.Reason; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; -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.OsinfoEvent; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange; -import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedIn; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedOut; import org.jdrupes.vmoperator.util.GsonPtr; import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Components.Timer; +import org.jgrapes.core.Component; import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.events.HandlingError; import org.jgrapes.core.events.Start; +import org.jgrapes.util.events.ConfigurationUpdate; +import org.jgrapes.util.events.InitialConfiguration; /** * Updates the CR status. */ -@SuppressWarnings({ "PMD.CouplingBetweenObjects" }) -public class StatusUpdater extends VmDefUpdater { +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class StatusUpdater extends Component { - @SuppressWarnings("PMD.FieldNamingConventions") - private static final Gson gson = new JSON().getGson(); - @SuppressWarnings("PMD.FieldNamingConventions") - private static final ObjectMapper objectMapper - = new ObjectMapper().registerModule(new JavaTimeModule()); + private static final Set RUNNING_STATES + = Set.of(State.RUNNING, State.TERMINATING); + private String namespace; + private String vmName; + private DynamicKubernetesApi vmCrApi; + private EventsV1Api evtsApi; + private long observedGeneration; private boolean guestShutdownStops; private boolean shutdownByGuest; - private VmDefinitionStub vmStub; - private String loggedInUser; - private BigInteger lastRamValue; - private Instant lastRamChange; - private Timer balloonTimer; - private BigInteger targetRamValue; /** * Instantiates a new status updater. @@ -91,7 +88,6 @@ public class StatusUpdater extends VmDefUpdater { */ public StatusUpdater(Channel componentChannel) { super(componentChannel); - attach(new ConsoleTracker(componentChannel)); } /** @@ -108,6 +104,43 @@ public class StatusUpdater extends VmDefUpdater { } } + /** + * On configuration update. + * + * @param event the event + */ + @Handler + @SuppressWarnings("unchecked") + public void onConfigurationUpdate(ConfigurationUpdate event) { + event.structured("/Runner").ifPresent(c -> { + if (event instanceof InitialConfiguration) { + namespace = (String) c.get("namespace"); + updateNamespace(); + vmName = Optional.ofNullable((Map) c.get("vm")) + .map(vm -> vm.get("name")).orElse(null); + } + }); + } + + private void updateNamespace() { + if (namespace == null) { + var path = Path + .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); + if (Files.isReadable(path)) { + try { + namespace = Files.lines(path).findFirst().orElse(null); + } catch (IOException e) { + logger.log(Level.WARNING, e, + () -> "Cannot read namespace."); + } + } + } + if (namespace == null) { + logger.warning(() -> "Namespace is unknown, some functions" + + " won't be available."); + } + } + /** * Handle the start event. * @@ -121,27 +154,59 @@ public class StatusUpdater extends VmDefUpdater { return; } try { - vmStub = VmDefinitionStub.get(apiClient, - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - namespace, vmName); - var vmDef = vmStub.model().orElse(null); - if (vmDef == null) { - return; - } - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.RUNNER_VERSION, Optional.ofNullable( - Runner.class.getPackage().getImplementationVersion()) - .orElse("(unknown)")); - status.remove(Status.LOGGED_IN_USER); - return status; - }); - } catch (ApiException e) { + initVmCrApi(event); + } catch (IOException | ApiException e) { logger.log(Level.SEVERE, e, - () -> "Cannot access VM object, terminating."); + () -> "Cannot access VM's CR, terminating."); event.cancel(true); fire(new Exit(1)); } + try { + evtsApi = new EventsV1Api(Config.defaultClient()); + } catch (IOException e) { + logger.log(Level.SEVERE, e, + () -> "Cannot access events API, terminating."); + event.cancel(true); + fire(new Exit(1)); + } + } + + private void initVmCrApi(Start event) throws IOException, ApiException { + var client = Config.defaultClient(); + var apis = new ApisApi(client).getAPIVersions(); + var crdVersions = apis.getGroups().stream() + .filter(g -> g.getName().equals(VM_OP_GROUP)).findFirst() + .map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream()) + .map(V1GroupVersionForDiscovery::getVersion).toList(); + var coa = new CustomObjectsApi(client); + for (var crdVersion : crdVersions) { + var crdApiRes = coa.getAPIResources(VM_OP_GROUP, + crdVersion).getResources().stream() + .filter(r -> VM_OP_KIND_VM.equals(r.getKind())).findFirst(); + if (crdApiRes.isEmpty()) { + continue; + } + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var crApi = new DynamicKubernetesApi(VM_OP_GROUP, + crdVersion, crdApiRes.get().getName(), client); + var vmCr = crApi.get(namespace, vmName); + if (vmCr.isSuccess()) { + vmCrApi = crApi; + observedGeneration + = vmCr.getObject().getMetadata().getGeneration(); + break; + } + } + if (vmCrApi == null) { + logger.severe(() -> "Cannot find VM's CR, terminating."); + event.cancel(true); + fire(new Exit(1)); + } + } + + @SuppressWarnings("PMD.AvoidDuplicateLiterals") + private JsonObject currentStatus(DynamicKubernetesObject vmCr) { + return vmCr.getRaw().getAsJsonObject("status").deepCopy(); } /** @@ -151,28 +216,28 @@ public class StatusUpdater extends VmDefUpdater { * @throws ApiException */ @Handler - public void onConfigureQemu(ConfigureQemu event) + public void onRunnerConfigurationUpdate(RunnerConfigurationUpdate event) throws ApiException { guestShutdownStops = event.configuration().guestShutdownStops; - loggedInUser = event.configuration().vm.display.loggedInUser; - targetRamValue = event.configuration().vm.currentRam; // Remainder applies only if we have a connection to k8s. - if (vmStub == null) { + if (vmCrApi == null) { return; } - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - if (!event.configuration().hasDisplayPassword) { - status.addProperty(Status.DISPLAY_PASSWORD_SERIAL, -1); - } + // A change of the runner configuration is typically caused + // by a new version of the CR. So we observe the new CR. + var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() + .getObject(); + if (vmCr.getMetadata().getGeneration() == observedGeneration) { + return; + } + vmCrApi.updateStatus(vmCr, from -> { + JsonObject status = currentStatus(from); status.getAsJsonArray("conditions").asList().stream() - .map(cond -> (JsonObject) cond) - .filter(cond -> Condition.RUNNING + .map(cond -> (JsonObject) cond).filter(cond -> "Running" .equals(cond.get("type").getAsString())) .forEach(cond -> cond.addProperty("observedGeneration", from.getMetadata().getGeneration())); - updateUserLoggedIn(from); return status; }); } @@ -184,146 +249,105 @@ public class StatusUpdater extends VmDefUpdater { * @throws ApiException */ @Handler - @SuppressWarnings({ "PMD.AssignmentInOperand" }) public void onRunnerStateChanged(RunnerStateChange event) throws ApiException { - VmDefinition vmDef; - if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) { + if (vmCrApi == null) { return; } - vmStub.updateStatus(from -> { - boolean running = event.runState().vmRunning(); - updateCondition(vmDef, Condition.RUNNING, running, event.reason(), - event.message()); - JsonObject status = updateCondition(vmDef, Condition.BOOTED, - event.runState() == RunState.BOOTED, event.reason(), - event.message()); - if (event.runState() == RunState.STARTING) { - status.addProperty(Status.RAM, GsonPtr.to(from.data()) + var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() + .getObject(); + vmCrApi.updateStatus(vmCr, from -> { + JsonObject status = currentStatus(from); + status.getAsJsonArray("conditions").asList().stream() + .map(cond -> (JsonObject) cond) + .forEach(cond -> { + if ("Running".equals(cond.get("type").getAsString())) { + updateRunningCondition(event, from, cond); + } + }); + if (event.state() == State.STARTING) { + status.addProperty("ram", GsonPtr.to(from.getRaw()) .getAsString("spec", "vm", "maximumRam").orElse("0")); - status.addProperty(Status.CPUS, 1); - } else if (event.runState() == RunState.STOPPED) { - status.addProperty(Status.RAM, "0"); - status.addProperty(Status.CPUS, 0); - status.remove(Status.LOGGED_IN_USER); - } - - if (!running) { - // In case console connection was still present - status.addProperty(Status.CONSOLE_CLIENT, ""); - updateCondition(from, Condition.CONSOLE_CONNECTED, false, - "VmStopped", - "The VM is not running"); - - // In case we had an irregular shutdown - updateCondition(from, Condition.USER_LOGGED_IN, false, - "VmStopped", "The VM is not running"); - status.remove(Status.OSINFO); - updateCondition(vmDef, "VmopAgentConnected", false, "VmStopped", - "The VM is not running"); + status.addProperty("cpus", 1); + } else if (event.state() == State.STOPPED) { + status.addProperty("ram", "0"); + status.addProperty("cpus", 0); } return status; - }, vmDef); + }).throwsApiException(); // Maybe stop VM - if (event.runState() == RunState.TERMINATING && !event.failed() + if (event.state() == State.TERMINATING && !event.failed() && guestShutdownStops && shutdownByGuest) { logger.info(() -> "Stopping VM because of shutdown by guest."); - var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, + PatchOptions patchOpts = new PatchOptions(); + patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); + var res = vmCrApi.patch(namespace, vmName, + V1Patch.PATCH_FORMAT_JSON_PATCH, new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state" + "\", \"value\": \"Stopped\"}]"), - apiClient.defaultPatchOptions()); - if (!res.isPresent()) { + patchOpts); + if (!res.isSuccess()) { logger.warning( - () -> "Cannot patch pod annotations for: " + vmStub.name()); + () -> "Cannot patch pod annotations: " + res.getStatus()); } } // Log event - var evt = new EventsV1Event() - .reportingController(Crd.GROUP + "/" + APP_NAME) + var evt = new EventsV1Event().kind("Event") + .metadata(new V1ObjectMeta().namespace(namespace) + .generateName("vmrunner-")) + .reportingController(VM_OP_GROUP + "/" + APP_NAME) + .reportingInstance(vmCr.getMetadata().getName()) + .eventTime(OffsetDateTime.now()).type("Normal") + .regarding(K8s.objectReference(vmCr)) .action("StatusUpdate").reason(event.reason()) .note(event.message()); - K8s.createEvent(apiClient, vmDef, evt); + evtsApi.createNamespacedEvent(namespace, evt, null, null, null, null); } - private void updateUserLoggedIn(VmDefinition from) { - if (loggedInUser == null) { - updateCondition(from, Condition.USER_LOGGED_IN, false, - Reason.NOT_REQUESTED, "No user to be logged in"); - return; + private void updateRunningCondition(RunnerStateChange event, + DynamicKubernetesObject from, JsonObject cond) { + boolean reportedRunning + = "True".equals(cond.get("status").getAsString()); + if (RUNNING_STATES.contains(event.state()) + && !reportedRunning) { + cond.addProperty("status", "True"); + cond.addProperty("lastTransitionTime", + Instant.now().toString()); } - if (!from.conditionStatus(Condition.VMOP_AGENT).orElse(false)) { - updateCondition(from, Condition.USER_LOGGED_IN, false, - "VmopAgentDisconnected", "Waiting for VMOP agent to connect"); - return; + if (!RUNNING_STATES.contains(event.state()) + && reportedRunning) { + cond.addProperty("status", "False"); + cond.addProperty("lastTransitionTime", + Instant.now().toString()); } - if (!from.fromStatus(Status.LOGGED_IN_USER).map(loggedInUser::equals) - .orElse(false)) { - updateCondition(from, Condition.USER_LOGGED_IN, false, - "Processing", "Waiting for user to be logged in"); - } - updateCondition(from, Condition.USER_LOGGED_IN, true, - Reason.LOGGED_IN, "User is logged in"); + cond.addProperty("reason", event.reason()); + cond.addProperty("message", event.message()); + cond.addProperty("observedGeneration", + from.getMetadata().getGeneration()); } /** - * Update the current RAM size in the status. Balloon changes happen - * more than once every second during changes. While this is nice - * to watch, this puts a heavy load on the system. Therefore we - * only update the status once every 15 seconds or when the target - * value is reached. + * On ballon change. * * @param event the event * @throws ApiException */ @Handler public void onBallonChange(BalloonChangeEvent event) throws ApiException { - if (vmStub == null) { + if (vmCrApi == null) { return; } - Instant now = Instant.now(); - if (lastRamChange == null - || lastRamChange.isBefore(now.minusSeconds(15)) - || event.size().equals(targetRamValue)) { - if (balloonTimer != null) { - balloonTimer.cancel(); - balloonTimer = null; - } - lastRamChange = now; - lastRamValue = event.size(); - updateRam(); - return; - } - - // Save for later processing and maybe start timer - lastRamChange = now; - lastRamValue = event.size(); - if (balloonTimer != null) { - return; - } - final var pipeline = activeEventPipeline(); - balloonTimer = Components.schedule(t -> { - pipeline.submit("Update RAM size", () -> { - try { - updateRam(); - } catch (ApiException e) { - logger.log(Level.WARNING, e, - () -> "Failed to update ram size: " + e.getMessage()); - } - balloonTimer = null; - }); - }, now.plusSeconds(15)); - } - - private void updateRam() throws ApiException { - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.RAM, - new Quantity(new BigDecimal(lastRamValue), Format.BINARY_SI) + var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() + .getObject(); + vmCrApi.updateStatus(vmCr, from -> { + JsonObject status = currentStatus(from); + status.addProperty("ram", + new Quantity(new BigDecimal(event.size()), Format.BINARY_SI) .toSuffixedString()); return status; - }); + }).throwsApiException(); } /** @@ -334,34 +358,16 @@ public class StatusUpdater extends VmDefUpdater { */ @Handler public void onCpuChange(HotpluggableCpuStatus event) throws ApiException { - if (vmStub == null) { + if (vmCrApi == null) { return; } - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.CPUS, event.usedCpus().size()); + var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() + .getObject(); + vmCrApi.updateStatus(vmCr, from -> { + JsonObject status = currentStatus(from); + status.addProperty("cpus", event.usedCpus().size()); return status; - }); - } - - /** - * On ballon change. - * - * @param event the event - * @throws ApiException - */ - @Handler - public void onDisplayPasswordChanged(DisplayPasswordChanged event) - throws ApiException { - if (vmStub == null) { - return; - } - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.DISPLAY_PASSWORD_SERIAL, - status.get(Status.DISPLAY_PASSWORD_SERIAL).getAsLong() + 1); - return status; - }); + }).throwsApiException(); } /** @@ -374,76 +380,4 @@ public class StatusUpdater extends VmDefUpdater { public void onShutdown(ShutdownEvent event) throws ApiException { shutdownByGuest = event.byGuest(); } - - /** - * On osinfo. - * - * @param event the event - * @throws ApiException - */ - @Handler - public void onOsinfo(OsinfoEvent event) throws ApiException { - if (vmStub == null) { - return; - } - var asGson = gson.toJsonTree( - objectMapper.convertValue(event.osinfo(), Object.class)); - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.add(Status.OSINFO, asGson); - return status; - }); - - } - - /** - * @param event the event - * @throws ApiException - */ - @Handler - @SuppressWarnings("PMD.AssignmentInOperand") - public void onVmopAgentConnected(VmopAgentConnected event) - throws ApiException { - VmDefinition vmDef; - if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) { - return; - } - vmStub.updateStatus(from -> { - var status = updateCondition(vmDef, "VmopAgentConnected", - true, "VmopAgentStarted", "The VM operator agent is running"); - updateUserLoggedIn(from); - return status; - }, vmDef); - } - - /** - * @param event the event - * @throws ApiException - */ - @Handler - public void onVmopAgentLoggedIn(VmopAgentLoggedIn event) - throws ApiException { - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.LOGGED_IN_USER, - event.triggering().user()); - updateUserLoggedIn(from); - return status; - }); - } - - /** - * @param event the event - * @throws ApiException - */ - @Handler - public void onVmopAgentLoggedOut(VmopAgentLoggedOut event) - throws ApiException { - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.remove(Status.LOGGED_IN_USER); - updateUserLoggedIn(from); - return status; - }); - } } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java deleted file mode 100644 index 406a0bc..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu; - -import com.google.gson.JsonObject; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Level; -import java.util.stream.Collectors; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sGenericStub; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.runner.qemu.events.Exit; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Component; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.util.events.ConfigurationUpdate; -import org.jgrapes.util.events.InitialConfiguration; - -/** - * Updates the CR status. - */ -public class VmDefUpdater extends Component { - - protected String namespace; - protected String vmName; - protected K8sClient apiClient; - - /** - * Instantiates a new status updater. - * - * @param componentChannel the component channel - * @throws IOException - */ - @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") - public VmDefUpdater(Channel componentChannel) { - super(componentChannel); - if (apiClient == null) { - try { - apiClient = new K8sClient(); - io.kubernetes.client.openapi.Configuration - .setDefaultApiClient(apiClient); - } catch (IOException e) { - logger.log(Level.SEVERE, e, - () -> "Cannot access events API, terminating."); - fire(new Exit(1)); - } - } - } - - /** - * On configuration update. - * - * @param event the event - */ - @Handler - @SuppressWarnings("unchecked") - public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured("/Runner").ifPresent(c -> { - if (event instanceof InitialConfiguration) { - namespace = (String) c.get("namespace"); - updateNamespace(); - vmName = Optional.ofNullable((Map) c.get("vm")) - .map(vm -> vm.get("name")).orElse(null); - } - }); - } - - private void updateNamespace() { - if (namespace == null) { - var path = Path - .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); - if (Files.isReadable(path)) { - try { - namespace = Files.lines(path).findFirst().orElse(null); - } catch (IOException e) { - logger.log(Level.WARNING, e, - () -> "Cannot read namespace."); - } - } - } - if (namespace == null) { - logger.warning(() -> "Namespace is unknown, some functions" - + " won't be available."); - } - } - - /** - * Update condition. The `from` VM definition is used to determine the - * observed generation and the current status. This method is intended - * to be called in the function passed to - * {@link K8sGenericStub#updateStatus}. - * - * @param from the VM definition - * @param type the condition type - * @param state the new state - * @param reason the reason for the change - * @param message the message - * @return the updated status - */ - protected JsonObject updateCondition(VmDefinition from, String type, - boolean state, String reason, String message) { - JsonObject status = from.statusJson(); - // Avoid redundant updates, as this may be called several times - var current = status.getAsJsonArray("conditions").asList().stream() - .map(cond -> (JsonObject) cond) - .filter(cond -> type.equals(cond.get("type").getAsString())) - .findFirst(); - var stateUnchanged = current.map(c -> c.get("status").getAsString()) - .map("True"::equals).map(s -> s == state).orElse(false); - if (stateUnchanged - && current.map(c -> c.get("reason").getAsString()) - .map(reason::equals).orElse(false) - && current.map(c -> c.get("observedGeneration").getAsLong()) - .map(from.getMetadata().getGeneration()::equals) - .orElse(false)) { - return status; - } - - // Do update - final var condition = new HashMap<>(Map.of("type", type, - "status", state ? "True" : "False", - "observedGeneration", from.getMetadata().getGeneration(), - "reason", reason, - "lastTransitionTime", stateUnchanged - ? current.get().get("lastTransitionTime").getAsString() - : Instant.now().toString())); - if (message != null) { - condition.put("message", message); - } - List toReplace = new ArrayList<>(List.of(condition)); - List newConds - = status.getAsJsonArray("conditions").asList().stream() - .map(cond -> (JsonObject) cond) - .map(cond -> type.equals(cond.get("type").getAsString()) - ? toReplace.remove(0) - : cond) - .collect(Collectors.toCollection(() -> new ArrayList<>())); - newConds.addAll(toReplace); - status.add("conditions", - apiClient.getJSON().getGson().toJsonTree(newConds)); - return status; - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmopAgentClient.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmopAgentClient.java deleted file mode 100644 index a940d73..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmopAgentClient.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu; - -import java.io.IOException; -import java.util.Deque; -import java.util.concurrent.ConcurrentLinkedDeque; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogIn; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogOut; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedIn; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedOut; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Event; -import org.jgrapes.core.annotation.Handler; - -/** - * A component that handles the communication over the vmop agent - * socket. - * - * If the log level for this class is set to fine, the messages - * exchanged on the socket are logged. - */ -public class VmopAgentClient extends AgentConnector { - - private final Deque> executing = new ConcurrentLinkedDeque<>(); - - /** - * Instantiates a new VM operator agent client. - * - * @param componentChannel the component channel - * @throws IOException Signals that an I/O exception has occurred. - */ - public VmopAgentClient(Channel componentChannel) throws IOException { - super(componentChannel); - } - - /** - * On vmop agent login. - * - * @param event the event - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onVmopAgentLogIn(VmopAgentLogIn event) throws IOException { - if (writer().isPresent()) { - logger.fine(() -> "Vmop agent handles:" + event); - executing.add(event); - logger.finer(() -> "vmop agent(out): login " + event.user()); - sendCommand("login " + event.user()); - } else { - logger - .warning(() -> "No vmop agent connection for sending " + event); - } - } - - /** - * On vmop agent logout. - * - * @param event the event - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onVmopAgentLogout(VmopAgentLogOut event) throws IOException { - if (writer().isPresent()) { - logger.fine(() -> "Vmop agent handles:" + event); - executing.add(event); - logger.finer(() -> "vmop agent(out): logout"); - sendCommand("logout"); - } - } - - @Override - @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" }) - protected void processInput(String line) throws IOException { - logger.finer(() -> "vmop agent(in): " + line); - - // Check validity - if (line.isEmpty() || !Character.isDigit(line.charAt(0))) { - logger.warning(() -> "Illegal vmop agent response: " + line); - return; - } - - // Check positive responses - if (line.startsWith("220 ")) { - var evt = new VmopAgentConnected(); - logger.fine(() -> "Vmop agent triggers " + evt); - rep().fire(evt); - return; - } - if (line.startsWith("201 ")) { - Event cmd = executing.pop(); - if (cmd instanceof VmopAgentLogIn login) { - var evt = new VmopAgentLoggedIn(login); - logger.fine(() -> "Vmop agent triggers " + evt); - rep().fire(evt); - } else { - logger.severe(() -> "Response " + line - + " does not match executing command " + cmd); - } - return; - } - if (line.startsWith("202 ")) { - Event cmd = executing.pop(); - if (cmd instanceof VmopAgentLogOut logout) { - var evt = new VmopAgentLoggedOut(logout); - logger.fine(() -> "Vmop agent triggers " + evt); - rep().fire(evt); - } else { - logger.severe(() -> "Response " + line - + "does not match executing command " + cmd); - } - return; - } - - // Ignore unhandled continuations - if (line.charAt(0) == '1') { - return; - } - - // Error - logger.warning(() -> "Error response from vmop agent: " + line); - executing.pop(); - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java index 86d92f6..e77a984 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java @@ -47,7 +47,7 @@ public class QmpAddCpu extends QmpCommand { cmd.put("execute", "device_add"); ObjectNode args = mapper.createObjectNode(); cmd.set("arguments", args); - args.setAll((ObjectNode) unused.get("props")); + args.setAll((ObjectNode) (unused.get("props"))); args.set("driver", unused.get("type")); args.put("id", cpuId); return cmd; diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCapabilities.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCapabilities.java index 918b7d5..ffd6ca6 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCapabilities.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCapabilities.java @@ -25,7 +25,8 @@ import com.fasterxml.jackson.databind.JsonNode; */ public class QmpCapabilities extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"qmp_capabilities\" }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpChangeMedium.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpChangeMedium.java index b60b619..158a318 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpChangeMedium.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpChangeMedium.java @@ -27,7 +27,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; */ public class QmpChangeMedium extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"blockdev-change-medium\",\"arguments\": {" + "\"id\": \"\",\"filename\": \"\",\"format\": \"raw\"," diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCommand.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCommand.java index 0db58e2..8a03ab0 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCommand.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCommand.java @@ -18,7 +18,6 @@ package org.jdrupes.vmoperator.runner.qemu.commands; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; @@ -30,7 +29,8 @@ import java.util.logging.Logger; */ public abstract class QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) protected static final ObjectMapper mapper = new ObjectMapper(); /** @@ -55,30 +55,4 @@ public abstract class QmpCommand { * @return the json node */ public abstract JsonNode toJson(); - - /** - * Returns the string representation. - * - * @return the string - * @throws JsonProcessingException the JSON processing exception - */ - public String asText() throws JsonProcessingException { - return mapper.writeValueAsString(toJson()); - } - - /** - * Calls {@link #asText()} but suppresses the - * {@link JsonProcessingException}. - * - * @return the string - */ - @Override - public String toString() { - try { - return asText(); - } catch (JsonProcessingException e) { - return "(no string representation)"; - } - } - } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCont.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCont.java index 0e06e34..7b1abbd 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCont.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCont.java @@ -25,7 +25,8 @@ import com.fasterxml.jackson.databind.JsonNode; */ public class QmpCont extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"cont\" }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpDelCpu.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpDelCpu.java index a97e6c6..46fba32 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpDelCpu.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpDelCpu.java @@ -27,7 +27,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; */ public class QmpDelCpu extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"device_del\", " + "\"arguments\": " + "{ \"id\": 0 } }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestGetOsinfo.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestGetOsinfo.java deleted file mode 100644 index cf4ba72..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestGetOsinfo.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.commands; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -/** - * A {@link QmpCommand} that pings the guest agent. - */ -public class QmpGuestGetOsinfo extends QmpCommand { - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "guest-get-osinfo"); - return cmd; - } - - @Override - public String toString() { - return "QmpGuestGetOsinfo()"; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestInfo.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestInfo.java deleted file mode 100644 index 75fdf73..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestInfo.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.commands; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -/** - * A {@link QmpCommand} that requests the guest info. - */ -public class QmpGuestInfo extends QmpCommand { - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "guest-info"); - return cmd; - } - - @Override - public String toString() { - return "QmpGuestInfo()"; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPing.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPing.java deleted file mode 100644 index 257c838..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPing.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.commands; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -/** - * A {@link QmpCommand} that pings the guest agent. - */ -public class QmpGuestPing extends QmpCommand { - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "guest-ping"); - return cmd; - } - - @Override - public String toString() { - return "QmpGuestPing()"; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPowerdown.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPowerdown.java deleted file mode 100644 index 04110a5..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPowerdown.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.commands; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -/** - * A {@link QmpCommand} that powers down the guest. - */ -public class QmpGuestPowerdown extends QmpCommand { - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "guest-shutdown"); - return cmd; - } - - @Override - public String toString() { - return "QmpGuestPowerdown()"; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpOpenTray.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpOpenTray.java index 88a392c..2f9ad55 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpOpenTray.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpOpenTray.java @@ -27,7 +27,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; */ public class QmpOpenTray extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"blockdev-open-tray\",\"arguments\": {" + "\"id\": \"\" } }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpPowerdown.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpPowerdown.java index dfb7d96..108a355 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpPowerdown.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpPowerdown.java @@ -25,7 +25,8 @@ import com.fasterxml.jackson.databind.JsonNode; */ public class QmpPowerdown extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"system_powerdown\" }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpQueryHotpluggableCpus.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpQueryHotpluggableCpus.java index d4fb5cc..6f87d10 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpQueryHotpluggableCpus.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpQueryHotpluggableCpus.java @@ -25,7 +25,8 @@ import com.fasterxml.jackson.databind.JsonNode; */ public class QmpQueryHotpluggableCpus extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson( "{\"execute\":\"query-hotpluggable-cpus\",\"arguments\":{}}"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpRemoveMedium.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpRemoveMedium.java index 71360cf..cc74555 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpRemoveMedium.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpRemoveMedium.java @@ -27,7 +27,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; */ public class QmpRemoveMedium extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"blockdev-remove-medium\",\"arguments\": {" + "\"id\": \"\" } }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpReset.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpReset.java deleted file mode 100644 index 5364811..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpReset.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.commands; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * A {@link QmpCommand} that send a system_reset to the VM. - */ -public class QmpReset extends QmpCommand { - - @SuppressWarnings({ "PMD.FieldNamingConventions" }) - private static final JsonNode jsonTemplate - = parseJson("{ \"execute\": \"system_reset\" }"); - - @Override - public JsonNode toJson() { - return jsonTemplate.deepCopy(); - } - - @Override - public String toString() { - return "QmpReset()"; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetBalloon.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetBalloon.java index f9d4c5d..c7f6bed 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetBalloon.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetBalloon.java @@ -28,7 +28,8 @@ import java.math.BigInteger; */ public class QmpSetBalloon extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"balloon\", " + "\"arguments\": " + "{ \"value\": 0 } }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetDisplayPassword.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetDisplayPassword.java deleted file mode 100644 index 0048b9a..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetDisplayPassword.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.commands; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; - -/** - * A {@link QmpCommand} that sets the display password. - */ -public class QmpSetDisplayPassword extends QmpCommand { - - private final String password; - private final String protocol; - - /** - * Instantiates a new command. - * - * @param protocol the protocol - * @param password the password - */ - public QmpSetDisplayPassword(String protocol, String password) { - this.protocol = protocol; - this.password = password; - } - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "set_password"); - ObjectNode args = mapper.createObjectNode(); - cmd.set("arguments", args); - args.set("protocol", new TextNode(protocol)); - args.set("password", new TextNode(password)); - return cmd; - } - - @Override - public String toString() { - try { - var json = toJson(); - ((ObjectNode) json.get("arguments")).set("password", - new TextNode("********")); - return mapper.writeValueAsString(json); - } catch (JsonProcessingException e) { - return "(no string representation)"; - } - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetPasswordExpiry.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetPasswordExpiry.java deleted file mode 100644 index 672767d..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetPasswordExpiry.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.commands; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; - -/** - * A {@link QmpCommand} that sets the password expiry. - */ -public class QmpSetPasswordExpiry extends QmpCommand { - - private final String protocol; - private final String expiry; - - /** - * Instantiates a new command. - * - * @param protocol the protocol - * @param expiry the expiry time - */ - public QmpSetPasswordExpiry(String protocol, String expiry) { - this.protocol = protocol; - this.expiry = expiry; - } - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "expire_password"); - ObjectNode args = mapper.createObjectNode(); - cmd.set("arguments", args); - args.set("protocol", new TextNode(protocol)); - args.set("time", new TextNode(expiry)); - return cmd; - } - - @Override - public String toString() { - try { - var json = toJson(); - return mapper.writeValueAsString(json); - } catch (JsonProcessingException e) { - return "(no string representation)"; - } - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/DisplayPasswordChanged.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/DisplayPasswordChanged.java deleted file mode 100644 index 0814f50..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/DisplayPasswordChanged.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; - -/** - * A {@link MonitorResult} that indicates that the display password has changed. - */ -public class DisplayPasswordChanged extends MonitorResult { - - /** - * Instantiates a new display password changed. - * - * @param command the command - * @param response the response - */ - public DisplayPasswordChanged(QmpCommand command, JsonNode response) { - super(command, response); - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/GuestAgentCommand.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/GuestAgentCommand.java deleted file mode 100644 index a1b585d..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/GuestAgentCommand.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Event; - -/** - * An {@link Event} that causes some component to send a QMP - * command to the guest agent process. - */ -public class GuestAgentCommand extends Event { - - private final QmpCommand command; - - /** - * Instantiates a new exec qmp command. - * - * @param command the command - */ - public GuestAgentCommand(QmpCommand command) { - this.command = command; - } - - /** - * Gets the command. - * - * @return the command - */ - public QmpCommand command() { - return command; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(Components.objectName(this)) - .append(" [").append(command); - if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); - } - builder.append(']'); - return builder.toString(); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java index 2ab2c5a..68641c9 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java @@ -30,9 +30,7 @@ import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; */ public class HotpluggableCpuStatus extends MonitorResult { - @SuppressWarnings("PMD.ImmutableField") private List usedCpus = new ArrayList<>(); - @SuppressWarnings("PMD.ImmutableField") private List unusedCpus = new ArrayList<>(); /** diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java index d2b5e8c..36d5b40 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java @@ -55,7 +55,8 @@ public class MonitorCommand extends Event { builder.append(Components.objectName(this)) .append(" [").append(command); if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); + builder.append(", channels="); + builder.append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java index 93e7785..ba04a26 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java @@ -20,8 +20,6 @@ package org.jdrupes.vmoperator.runner.qemu.events; import com.fasterxml.jackson.databind.JsonNode; import java.util.Optional; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; import org.jgrapes.core.Event; /** @@ -36,8 +34,7 @@ public class MonitorEvent extends Event { * The kind of monitor event. */ public enum Kind { - READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE, SHUTDOWN, - SPICE_CONNECTED, SPICE_INITIALIZED, SPICE_DISCONNECTED, VSERPORT_CHANGE + READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE, SHUTDOWN } private final Kind kind; @@ -49,9 +46,11 @@ public class MonitorEvent extends Event { * @param response the response * @return the optional */ + @SuppressWarnings("PMD.TooFewBranchesForASwitchStatement") public static Optional from(JsonNode response) { try { - var kind = Kind.valueOf(response.get("event").asText()); + var kind = MonitorEvent.Kind + .valueOf(response.get("event").asText()); switch (kind) { case POWERDOWN: return Optional.of(new PowerdownEvent(kind, null)); @@ -64,18 +63,6 @@ public class MonitorEvent extends Event { case SHUTDOWN: return Optional .of(new ShutdownEvent(kind, response.get(EVENT_DATA))); - case SPICE_CONNECTED: - return Optional.of(new SpiceConnectedEvent(kind, - response.get(EVENT_DATA))); - case SPICE_INITIALIZED: - return Optional.of(new SpiceInitializedEvent(kind, - response.get(EVENT_DATA))); - case SPICE_DISCONNECTED: - return Optional.of(new SpiceDisconnectedEvent(kind, - response.get(EVENT_DATA))); - case VSERPORT_CHANGE: - return Optional.of(new VserportChangeEvent(kind, - response.get(EVENT_DATA))); default: return Optional .of(new MonitorEvent(kind, response.get(EVENT_DATA))); @@ -113,20 +100,4 @@ public class MonitorEvent extends Event { public JsonNode data() { return data; } - - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(Components.objectName(this)).append(" [").append(data); - if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); - } - builder.append(']'); - return builder.toString(); - } } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java index 6d7278c..c0f55fe 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java @@ -25,7 +25,6 @@ import org.jdrupes.vmoperator.runner.qemu.commands.QmpCapabilities; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; import org.jdrupes.vmoperator.runner.qemu.commands.QmpDelCpu; import org.jdrupes.vmoperator.runner.qemu.commands.QmpQueryHotpluggableCpus; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword; import org.jgrapes.core.Channel; import org.jgrapes.core.Components; import org.jgrapes.core.Event; @@ -58,9 +57,6 @@ public class MonitorResult extends Event { if (command instanceof QmpDelCpu) { return new CpuDeleted(command, response); } - if (command instanceof QmpSetDisplayPassword) { - return new DisplayPasswordChanged(command, response); - } return new MonitorResult(command, response); } @@ -152,7 +148,8 @@ public class MonitorResult extends Event { builder.append(Components.objectName(this)) .append(" [").append(executed).append(", ").append(successful()); if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); + builder.append(", channels="); + builder.append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/OsinfoEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/OsinfoEvent.java deleted file mode 100644 index 0e90019..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/OsinfoEvent.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Event; - -/** - * Signals information about the guest OS. - */ -public class OsinfoEvent extends Event { - - private final JsonNode osinfo; - - /** - * Instantiates a new osinfo event. - * - * @param data the data - */ - public OsinfoEvent(JsonNode data) { - osinfo = data; - } - - public JsonNode osinfo() { - return osinfo; - } - - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(Components.objectName(this)).append(" [") - .append(osinfo); - if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); - } - builder.append(']'); - return builder.toString(); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ConfigureQemu.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerConfigurationUpdate.java similarity index 90% rename from org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ConfigureQemu.java rename to org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerConfigurationUpdate.java index 7afa738..bdb0c73 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ConfigureQemu.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerConfigurationUpdate.java @@ -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.RunState; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; import org.jgrapes.core.Channel; import org.jgrapes.core.Event; @@ -31,17 +31,17 @@ import org.jgrapes.core.Event; * on the event and only {@link Event#resumeHandling() resume handling} * when the adaption has completed. */ -public class ConfigureQemu extends Event { +public class RunnerConfigurationUpdate extends Event { private final Configuration configuration; - private final RunState state; + private final State state; /** * Instantiates a new configuration event. * * @param channels the channels */ - public ConfigureQemu(Configuration configuration, RunState state, + public RunnerConfigurationUpdate(Configuration configuration, State state, Channel... channels) { super(channels); this.state = state; @@ -62,7 +62,7 @@ public class ConfigureQemu extends Event { * * @return the state */ - public RunState runState() { + public State state() { return state; } } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java index 261eebf..5d5bffd 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java @@ -18,7 +18,6 @@ package org.jdrupes.vmoperator.runner.qemu.events; -import java.util.EnumSet; import org.jgrapes.core.Channel; import org.jgrapes.core.Components; import org.jgrapes.core.Event; @@ -26,34 +25,17 @@ import org.jgrapes.core.Event; /** * The Class RunnerStateChange. */ +@SuppressWarnings("PMD.DataClass") public class RunnerStateChange extends Event { /** - * The states. + * The state. */ - public enum RunState { - INITIALIZING, STARTING, BOOTING, BOOTED, TERMINATING, STOPPED; - - /** - * Checks if the state is one of the states in which the VM is running. - * - * @return true, if is running - */ - public boolean vmRunning() { - return EnumSet.of(BOOTING, BOOTED, TERMINATING).contains(this); - } - - /** - * Checks if the state is one of the states in which the VM is active. - * - * @return true, if is active - */ - public boolean vmActive() { - return EnumSet.of(BOOTING, BOOTED).contains(this); - } + public enum State { + INITIALIZING, STARTING, RUNNING, TERMINATING, STOPPED } - private final RunState state; + private final State state; private final String reason; private final String message; private final boolean failed; @@ -66,7 +48,7 @@ public class RunnerStateChange extends Event { * @param message the message * @param channels the channels */ - public RunnerStateChange(RunState state, String reason, String message, + public RunnerStateChange(State state, String reason, String message, Channel... channels) { this(state, reason, message, false, channels); } @@ -80,7 +62,7 @@ public class RunnerStateChange extends Event { * @param failed the failed * @param channels the channels */ - public RunnerStateChange(RunState state, String reason, String message, + public RunnerStateChange(State state, String reason, String message, boolean failed, Channel... channels) { super(channels); this.state = state; @@ -94,7 +76,7 @@ public class RunnerStateChange extends Event { * * @return the state */ - public RunState runState() { + public State state() { return state; } @@ -127,14 +109,15 @@ public class RunnerStateChange extends Event { @Override public String toString() { - StringBuilder builder = new StringBuilder(50); + StringBuilder builder = new StringBuilder(); builder.append(Components.objectName(this)) .append(" [").append(state).append(": ").append(reason); if (failed) { builder.append(" (failed)"); } if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); + builder.append(", channels="); + builder.append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceConnectedEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceConnectedEvent.java deleted file mode 100644 index c133307..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceConnectedEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Signals a connection from a client. - */ -public class SpiceConnectedEvent extends SpiceEvent { - - /** - * Instantiates a new spice connected event. - * - * @param kind the kind - * @param data the data - */ - public SpiceConnectedEvent(Kind kind, JsonNode data) { - super(kind, data); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceDisconnectedEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceDisconnectedEvent.java deleted file mode 100644 index cfcb489..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceDisconnectedEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Signals a connection from a client. - */ -public class SpiceDisconnectedEvent extends SpiceEvent { - - /** - * Instantiates a new spice disconnected event. - * - * @param kind the kind - * @param data the data - */ - public SpiceDisconnectedEvent(Kind kind, JsonNode data) { - super(kind, data); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceEvent.java deleted file mode 100644 index 4ce27e2..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceEvent.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Signals a connection from a client. - */ -public class SpiceEvent extends MonitorEvent { - - /** - * Instantiates a new tray moved. - * - * @param kind the kind - * @param data the data - */ - public SpiceEvent(Kind kind, JsonNode data) { - super(kind, data); - } - - /** - * Returns the client's host. - * - * @return the client's host address - */ - public String clientHost() { - return data().get("client").get("host").asText(); - } - - /** - * Returns the client's port. - * - * @return the client's port number - */ - public long clientPort() { - return data().get("client").get("port").asLong(); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceInitializedEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceInitializedEvent.java deleted file mode 100644 index 7bb84b7..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceInitializedEvent.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Signals a connection from a client. - */ -public class SpiceInitializedEvent extends SpiceEvent { - - /** - * Instantiates a new spice connected event. - * - * @param kind the kind - * @param data the data - */ - public SpiceInitializedEvent(Kind kind, JsonNode data) { - super(kind, data); - } - - /** - * Returns the channel type. - * - * @return the channel type - */ - public int channelType() { - return data().get("client").get("channel-type").asInt(); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/TrayMovedEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/TrayMovedEvent.java index e2d2286..f5ef725 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/TrayMovedEvent.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/TrayMovedEvent.java @@ -50,7 +50,7 @@ public class TrayMovedEvent extends MonitorEvent { * * @return the tray state */ - public TrayState trayState() { + public TrayState state() { return data().get("tray-open").asBoolean() ? TrayState.OPEN : TrayState.CLOSED; diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentConnected.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentConnected.java deleted file mode 100644 index dc13569..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentConnected.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import org.jgrapes.core.Event; - -/** - * Signals information about the guest OS. - */ -public class VmopAgentConnected extends Event { -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogIn.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogIn.java deleted file mode 100644 index 96db884..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogIn.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import org.jgrapes.core.Event; - -/** - * Sends the login command to the VM operator agent. - */ -public class VmopAgentLogIn extends Event { - - private final String user; - - /** - * Instantiates a new vmop agent logout. - */ - public VmopAgentLogIn(String user) { - this.user = user; - } - - /** - * Returns the user. - * - * @return the user - */ - public String user() { - return user; - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogOut.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogOut.java deleted file mode 100644 index 1502200..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogOut.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import org.jgrapes.core.Event; - -/** - * Sends the logout command to the VM operator agent. - */ -public class VmopAgentLogOut extends Event { -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedIn.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedIn.java deleted file mode 100644 index f59ed71..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedIn.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import org.jgrapes.core.Event; - -/** - * Signals that the logout command has been processes by the - * VM operator agent. - */ -public class VmopAgentLoggedIn extends Event { - - private final VmopAgentLogIn triggering; - - /** - * Instantiates a new vmop agent logged in. - * - * @param triggeringEvent the triggering event - */ - public VmopAgentLoggedIn(VmopAgentLogIn triggeringEvent) { - this.triggering = triggeringEvent; - } - - /** - * Gets the triggering event. - * - * @return the triggering - */ - public VmopAgentLogIn triggering() { - return triggering; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedOut.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedOut.java deleted file mode 100644 index 5f60e00..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedOut.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import org.jgrapes.core.Event; - -/** - * Signals that the logout command has been processes by the - * VM operator agent. - */ -public class VmopAgentLoggedOut extends Event { - - private final VmopAgentLogOut triggering; - - /** - * Instantiates a new vmop agent logged out. - * - * @param triggeringEvent the triggering event - */ - public VmopAgentLoggedOut(VmopAgentLogOut triggeringEvent) { - this.triggering = triggeringEvent; - } - - /** - * Gets the triggering event. - * - * @return the triggering - */ - public VmopAgentLogOut triggering() { - return triggering; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VserportChangeEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VserportChangeEvent.java deleted file mode 100644 index b590cd3..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VserportChangeEvent.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Signals a virtual serial port's open state change. - */ -public class VserportChangeEvent extends MonitorEvent { - - /** - * Initializes a new instance. - * - * @param kind the kind - * @param data the data - */ - public VserportChangeEvent(Kind kind, JsonNode data) { - super(kind, data); - } - - /** - * Return the channel's id. - * - * @return the string - */ - @SuppressWarnings("PMD.ShortMethodName") - public String id() { - return data().get("id").asText(); - } - - /** - * Returns the open state of the port. - * - * @return true, if is open - */ - public boolean isOpen() { - return Boolean.parseBoolean(data().get("open").asText()); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml index c5c0252..aa7f49e 100644 --- a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml +++ b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml @@ -122,16 +122,11 @@ # Best explanation found: # https://fedoraproject.org/wiki/Features/VirtioSerial - [ "-device", "virtio-serial-pci,id=virtio-serial0" ] - # - Guest agent serial connection. + # - Guest agent serial connection - [ "-device", "virtserialport,id=channel0,name=org.qemu.guest_agent.0,\ chardev=guest-agent-socket" ] - [ "-chardev","socket,id=guest-agent-socket,\ path=${ runtimeDir }/org.qemu.guest_agent.0,server=on,wait=off" ] - # - VM operator agent serial connection. - - [ "-device", "virtserialport,id=channel1,name=org.jdrupes.vmop_agent.0,\ - chardev=vmop-agent-socket" ] - - [ "-chardev","socket,id=vmop-agent-socket,\ - path=${ runtimeDir }/org.jdrupes.vmop_agent.0,server=on,wait=off" ] # * USB Hub and devices (more in SPICE configuration below) # https://qemu-project.gitlab.io/qemu/system/devices/usb.html # https://github.com/qemu/qemu/blob/master/hw/usb/hcd-xhci.c @@ -142,8 +137,7 @@ - [ "-device", "virtio-rng-pci,rng=objrng0,id=rng0" ] # * Graphics and Audio Card # This is the only video "card" without a flickering cursor. - - [ "-device", "virtio-vga,id=video0,max_outputs=${ vm.display.outputs },\ - max_hostmem=${ (vm.display.outputs * 256 * 1024 * 1024)?c }" ] + - [ "-device", "virtio-vga,id=video0,max_outputs=1" ] - [ "-device", "ich9-intel-hda,id=sound0" ] # Network <#assign nwCounter = 0/> @@ -221,8 +215,12 @@ <#assign spice = vm.display.spice/> # SPICE (display, channels ...) # https://www.linux-kvm.org/page/SPICE + <#if ticketPath??> + - [ "-object", "secret,id=spiceTicket,file=${ ticketPath }" ] + - [ "-spice", "port=${ spice.port?c }\ - ,disable-ticketing=<#if hasDisplayPassword!false>off<#else>on\ + <#if spice.ticket??>,password-secret=spiceTicket\ + <#else>,disable-ticketing=on\ <#if spice.streamingVideo??>,streaming-video=${ spice.streamingVideo }\ ,seamless-migration=on" ] - [ "-chardev", "spicevmc,id=vdagentdev,name=vdagent" ] diff --git a/org.jdrupes.vmoperator.util/.eclipse-pmd b/org.jdrupes.vmoperator.util/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/org.jdrupes.vmoperator.util/.eclipse-pmd +++ b/org.jdrupes.vmoperator.util/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/DataPath.java b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/DataPath.java deleted file mode 100644 index e83cf27..0000000 --- a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/DataPath.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.util; - -import java.lang.reflect.Array; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Logger; - -/** - * Utility class that supports navigation through arbitrary data structures. - */ -public final class DataPath { - - private static final Logger logger - = Logger.getLogger(DataPath.class.getName()); - - private DataPath() { - } - - /** - * Apply the given selectors on the given object and return the - * value reached. - * - * Selectors can be if type {@link String} or {@link Number}. The - * former are used to access a property of an object, the latter to - * access an element in an array or a {@link List}. - * - * Depending on the object currently visited, a {@link String} can - * be the key of a {@link Map}, the property part of a getter method - * or the name of a method that has an empty parameter list. - * - * @param the generic type - * @param from the from - * @param selectors the selectors - * @return the result - */ - public static Optional get(Object from, Object... selectors) { - Object cur = from; - for (var selector : selectors) { - if (cur == null) { - return Optional.empty(); - } - if (selector instanceof String && cur instanceof Map map) { - cur = map.get(selector); - continue; - } - if (selector instanceof Number index && cur instanceof List list) { - cur = list.get(index.intValue()); - continue; - } - if (selector instanceof String property) { - var retrieved = tryAccess(cur, property); - if (retrieved.isEmpty()) { - return Optional.empty(); - } - cur = retrieved.get(); - } - } - @SuppressWarnings("unchecked") - var result = Optional.ofNullable((T) cur); - return result; - } - - @SuppressWarnings("PMD.UseLocaleWithCaseConversions") - private static Optional tryAccess(Object obj, String property) { - Method acc = null; - try { - // Try getter - acc = obj.getClass().getMethod("get" + property.substring(0, 1) - .toUpperCase() + property.substring(1)); - } catch (SecurityException e) { - return Optional.empty(); - } catch (NoSuchMethodException e) { // NOPMD - // Can happen... - } - if (acc == null) { - try { - // Try method - acc = obj.getClass().getMethod(property); - } catch (SecurityException | NoSuchMethodException e) { - return Optional.empty(); - } - } - if (acc != null) { - try { - return Optional.ofNullable(acc.invoke(obj)); - } catch (IllegalAccessException - | InvocationTargetException e) { - return Optional.empty(); - } - } - return Optional.empty(); - } - - /** - * Attempts to make a as-deep-as-possible copy of the given - * container. New containers will be created for Maps, Lists and - * Arrays. The method is invoked recursively for the entries/items. - * - * If invoked with an object that is neither a map, list or array, - * the methods checks if the object implements {@link Cloneable} - * and if it does, invokes its {@link Object#clone()} method. - * Else the method return the object. - * - * @param the generic type - * @param object the container - * @return the t - */ - @SuppressWarnings({ "PMD.CognitiveComplexity", "unchecked" }) - public static T deepCopy(T object) { - if (object instanceof Map map) { - Map copy; - try { - copy = (Map) object.getClass().getConstructor() - .newInstance(); - } catch (InstantiationException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - logger.severe( - () -> "Cannot create new instance of " + object.getClass()); - return null; - } - for (var entry : ((Map) map).entrySet()) { - copy.put(entry.getKey(), - deepCopy(entry.getValue())); - } - return (T) copy; - } - if (object instanceof List list) { - List copy = new ArrayList<>(); - for (var item : list) { - copy.add(deepCopy(item)); - } - return (T) copy; - } - if (object.getClass().isArray()) { - var copy = Array.newInstance(object.getClass().getComponentType(), - Array.getLength(object)); - for (int i = 0; i < Array.getLength(object); i++) { - Array.set(copy, i, deepCopy(Array.get(object, i))); - } - return (T) copy; - } - if (object instanceof Cloneable) { - try { - return (T) object.getClass().getMethod("clone") - .invoke(object); - } catch (IllegalAccessException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - return object; - } - } - return object; - } -} diff --git a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java index c6fb101..e3d9fcd 100644 --- a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java +++ b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java @@ -23,7 +23,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import java.math.BigInteger; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -32,7 +31,8 @@ import java.util.function.Supplier; /** * Utility class for pointing to elements on a Gson (Json) tree. */ -@SuppressWarnings({ "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal" }) +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", + "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal", "PMD.GodClass" }) public class GsonPtr { private final JsonElement position; @@ -62,8 +62,7 @@ public class GsonPtr { * @param selectors the selectors * @return the Gson pointer */ - @SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace", - "PMD.AvoidDuplicateLiterals" }) + @SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace" }) public GsonPtr to(Object... selectors) { JsonElement element = position; for (Object sel : selectors) { @@ -92,42 +91,6 @@ public class GsonPtr { return new GsonPtr(element); } - /** - * Create a new instance pointing to the {@link JsonElement} - * selected by the given selectors. If a selector of type - * {@link String} denotes a non-existant member of a - * {@link JsonObject} the result is empty. - * - * @param selectors the selectors - * @return the Gson pointer - */ - @SuppressWarnings({ "PMD.PreserveStackTrace" }) - public Optional get(Object... selectors) { - JsonElement element = position; - for (Object sel : selectors) { - if (element instanceof JsonObject obj - && sel instanceof String member) { - element = obj.get(member); - if (element == null) { - return Optional.empty(); - } - continue; - } - if (element instanceof JsonArray arr - && sel instanceof Integer index) { - try { - element = arr.get(index); - } catch (IndexOutOfBoundsException e) { - throw new IllegalStateException("Selected array index" - + " may not be empty."); - } - continue; - } - throw new IllegalStateException("Invalid selection"); - } - return Optional.of(new GsonPtr(element)); - } - /** * Returns {@link JsonElement} that the pointer points to. * @@ -145,7 +108,8 @@ public class GsonPtr { * @param cls the cls * @return the result */ - public T getAs(Class cls) { + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" }) + public T get(Class cls) { if (cls.isAssignableFrom(position.getClass())) { return cls.cast(position); } @@ -164,7 +128,7 @@ public class GsonPtr { */ @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" }) public Optional - getAs(Class cls, Object... selectors) { + get(Class cls, Object... selectors) { JsonElement element = position; for (Object sel : selectors) { if (element instanceof JsonObject obj @@ -199,7 +163,7 @@ public class GsonPtr { * @return the as string */ public Optional getAsString(Object... selectors) { - return getAs(JsonPrimitive.class, selectors) + return get(JsonPrimitive.class, selectors) .map(JsonPrimitive::getAsString); } @@ -210,7 +174,7 @@ public class GsonPtr { * @return the as string */ public Optional getAsInt(Object... selectors) { - return getAs(JsonPrimitive.class, selectors) + return get(JsonPrimitive.class, selectors) .map(JsonPrimitive::getAsInt); } @@ -221,7 +185,7 @@ public class GsonPtr { * @return the as string */ public Optional getAsBigInteger(Object... selectors) { - return getAs(JsonPrimitive.class, selectors) + return get(JsonPrimitive.class, selectors) .map(JsonPrimitive::getAsBigInteger); } @@ -232,7 +196,7 @@ public class GsonPtr { * @return the as string */ public Optional getAsLong(Object... selectors) { - return getAs(JsonPrimitive.class, selectors) + return get(JsonPrimitive.class, selectors) .map(JsonPrimitive::getAsLong); } @@ -243,7 +207,7 @@ public class GsonPtr { * @return the boolean */ public Optional getAsBoolean(Object... selectors) { - return getAs(JsonPrimitive.class, selectors) + return get(JsonPrimitive.class, selectors) .map(JsonPrimitive::getAsBoolean); } @@ -258,7 +222,7 @@ public class GsonPtr { @SuppressWarnings("unchecked") public List getAsListOf(Class cls, Object... selectors) { - return getAs(JsonArray.class, selectors).map(a -> (List) a.asList()) + return get(JsonArray.class, selectors).map(a -> (List) a.asList()) .orElse(Collections.emptyList()); } @@ -301,18 +265,6 @@ public class GsonPtr { return set(selector, new JsonPrimitive(value)); } - /** - * Short for `set(selector, new JsonPrimitive(value))`. - * - * @param selector the selector - * @param value the value - * @return the gson ptr - * @see #set(Object, JsonElement) - */ - public GsonPtr set(Object selector, Long value) { - return set(selector, new JsonPrimitive(value)); - } - /** * Short for `set(selector, new JsonPrimitive(value))`. * @@ -325,18 +277,6 @@ public class GsonPtr { return set(selector, new JsonPrimitive(value)); } - /** - * Short for `set(selector, new JsonPrimitive(value))`. - * - * @param selector the selector - * @param value the value - * @return the gson ptr - * @see #set(Object, JsonElement) - */ - public GsonPtr set(Object selector, Boolean value) { - return set(selector, new JsonPrimitive(value)); - } - /** * Same as {@link #set(Object, JsonElement)}, but sets the value * only if it doesn't exist yet, else returns the existing value. @@ -384,22 +324,4 @@ public class GsonPtr { return this; } - /** - * Removes all properties except the specified ones. - * - * @param properties the properties - */ - public void removeExcept(String... properties) { - if (!position.isJsonObject()) { - return; - } - for (var itr = ((JsonObject) position).entrySet().iterator(); - itr.hasNext();) { - var entry = itr.next(); - if (Arrays.asList(properties).contains(entry.getKey())) { - continue; - } - itr.remove(); - } - } } diff --git a/org.jdrupes.vmoperator.util/test/org/jdrupes/vmoperator/util/DataPathTests.java b/org.jdrupes.vmoperator.util/test/org/jdrupes/vmoperator/util/DataPathTests.java deleted file mode 100644 index 9c7855f..0000000 --- a/org.jdrupes.vmoperator.util/test/org/jdrupes/vmoperator/util/DataPathTests.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.jdrupes.vmoperator.util; - -import static org.junit.jupiter.api.Assertions.*; -import org.junit.jupiter.api.Test; - -class DataPathTests { - - @Test - void testArray() { - int[] orig - = { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3) }; - var copy = DataPath.deepCopy(orig); - for (int i = 0; i < orig.length; i++) { - assertEquals(orig[i], copy[i]); - } - } -} diff --git a/org.jdrupes.vmoperator.vmaccess/.eclipse-pmd b/org.jdrupes.vmoperator.vmaccess/.eclipse-pmd deleted file mode 100644 index 60d7780..0000000 --- a/org.jdrupes.vmoperator.vmaccess/.eclipse-pmd +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/build.gradle b/org.jdrupes.vmoperator.vmaccess/build.gradle deleted file mode 100644 index 606c6cd..0000000 --- a/org.jdrupes.vmoperator.vmaccess/build.gradle +++ /dev/null @@ -1,57 +0,0 @@ -plugins { - id 'org.jdrupes.vmoperator.java-library-conventions' -} - -dependencies { - implementation project(':org.jdrupes.vmoperator.manager.events') - - implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.1.0,3)' - implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)' - implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)' - implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)' - -} - -apply plugin: 'com.github.node-gradle.node' - -node { - download = true -} - -task extractDependencies(type: Copy) { - from configurations.compileClasspath - .findAll{ it.name.contains('.provider.') - || it.name.contains('org.jgrapes.webconsole.base') - } - .collect{ zipTree (it) } - exclude '*.class' - into 'build/unpacked' - duplicatesStrategy 'include' - } - -task compileTs(type: NodeTask) { - dependsOn ':npmInstall' - dependsOn extractDependencies - inputs.dir project.file('src') - inputs.file project.file('tsconfig.json') - inputs.file project.file('rollup.config.mjs') - outputs.dir project.file('build/generated/resources') - script = file("${rootProject.rootDir}/node_modules/rollup/dist/bin/rollup") - args = ["-c"] -} - -sourceSets { - main { - resources { - srcDir project.file('build/generated/resources') - } - } -} - -processResources { - dependsOn compileTs -} - -eclipse { - autoBuildTasks compileTs -} diff --git a/org.jdrupes.vmoperator.vmaccess/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory b/org.jdrupes.vmoperator.vmaccess/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory deleted file mode 100644 index ec5cf30..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory +++ /dev/null @@ -1 +0,0 @@ -org.jdrupes.vmoperator.vmaccess.VmAccessFactory diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-confirmReset.ftl.html b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-confirmReset.ftl.html deleted file mode 100644 index d7b9405..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-confirmReset.ftl.html +++ /dev/null @@ -1,13 +0,0 @@ -
-

${_("confirmResetMsg")}

-

- - - - - - -

-
\ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html deleted file mode 100644 index a34f725..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html +++ /dev/null @@ -1,39 +0,0 @@ -
-
-
-
- {{ localize("Select VM or pool") }} -
    -
  • - -
  • -
  • - -
  • -
-
-
-
-
diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-l10nBundles.ftl.js b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-l10nBundles.ftl.js deleted file mode 100644 index 96928ef..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-l10nBundles.ftl.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -"use strict"; - -const l10nBundles = new Map(); -let entries = null; -// <#list supportedLanguages() as l> -entries = new Map(); -l10nBundles.set("${l.locale.toLanguageTag()}", entries); -// <#list l.l10nBundle.keys as key> -entries.set("${key}", "${l.l10nBundle.getString(key)}"); -// -// - -export default l10nBundles; diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-preview.ftl.html b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-preview.ftl.html deleted file mode 100644 index 57693ea..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-preview.ftl.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-in-use.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-in-use.svg deleted file mode 100644 index 00e4cc0..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-in-use.svg +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-off.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-off.svg deleted file mode 100644 index 27c11ae..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-off.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer.svg deleted file mode 100644 index f7a6b94..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n.properties b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n.properties deleted file mode 100644 index 6ec24aa..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n.properties +++ /dev/null @@ -1,9 +0,0 @@ -conletName = VM Access - -okayLabel = Apply and Close - -confirmResetTitle = Confirm reset -confirmResetMsg = Resetting the VM may cause loss of data. \ - Please confirm to continue. -consoleInaccessibleNotification = Console is not ready or in use. -poolEmptyNotification = No VM available. Please consult your administrator. diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties deleted file mode 100644 index 28c01f0..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties +++ /dev/null @@ -1,17 +0,0 @@ -conletName = VM-Zugriff - -okayLabel = Anwenden und Schließen -Select\ VM\ or\ pool = VM oder Pool auswählen - -Start\ VM = VM starten -Stop\ VM = VM anhalten -Reset\ VM = VM zurücksetzen -Open\ console = Konsole anzeigen - -confirmResetTitle = Zurücksetzen bestätigen -confirmResetMsg = Zurücksetzen der VM kann zu Datenverlust führen. \ - Bitte bestätigen um fortzufahren. -consoleInaccessibleNotification = Die Konsole ist nicht bereit oder belegt. -poolEmptyNotification = Keine VM verfügbar. Wenden Sie sich bitte an den \ - Systemadministrator. - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/Licenses.txt b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/Licenses.txt deleted file mode 100644 index ac24b16..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/Licenses.txt +++ /dev/null @@ -1,20 +0,0 @@ -almalinux.svg: - Source: https://commons.wikimedia.org/wiki/File:AlmaLinux_Icon_Logo.svg - License: https://github.com/AlmaLinux/wiki/blob/master/LICENSE - -archlinux.svg: - Source: https://commons.wikimedia.org/wiki/File:Arch_Linux_%22Crystal%22_icon.svghttps://commons.wikimedia.org/wiki/File:Arch_Linux_%22Crystal%22_icon.svg - License: GPL v2 or later - -debian.svg: - Source: https://commons.wikimedia.org/wiki/File:Openlogo-debianV2.svg - License : LGPL - -fedora.svg: - Source: https://commons.wikimedia.org/wiki/File:Fedora_icon_(2021).svg - License: Public Domain - -tux.svg: - Source: https://commons.wikimedia.org/wiki/File:Tux.svghttps://commons.wikimedia.org/wiki/File:Tux.svg - License: Creative Commons CC0 1.0 Universal Public Domain Dedication. Creative Commons CC0 1.0 Universal Public Domain Dedication. - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/almalinux.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/almalinux.svg deleted file mode 100644 index b2e050a..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/almalinux.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/arch.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/arch.svg deleted file mode 100644 index ca8204c..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/arch.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/debian.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/debian.svg deleted file mode 100644 index 685f632..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/debian.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/fedora.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/fedora.svg deleted file mode 100644 index e227311..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/fedora.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/tux.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/tux.svg deleted file mode 100644 index 6b558e7..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/tux.svg +++ /dev/null @@ -1,438 +0,0 @@ - - - Tux - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/ubuntu.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/ubuntu.svg deleted file mode 100644 index f217bc8..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/ubuntu.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/unknown.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/unknown.svg deleted file mode 100644 index 51f3016..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/unknown.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - OS - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/windows.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/windows.svg deleted file mode 100644 index 2c7392e..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/windows.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/reset-icon.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/reset-icon.svg deleted file mode 100644 index d47e33d..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/reset-icon.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/rollup.config.mjs b/org.jdrupes.vmoperator.vmaccess/rollup.config.mjs deleted file mode 100644 index ab1aae9..0000000 --- a/org.jdrupes.vmoperator.vmaccess/rollup.config.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import typescript from 'rollup-plugin-typescript2'; -import postcss from 'rollup-plugin-postcss'; - -let packagePath = "org/jdrupes/vmoperator/vmaccess"; -let baseName = "VmAccess" -let module = "build/generated/resources/" + packagePath - + "/" + baseName + "-functions.js"; - -let pathsMap = { - "aash-plugin": "../../page-resource/aash-vue-components/lib/aash-vue-components.js", - "jgconsole": "../../console-base-resource/jgconsole.js", - "jgwc": "../../page-resource/jgwc-vue-components/jgwc-components.js", - "l10nBundles": "./" + baseName + "-l10nBundles.ftl.js", - "vue": "../../page-resource/vue/vue.esm-browser.js" -} - -export default { - external: ['aash-plugin', 'jgconsole', 'jgwc', 'l10nBundles', 'vue', 'chartjs'], - input: "src/" + packagePath + "/browser/" + baseName + "-functions.ts", - output: [ - { - format: "esm", - file: module, - sourcemap: true, - sourcemapPathTransform: (relativeSourcePath, _sourcemapPath) => { - return relativeSourcePath.replace(/^([^/]*\/){12}/, "./"); - }, - paths: pathsMap - } - ], - plugins: [ - typescript(), - postcss() - ] -}; diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java deleted file mode 100644 index f30b771..0000000 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java +++ /dev/null @@ -1,991 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.vmaccess; - -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.google.gson.JsonSyntaxException; -import freemarker.core.ParseException; -import freemarker.template.MalformedTemplateNameException; -import freemarker.template.Template; -import freemarker.template.TemplateNotFoundException; -import io.kubernetes.client.util.Strings; -import java.io.IOException; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.time.Duration; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.ResourceBundle; -import java.util.Set; -import java.util.logging.Level; -import java.util.stream.Collectors; -import org.bouncycastle.util.Objects; -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinition.Assignment; -import org.jdrupes.vmoperator.common.VmDefinition.Permission; -import org.jdrupes.vmoperator.common.VmPool; -import org.jdrupes.vmoperator.manager.events.AssignVm; -import org.jdrupes.vmoperator.manager.events.GetDisplaySecret; -import org.jdrupes.vmoperator.manager.events.GetPools; -import org.jdrupes.vmoperator.manager.events.GetVms; -import org.jdrupes.vmoperator.manager.events.GetVms.VmData; -import org.jdrupes.vmoperator.manager.events.ModifyVm; -import org.jdrupes.vmoperator.manager.events.ResetVm; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmPoolChanged; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Event; -import org.jgrapes.core.EventPipeline; -import org.jgrapes.core.Manager; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Start; -import org.jgrapes.http.Session; -import org.jgrapes.util.events.ConfigurationUpdate; -import org.jgrapes.util.events.KeyValueStoreQuery; -import org.jgrapes.util.events.KeyValueStoreUpdate; -import org.jgrapes.webconsole.base.Conlet.RenderMode; -import org.jgrapes.webconsole.base.ConletBaseModel; -import org.jgrapes.webconsole.base.ConsoleConnection; -import org.jgrapes.webconsole.base.ConsoleRole; -import org.jgrapes.webconsole.base.ConsoleUser; -import org.jgrapes.webconsole.base.WebConsoleUtils; -import org.jgrapes.webconsole.base.events.AddConletRequest; -import org.jgrapes.webconsole.base.events.AddConletType; -import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource; -import org.jgrapes.webconsole.base.events.ConletDeleted; -import org.jgrapes.webconsole.base.events.ConsoleConfigured; -import org.jgrapes.webconsole.base.events.ConsolePrepared; -import org.jgrapes.webconsole.base.events.ConsoleReady; -import org.jgrapes.webconsole.base.events.DeleteConlet; -import org.jgrapes.webconsole.base.events.DisplayNotification; -import org.jgrapes.webconsole.base.events.NotifyConletModel; -import org.jgrapes.webconsole.base.events.NotifyConletView; -import org.jgrapes.webconsole.base.events.OpenModalDialog; -import org.jgrapes.webconsole.base.events.RenderConlet; -import org.jgrapes.webconsole.base.events.RenderConletRequestBase; -import org.jgrapes.webconsole.base.events.SetLocale; -import org.jgrapes.webconsole.base.events.UpdateConletType; -import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; - -/** - * The Class {@link VmAccess}. The component supports the following - * configuration properties: - * - * * `displayResource`: a map with the following entries: - * - `preferredIpVersion`: `ipv4` or `ipv6` (default: `ipv4`). - * Determines the IP addresses uses in the generated - * connection file. - * * `deleteConnectionFile`: `true` or `false` (default: `true`). - * If `true`, the downloaded connection file will be deleted by - * the remote viewer when opened. - * * `syncPreviewsFor`: a list objects with either property `user` or - * `role` and the associated name (default: `[]`). - * The remote viewer will synchronize the previews for the specified - * users and roles. - * - */ -@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.CouplingBetweenObjects", - "PMD.GodClass", "PMD.TooManyMethods", "PMD.CyclomaticComplexity" }) -public class VmAccess extends FreeMarkerConlet { - - private static final String VM_NAME_PROPERTY = "vmName"; - private static final String POOL_NAME_PROPERTY = "poolName"; - private static final String RENDERED - = VmAccess.class.getName() + ".rendered"; - private static final String PENDING - = VmAccess.class.getName() + ".pending"; - private static final Set MODES = RenderMode.asSet( - RenderMode.Preview, RenderMode.Edit); - private static final Set MODES_FOR_GENERATED = RenderMode.asSet( - RenderMode.Preview, RenderMode.StickyPreview); - private EventPipeline appPipeline; - private static ObjectMapper objectMapper - = new ObjectMapper().registerModule(new JavaTimeModule()); - - private Class preferredIpVersion = Inet4Address.class; - private Set syncUsers = Collections.emptySet(); - private Set syncRoles = Collections.emptySet(); - private boolean deleteConnectionFile = true; - - /** - * The periodically generated update event. - */ - public static class Update extends Event { - } - - /** - * Creates a new component with its channel set to the given channel. - * - * @param componentChannel the channel that the component's handlers listen - * on by default and that {@link Manager#fire(Event, Channel...)} - * sends the event to - */ - public VmAccess(Channel componentChannel) { - super(componentChannel); - } - - /** - * On start. - * - * @param event the event - */ - @Handler - public void onStart(Start event) { - appPipeline = event.processedBy().get(); - } - - /** - * Configure the component. - * - * @param event the event - */ - @SuppressWarnings({ "unchecked" }) - @Handler - public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured(componentPath()) - .or(() -> { - var oldConfig = event.structured("/Manager/GuiHttpServer" - + "/ConsoleWeblet/WebConsole/ComponentCollector/VmViewer"); - if (oldConfig.isPresent()) { - logger.warning(() -> "Using configuration with old " - + "component name \"VmViewer\", please update to " - + "\"VmAccess\""); - } - return oldConfig; - }) - .ifPresent(c -> { - try { - var dispRes = (Map) c - .getOrDefault("displayResource", - Collections.emptyMap()); - switch ((String) dispRes.getOrDefault("preferredIpVersion", - "")) { - case "ipv6": - preferredIpVersion = Inet6Address.class; - break; - case "ipv4": - default: - preferredIpVersion = Inet4Address.class; - break; - } - - // Delete connection file - deleteConnectionFile - = Optional.ofNullable(c.get("deleteConnectionFile")) - .map(Object::toString).map(Boolean::parseBoolean) - .orElse(true); - - // Users or roles for which previews should be synchronized - syncUsers = ((List>) c.getOrDefault( - "syncPreviewsFor", Collections.emptyList())).stream() - .map(m -> m.get("user")) - .filter(s -> s != null).collect(Collectors.toSet()); - logger.finest(() -> "Syncing previews for users: " - + syncUsers.toString()); - syncRoles = ((List>) c.getOrDefault( - "syncPreviewsFor", Collections.emptyList())).stream() - .map(m -> m.get("role")) - .filter(s -> s != null).collect(Collectors.toSet()); - logger.finest(() -> "Syncing previews for roles: " - + syncRoles.toString()); - } catch (ClassCastException e) { - logger.config("Malformed configuration: " + e.getMessage()); - } - }); - } - - private boolean syncPreviews(Session session) { - return WebConsoleUtils.userFromSession(session) - .filter(u -> syncUsers.contains(u.getName())).isPresent() - || WebConsoleUtils.rolesFromSession(session).stream() - .filter(cr -> syncRoles.contains(cr.getName())).findAny() - .isPresent(); - } - - /** - * On {@link ConsoleReady}, fire the {@link AddConletType}. - * - * @param event the event - * @param channel the channel - * @throws TemplateNotFoundException the template not found exception - * @throws MalformedTemplateNameException the malformed template name - * exception - * @throws ParseException the parse exception - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onConsoleReady(ConsoleReady event, ConsoleConnection channel) - throws TemplateNotFoundException, MalformedTemplateNameException, - ParseException, IOException { - // Add conlet resources to page - channel.respond(new AddConletType(type()) - .setDisplayNames( - localizations(channel.supportedLocales(), "conletName")) - .addRenderMode(RenderMode.Preview) - .addScript(new ScriptResource().setScriptType("module") - .setScriptUri(event.renderSupport().conletResource( - type(), "VmAccess-functions.js")))); - channel.session().put(RENDERED, new HashSet<>()); - } - - /** - * On console configured. - * - * @param event the event - * @param connection the console connection - * @throws InterruptedException the interrupted exception - */ - @Handler - public void onConsoleConfigured(ConsoleConfigured event, - ConsoleConnection connection) throws InterruptedException, - IOException { - @SuppressWarnings({ "unchecked" }) - final var rendered - = (Set) connection.session().get(RENDERED); - connection.session().remove(RENDERED); - if (!syncPreviews(connection.session())) { - return; - } - addMissingConlets(event, connection, rendered); - } - - @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" }) - private void addMissingConlets(ConsoleConfigured event, - ConsoleConnection connection, final Set rendered) - throws InterruptedException { - var session = connection.session(); - - // Evaluate missing VMs - var missingVms = appPipeline.fire(new GetVms().accessibleFor( - WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null), - WebConsoleUtils.rolesFromSession(session).stream() - .map(ConsoleRole::getName).toList())) - .get().stream().map(d -> d.definition().name()) - .collect(Collectors.toCollection(HashSet::new)); - missingVms.removeAll(rendered.stream() - .filter(r -> r.mode() == ResourceModel.Mode.VM) - .map(ResourceModel::name).toList()); - - // Evaluate missing pools - var missingPools = appPipeline.fire(new GetPools().accessibleFor( - WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null), - WebConsoleUtils.rolesFromSession(session).stream() - .map(ConsoleRole::getName).toList())) - .get().stream().map(VmPool::name) - .collect(Collectors.toCollection(HashSet::new)); - missingPools.removeAll(rendered.stream() - .filter(r -> r.mode() == ResourceModel.Mode.POOL) - .map(ResourceModel::name).toList()); - - // Nothing to do - if (missingVms.isEmpty() && missingPools.isEmpty()) { - return; - } - - // Suspending to allow rendering of conlets to be noticed - var failSafe = Components.schedule(t -> event.resumeHandling(), - Duration.ofSeconds(1)); - event.suspendHandling(failSafe::cancel); - connection.setAssociated(PENDING, event); - - // Create conlets for VMs and pools that haven't been rendered - for (var vmName : missingVms) { - fire(new AddConletRequest(event.event().event().renderSupport(), - VmAccess.class.getName(), RenderMode.asSet(RenderMode.Preview)) - .addProperty(VM_NAME_PROPERTY, vmName), - connection); - } - for (var poolName : missingPools) { - fire(new AddConletRequest(event.event().event().renderSupport(), - VmAccess.class.getName(), RenderMode.asSet(RenderMode.Preview)) - .addProperty(POOL_NAME_PROPERTY, poolName), - connection); - } - } - - /** - * On console prepared. - * - * @param event the event - * @param connection the connection - */ - @Handler - public void onConsolePrepared(ConsolePrepared event, - ConsoleConnection connection) { - if (syncPreviews(connection.session())) { - connection.respond(new UpdateConletType(type())); - } - } - - private String storagePath(Session session, String conletId) { - return "/" + WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse("") - + "/" + VmAccess.class.getName() + "/" + conletId; - } - - @Override - protected Optional createNewState(AddConletRequest event, - ConsoleConnection connection, String conletId) throws Exception { - var model = new ResourceModel(conletId); - var poolName = (String) event.properties().get(POOL_NAME_PROPERTY); - if (poolName != null) { - model.setMode(ResourceModel.Mode.POOL); - model.setName(poolName); - } else { - model.setMode(ResourceModel.Mode.VM); - model.setName((String) event.properties().get(VM_NAME_PROPERTY)); - } - String jsonState = objectMapper.writeValueAsString(model); - connection.respond(new KeyValueStoreUpdate().update( - storagePath(connection.session(), model.getConletId()), jsonState)); - return Optional.of(model); - } - - @Override - protected Optional createStateRepresentation(Event event, - ConsoleConnection connection, String conletId) throws Exception { - var model = new ResourceModel(conletId); - String jsonState = objectMapper.writeValueAsString(model); - connection.respond(new KeyValueStoreUpdate().update( - storagePath(connection.session(), model.getConletId()), jsonState)); - return Optional.of(model); - } - - @Override - @SuppressWarnings("PMD.EmptyCatchBlock") - protected Optional recreateState(Event event, - ConsoleConnection channel, String conletId) throws Exception { - KeyValueStoreQuery query = new KeyValueStoreQuery( - storagePath(channel.session(), conletId), channel); - newEventPipeline().fire(query, channel); - try { - if (!query.results().isEmpty()) { - var json = query.results().get(0).values().stream().findFirst() - .get(); - ResourceModel model - = objectMapper.readValue(json, ResourceModel.class); - return Optional.of(model); - } - } catch (InterruptedException e) { - // Means we have no result. - } - - // Fall back to creating default state. - return createStateRepresentation(event, channel, conletId); - } - - @Override - protected Set doRenderConlet(RenderConletRequestBase event, - ConsoleConnection channel, String conletId, ResourceModel model) - throws Exception { - if (event.renderAs().contains(RenderMode.Preview)) { - return renderPreview(event, channel, conletId, model); - } - - // Render edit - ResourceBundle resourceBundle = resourceBundle(channel.locale()); - Set renderedAs = EnumSet.noneOf(RenderMode.class); - if (event.renderAs().contains(RenderMode.Edit)) { - var session = channel.session(); - var vmNames = appPipeline.fire(new GetVms().accessibleFor( - WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null), - WebConsoleUtils.rolesFromSession(session).stream() - .map(ConsoleRole::getName).toList())) - .get().stream().map(d -> d.definition().name()).sorted() - .toList(); - var poolNames = appPipeline.fire(new GetPools().accessibleFor( - WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null), - WebConsoleUtils.rolesFromSession(session).stream() - .map(ConsoleRole::getName).toList())) - .get().stream().map(VmPool::name).sorted().toList(); - Template tpl - = freemarkerConfig().getTemplate("VmAccess-edit.ftl.html"); - var fmModel = fmModel(event, channel, conletId, model); - fmModel.put("vmNames", vmNames); - fmModel.put("poolNames", poolNames); - channel.respond(new OpenModalDialog(type(), conletId, - processTemplate(event, tpl, fmModel)) - .addOption("cancelable", true) - .addOption("okayLabel", - resourceBundle.getString("okayLabel"))); - } - return renderedAs; - } - - @SuppressWarnings("unchecked") - private Set renderPreview(RenderConletRequestBase event, - ConsoleConnection channel, String conletId, ResourceModel model) - throws TemplateNotFoundException, MalformedTemplateNameException, - ParseException, IOException, InterruptedException { - channel.associated(PENDING, Event.class) - .ifPresent(e -> { - e.resumeHandling(); - channel.setAssociated(PENDING, null); - }); - - VmDefinition vmDef = null; - if (model.mode() == ResourceModel.Mode.VM && model.name() != null) { - // Remove conlet if VM definition has been removed - // or user has not at least one permission - vmDef = getVmData(model, channel).map(VmData::definition) - .orElse(null); - if (vmDef == null) { - channel.respond( - new DeleteConlet(conletId, Collections.emptySet())); - return Collections.emptySet(); - } - } - - if (model.mode() == ResourceModel.Mode.POOL && model.name() != null) { - // Remove conlet if pool definition has been removed - // or user has not at least one permission - VmPool pool = appPipeline - .fire(new GetPools().withName(model.name())).get() - .stream().findFirst().orElse(null); - if (pool == null - || permissions(pool, channel.session()).isEmpty()) { - channel.respond( - new DeleteConlet(conletId, Collections.emptySet())); - return Collections.emptySet(); - } - vmDef = getVmData(model, channel).map(VmData::definition) - .orElse(null); - } - - // Render - Template tpl - = freemarkerConfig().getTemplate("VmAccess-preview.ftl.html"); - channel.respond(new RenderConlet(type(), conletId, - processTemplate(event, tpl, - fmModel(event, channel, conletId, model))) - .setRenderAs( - RenderMode.Preview.addModifiers(event.renderAs())) - .setSupportedModes(syncPreviews(channel.session()) - ? MODES_FOR_GENERATED - : MODES)); - if (!Strings.isNullOrEmpty(model.name())) { - Optional.ofNullable(channel.session().get(RENDERED)) - .ifPresent(s -> ((Set) s).add(model)); - updatePreview(channel, model, vmDef); - } - return EnumSet.of(RenderMode.Preview); - } - - private Optional getVmData(ResourceModel model, - ConsoleConnection channel) throws InterruptedException { - if (model.mode() == ResourceModel.Mode.VM) { - // Get the VM data by name. - var session = channel.session(); - return appPipeline.fire(new GetVms().withName(model.name()) - .accessibleFor(WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null), - WebConsoleUtils.rolesFromSession(session).stream() - .map(ConsoleRole::getName).toList())) - .get().stream().findFirst(); - } - - // Look for an (already) assigned VM - var user = WebConsoleUtils.userFromSession(channel.session()) - .map(ConsoleUser::getName).orElse(null); - return appPipeline.fire(new GetVms().assignedFrom(model.name()) - .assignedTo(user)).get().stream().findFirst(); - } - - /** - * Returns the permissions from the VM definition. - * - * @param vmDef the VM definition - * @param session the session - * @return the sets the - */ - private Set permissions(VmDefinition vmDef, Session session) { - var user = WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null); - var roles = WebConsoleUtils.rolesFromSession(session) - .stream().map(ConsoleRole::getName).toList(); - return vmDef.permissionsFor(user, roles); - } - - /** - * Returns the permissions from the pool. - * - * @param pool the pool - * @param session the session - * @return the sets the - */ - private Set permissions(VmPool pool, Session session) { - var user = WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null); - var roles = WebConsoleUtils.rolesFromSession(session) - .stream().map(ConsoleRole::getName).toList(); - return pool.permissionsFor(user, roles); - } - - /** - * Returns the permissions from the VM definition or the pool depending - * on the state of the model. - * - * @param session the session - * @param model the model - * @param vmDef the vm def - * @return the sets the - * @throws InterruptedException the interrupted exception - */ - private Set permissions(Session session, ResourceModel model, - VmDefinition vmDef) throws InterruptedException { - var user = WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null); - var roles = WebConsoleUtils.rolesFromSession(session) - .stream().map(ConsoleRole::getName).toList(); - if (model.mode() == ResourceModel.Mode.POOL) { - // Use permissions from pool - var pool = appPipeline.fire(new GetPools().withName(model.name())) - .get().stream().findFirst().orElse(null); - if (pool == null) { - return Collections.emptySet(); - } - return pool.permissionsFor(user, roles); - } - - // Use permissions from VM - if (vmDef == null) { - vmDef = appPipeline.fire(new GetVms().assignedFrom(model.name()) - .assignedTo(user)).get().stream().map(VmData::definition) - .findFirst().orElse(null); - } - if (vmDef == null) { - return Collections.emptySet(); - } - return vmDef.permissionsFor(user, roles); - } - - private void updatePreview(ConsoleConnection channel, ResourceModel model, - VmDefinition vmDef) throws InterruptedException { - updateConfig(channel, model, vmDef); - updateVmDef(channel, model, vmDef); - } - - private void updateConfig(ConsoleConnection channel, ResourceModel model, - VmDefinition vmDef) throws InterruptedException { - channel.respond(new NotifyConletView(type(), - model.getConletId(), "updateConfig", model.mode(), model.name(), - permissions(channel.session(), model, vmDef).stream() - .map(VmDefinition.Permission::toString).toList())); - } - - private void updateVmDef(ConsoleConnection channel, ResourceModel model, - VmDefinition vmDef) throws InterruptedException { - Map data = null; - if (vmDef == null) { - model.setAssignedVm(null); - } else { - model.setAssignedVm(vmDef.name()); - var session = channel.session(); - var user = WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null); - var perms = permissions(session, model, vmDef); - try { - data = Map.of( - "metadata", Map.of("namespace", vmDef.namespace(), - "name", vmDef.name()), - "spec", vmDef.spec(), - "status", vmDef.status(), - "consoleAccessible", vmDef.consoleAccessible(user, perms)); - } catch (JsonSyntaxException e) { - logger.log(Level.SEVERE, e, - () -> "Failed to serialize VM definition"); - return; - } - } - channel.respond(new NotifyConletView(type(), - model.getConletId(), "updateVmDefinition", data)); - } - - @Override - protected void doConletDeleted(ConletDeleted event, - ConsoleConnection channel, String conletId, - ResourceModel conletState) - throws Exception { - if (event.renderModes().isEmpty()) { - channel.respond(new KeyValueStoreUpdate().delete( - storagePath(channel.session(), conletId))); - } - } - - /** - * Track the VM definitions and update conlets. - * - * @param event the event - * @param channel the channel - * @throws IOException - * @throws InterruptedException - */ - @Handler(namedChannels = "manager") - @SuppressWarnings({ "PMD.CognitiveComplexity", - "PMD.AvoidInstantiatingObjectsInLoops" }) - public void onVmResourceChanged(VmResourceChanged event, VmChannel channel) - throws IOException, InterruptedException { - var vmDef = event.vmDefinition(); - - // Update known conlets - for (var entry : conletIdsByConsoleConnection().entrySet()) { - var connection = entry.getKey(); - var user = WebConsoleUtils.userFromSession(connection.session()) - .map(ConsoleUser::getName).orElse(null); - for (var conletId : entry.getValue()) { - var model = stateFromSession(connection.session(), conletId); - if (model.isEmpty() - || Strings.isNullOrEmpty(model.get().name())) { - continue; - } - if (model.get().mode() == ResourceModel.Mode.VM) { - // Check if this VM is used by conlet - if (!Objects.areEqual(model.get().name(), vmDef.name())) { - continue; - } - if (event.type() == K8sObserver.ResponseType.DELETED - || permissions(vmDef, connection.session()).isEmpty()) { - connection.respond( - new DeleteConlet(conletId, Collections.emptySet())); - continue; - } - } else { - // Check if VM is used by pool conlet or to be assigned to - // it - var toBeUsedByConlet = vmDef.assignment() - .map(Assignment::pool) - .map(p -> p.equals(model.get().name())).orElse(false) - && vmDef.assignment().map(Assignment::user) - .map(u -> u.equals(user)).orElse(false); - if (!Objects.areEqual(model.get().assignedVm(), - vmDef.name()) && !toBeUsedByConlet) { - continue; - } - - // Now unassigned if VM is deleted or no longer to be used - if (event.type() == K8sObserver.ResponseType.DELETED - || !toBeUsedByConlet) { - updateVmDef(connection, model.get(), null); - continue; - } - } - - // Full update because permissions may have changed - updatePreview(connection, model.get(), vmDef); - } - } - } - - /** - * On vm pool changed. - * - * @param event the event - * @throws InterruptedException the interrupted exception - */ - @Handler(namedChannels = "manager") - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - public void onVmPoolChanged(VmPoolChanged event) - throws InterruptedException { - var poolName = event.vmPool().name(); - // Update known conlets - for (var entry : conletIdsByConsoleConnection().entrySet()) { - var connection = entry.getKey(); - for (var conletId : entry.getValue()) { - var model = stateFromSession(connection.session(), conletId); - if (model.isEmpty() - || model.get().mode() != ResourceModel.Mode.POOL - || !Objects.areEqual(model.get().name(), poolName)) { - continue; - } - if (event.deleted() - || permissions(event.vmPool(), connection.session()) - .isEmpty()) { - connection.respond( - new DeleteConlet(conletId, Collections.emptySet())); - continue; - } - updateConfig(connection, model.get(), null); - } - } - } - - @SuppressWarnings({ "PMD.NcssCount", "PMD.CognitiveComplexity", - "PMD.AvoidLiteralsInIfCondition" }) - @Override - protected void doUpdateConletState(NotifyConletModel event, - ConsoleConnection channel, ResourceModel model) throws Exception { - event.stop(); - if ("selectedResource".equals(event.method())) { - selectResource(event, channel, model); - return; - } - - Optional vmData = getVmData(model, channel); - if (vmData.isEmpty()) { - if (model.mode() == ResourceModel.Mode.VM) { - return; - } - if ("start".equals(event.method())) { - // Assign a VM. - var user = WebConsoleUtils.userFromSession(channel.session()) - .map(ConsoleUser::getName).orElse(null); - vmData = Optional.ofNullable(appPipeline - .fire(new AssignVm(model.name(), user)).get()); - if (vmData.isEmpty()) { - ResourceBundle resourceBundle - = resourceBundle(channel.locale()); - channel.respond(new DisplayNotification( - resourceBundle.getString("poolEmptyNotification"), - Map.of("autoClose", 10_000, "type", "Error"))); - return; - } - } - } - - // Handle command for selected VM - var vmChannel = vmData.get().channel(); - var vmDef = vmData.get().definition(); - var vmName = vmDef.metadata().getName(); - var perms = permissions(channel.session(), model, vmDef); - var resourceBundle = resourceBundle(channel.locale()); - switch (event.method()) { - case "start": - if (perms.contains(VmDefinition.Permission.START)) { - vmChannel.fire(new ModifyVm(vmName, "state", "Running")); - } - break; - case "stop": - if (perms.contains(VmDefinition.Permission.STOP)) { - vmChannel.fire(new ModifyVm(vmName, "state", "Stopped")); - } - break; - case "reset": - if (perms.contains(VmDefinition.Permission.RESET)) { - confirmReset(event, channel, model, resourceBundle); - } - break; - case "resetConfirmed": - if (perms.contains(VmDefinition.Permission.RESET)) { - vmChannel.fire(new ResetVm(vmName)); - } - break; - case "openConsole": - openConsole(channel, model, vmChannel, vmDef, perms); - break; - default:// ignore - break; - } - } - - private void confirmReset(NotifyConletModel event, - ConsoleConnection channel, ResourceModel model, - ResourceBundle resourceBundle) throws TemplateNotFoundException, - MalformedTemplateNameException, ParseException, IOException { - Template tpl = freemarkerConfig() - .getTemplate("VmAccess-confirmReset.ftl.html"); - channel.respond(new OpenModalDialog(type(), model.getConletId(), - processTemplate(event, tpl, - fmModel(event, channel, model.getConletId(), model))) - .addOption("cancelable", true).addOption("closeLabel", "") - .addOption("title", - resourceBundle.getString("confirmResetTitle"))); - } - - private void openConsole(ConsoleConnection channel, ResourceModel model, - VmChannel vmChannel, VmDefinition vmDef, Set perms) { - var resourceBundle = resourceBundle(channel.locale()); - var user = WebConsoleUtils.userFromSession(channel.session()) - .map(ConsoleUser::getName).orElse(""); - if (!vmDef.consoleAccessible(user, perms)) { - channel.respond(new DisplayNotification( - resourceBundle.getString("consoleInaccessibleNotification"), - Map.of("autoClose", 5_000, "type", "Warning"))); - return; - } - var pwQuery = Event.onCompletion(new GetDisplaySecret(vmDef, user), - e -> gotPassword(channel, model, vmDef, e)); - vmChannel.fire(pwQuery); - } - - private void gotPassword(ConsoleConnection channel, ResourceModel model, - VmDefinition vmDef, GetDisplaySecret event) { - if (!event.secretAvailable()) { - return; - } - vmDef.extra().connectionFile(event.secret(), - preferredIpVersion, deleteConnectionFile) - .ifPresent(cf -> channel.respond(new NotifyConletView(type(), - model.getConletId(), "openConsole", cf))); - } - - @SuppressWarnings({ "PMD.UseLocaleWithCaseConversions" }) - private void selectResource(NotifyConletModel event, - ConsoleConnection channel, ResourceModel model) - throws JsonProcessingException, InterruptedException { - try { - model.setMode(ResourceModel.Mode - .valueOf(event. param(0).toUpperCase())); - model.setName(event.param(1)); - String jsonState = objectMapper.writeValueAsString(model); - channel.respond(new KeyValueStoreUpdate().update(storagePath( - channel.session(), model.getConletId()), jsonState)); - updatePreview(channel, model, - getVmData(model, channel).map(VmData::definition).orElse(null)); - } catch (IllegalArgumentException e) { - logger.warning(() -> "Invalid resource type: " + e.getMessage()); - } - } - - @Override - protected boolean doSetLocale(SetLocale event, ConsoleConnection channel, - String conletId) throws Exception { - return true; - } - - /** - * The Class AccessModel. - */ - public static class ResourceModel extends ConletBaseModel { - - /** - * The Enum ResourceType. - */ - @SuppressWarnings("PMD.ShortVariable") - public enum Mode { - VM, POOL - } - - private Mode mode; - private String name; - private String assignedVm; - - /** - * Instantiates a new resource model. - * - * @param conletId the conlet id - */ - public ResourceModel(@JsonProperty("conletId") String conletId) { - super(conletId); - } - - /** - * Returns the mode. - * - * @return the resourceType - */ - @JsonGetter("mode") - public Mode mode() { - return mode; - } - - /** - * Sets the mode. - * - * @param mode the resource mode to set - */ - public void setMode(Mode mode) { - this.mode = mode; - } - - /** - * Gets the resource name. - * - * @return the string - */ - @JsonGetter("name") - public String name() { - return name; - } - - /** - * Sets the name. - * - * @param name the resource name to set - */ - public void setName(String name) { - this.name = name; - } - - /** - * Gets the assigned vm. - * - * @return the string - */ - @JsonGetter("assignedVm") - public String assignedVm() { - return assignedVm; - } - - /** - * Sets the assigned vm. - * - * @param name the assigned vm - */ - public void setAssignedVm(String name) { - this.assignedVm = name; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + java.util.Objects.hash(mode, name); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - ResourceModel other = (ResourceModel) obj; - return mode == other.mode - && java.util.Objects.equals(name, other.name); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(50); - builder.append("AccessModel [mode=").append(mode) - .append(", name=").append(name).append(']'); - return builder.toString(); - } - } -} diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts deleted file mode 100644 index 47e6e11..0000000 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts +++ /dev/null @@ -1,306 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024,2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { - reactive, ref, createApp, computed, watch -} from "vue"; -import JGConsole from "jgconsole"; -import JgwcPlugin, { JGWC } from "jgwc"; -import { provideApi, getApi } from "aash-plugin"; -import l10nBundles from "l10nBundles"; - -import "./VmAccess-style.scss"; - -// For global access -declare global { - interface Window { - orgJDrupesVmOperatorVmAccess: { - initPreview?: (previewDom: HTMLElement, isUpdate: boolean) => void, - initEdit?: (viewDom: HTMLElement, isUpdate: boolean) => void, - applyEdit?: (viewDom: HTMLElement, apply: boolean) => void, - confirmReset?: (conletType: string, conletId: string) => void - } - } -} - -window.orgJDrupesVmOperatorVmAccess = {}; - -interface Api { - /* eslint-disable @typescript-eslint/no-explicit-any */ - vmName: string; - vmDefinition: any; - poolName: string | null; - permissions: string[]; -} - -const localize = (key: string) => { - return JGConsole.localize( - l10nBundles, JGWC.lang(), key); -}; - -window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, - _isUpdate: boolean) => { - const app = createApp({ - setup(_props: object) { - const conletId = (previewDom.closest( - "[data-conlet-id]")!).dataset["conletId"]!; - const resourceBase = (previewDom.closest( - "*[data-conlet-resource-base]")!).dataset.conletResourceBase; - - const previewApi: Api = reactive({ - vmName: "", - vmDefinition: {}, - poolName: null, - permissions: [] - }); - const poolName = computed(() => previewApi.poolName); - const vmName = computed(() => previewApi.vmDefinition.name); - const configured = computed(() => previewApi.vmDefinition.spec); - const accessible = computed(() => previewApi.vmDefinition.consoleAccessible); - const busy = computed(() => previewApi.vmDefinition.spec - && (previewApi.vmDefinition.spec.vm.state === 'Running' - && (!previewApi.vmDefinition.consoleAccessible) - || previewApi.vmDefinition.spec.vm.state === 'Stopped' - && previewApi.vmDefinition.running)); - const startable = computed(() => previewApi.vmDefinition.spec - && previewApi.vmDefinition.spec.vm.state !== 'Running' - && !previewApi.vmDefinition.running - && previewApi.permissions.includes('start') - || previewApi.poolName !== null && !previewApi.vmDefinition.name); - const stoppable = computed(() => previewApi.vmDefinition.spec && - previewApi.vmDefinition.spec.vm.state !== 'Stopped' - && previewApi.vmDefinition.running); - const running = computed(() => previewApi.vmDefinition.running); - const inUse = computed(() => previewApi.vmDefinition.usedBy != ''); - const permissions = computed(() => previewApi.permissions); - const osicon = computed(() => { - if (!previewApi.vmDefinition.status?.osinfo?.id) { - return null; - } - switch(previewApi.vmDefinition.status.osinfo.id) { - case "almalinux": return "almalinux.svg"; - case "arch": return "arch.svg"; - case "debian": return "debian.svg"; - case "fedora": return "fedora.svg"; - case "mswindows": return "windows.svg"; - case "ubuntu": return "ubuntu.svg"; - default: { - if ((previewApi.vmDefinition.status.osinfo.name || "") - .toLowerCase().includes("linux")) { - return "tux.svg"; - } - return "unknown.svg"; - } - } - }); - - watch(previewApi, (api: Api) => { - JGConsole.instance.updateConletTitle(conletId, - api.poolName || api.vmDefinition.name || ""); - }); - - provideApi(previewDom, previewApi); - - const vmAction = (action: string) => { - JGConsole.notifyConletModel(conletId, action); - }; - - return { localize, resourceBase, vmAction, poolName, vmName, - configured, accessible, busy, startable, stoppable, running, - inUse, permissions, osicon }; - }, - template: ` - - - - - - - - - - - -
{{ vmName }}
- - - - - - - - -
` - }); - app.use(JgwcPlugin, []); - app.config.globalProperties.window = window; - app.mount(previewDom); -}; - -JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess", - "updateConfig", - function(conletId: string, type: string, resource: string, - permissions: []) { - const conlet = JGConsole.findConletPreview(conletId); - if (!conlet) { - return; - } - const api = getApi(conlet.element().querySelector( - ":scope .jdrupes-vmoperator-vmaccess-preview"))!; - if (type === "VM") { - api.vmName = resource; - api.poolName = ""; - } else { - api.poolName = resource; - api.vmName = ""; - } - api.permissions = permissions; - }); - -JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess", - "updateVmDefinition", function(conletId: string, vmDefinition: any | null) { - const conlet = JGConsole.findConletPreview(conletId); - if (!conlet) { - return; - } - const api = getApi(conlet.element().querySelector( - ":scope .jdrupes-vmoperator-vmaccess-preview"))!; - if (vmDefinition) { - // Add some short-cuts for rendering - vmDefinition.name = vmDefinition.metadata.name; - vmDefinition.currentCpus = vmDefinition.status.cpus; - vmDefinition.currentRam = Number(vmDefinition.status.ram); - vmDefinition.usedBy = vmDefinition.status.consoleClient || ""; - // safety fallbacks - vmDefinition.status.conditions.forEach((condition: any) => { - if (condition.type === "Running") { - vmDefinition.running = condition.status === "True"; - vmDefinition.runningConditionSince - = new Date(condition.lastTransitionTime); - } - }) - } else { - vmDefinition = {}; - } - api.vmDefinition = vmDefinition; - }); - -JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess", - "openConsole", function(_conletId: string, data: string) { - let target = document.getElementById( - "org.jdrupes.vmoperator.vmaccess.VmAccess.target"); - if (!target) { - target = document.createElement("iframe"); - target.id = "org.jdrupes.vmoperator.vmaccess.VmAccess.target"; - target.setAttribute("name", target.id); - target.setAttribute("style", "display: none;"); - document.querySelector("body")!.append(target); - } - const url = "data:application/x-virt-viewer;base64," - + window.btoa(data); - window.open(url, target.id); - }); - -window.orgJDrupesVmOperatorVmAccess.initEdit = (dialogDom: HTMLElement, - isUpdate: boolean) => { - if (isUpdate) { - return; - } - const app = createApp({ - setup() { - const formId = (dialogDom - .closest("*[data-conlet-id]")!).id + "-form"; - - const localize = (key: string) => { - return JGConsole.localize( - l10nBundles, JGWC.lang()!, key); - }; - - const resource = ref("vm"); - const vmNameInput = ref(""); - const poolNameInput = ref(""); - - watch(resource, (resource: string) => { - if (resource === "vm") { - poolNameInput.value = ""; - } - if (resource === "pool") - vmNameInput.value = ""; - }); - - const conletId = (dialogDom.closest( - "[data-conlet-id]")!).dataset["conletId"]!; - const conlet = JGConsole.findConletPreview(conletId); - if (conlet) { - const api = getApi(conlet.element().querySelector( - ":scope .jdrupes-vmoperator-vmaccess-preview"))!; - if (api.poolName) { - resource.value = "pool"; - } - vmNameInput.value = api.vmName; - poolNameInput.value = api.poolName; - } - - provideApi(dialogDom, { resource: () => resource.value, - name: () => resource.value === "vm" - ? vmNameInput.value : poolNameInput.value }); - - return { formId, localize, resource, vmNameInput, poolNameInput }; - } - }); - app.use(JgwcPlugin); - app.mount(dialogDom); -} - -window.orgJDrupesVmOperatorVmAccess.applyEdit = - (dialogDom: HTMLElement, apply: boolean) => { - if (!apply) { - return; - } - const conletId = (dialogDom.closest("[data-conlet-id]")!) - .dataset["conletId"]!; - const editApi = getApi>(dialogDom!)!; - JGConsole.notifyConletModel(conletId, "selectedResource", editApi.resource(), - editApi.name()); -} - -window.orgJDrupesVmOperatorVmAccess.confirmReset = - (conletType: string, conletId: string) => { - JGConsole.instance.closeModalDialog(conletType, conletId); - JGConsole.notifyConletModel(conletId, "resetConfirmed"); -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss deleted file mode 100644 index 3a291dd..0000000 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss +++ /dev/null @@ -1,124 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/* - * Conlet specific styles. - */ -.jdrupes-vmoperator-vmaccess { - - span[role="button"].svg-icon { - display: inline-block; - line-height: 1; - - /* Align with forkawesome */ - font-size: 14px; - fill: var(--primary); - - &[aria-disabled="true"], &[aria-disabled=""] { - fill: var(--disabled); - } - - svg { - height: 2ex; - width: 1em; - } - } - - [role=button] { - padding: 0.25rem; - - &:not([aria-disabled]):hover, &[aria-disabled='false']:hover { - box-shadow: var(--darkening); - } - } -} - -.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-preview { - - table { - border-spacing: 0; - } - - img { - display: block; - height: 3em; - padding: 0.25rem; - - &[aria-disabled=''], &[aria-disabled='true'] { - opacity: 0.4; - } - } - - .jdrupes-vmoperator-vmaccess-preview-action-list { - white-space: nowrap; - } - - span.busy::before { - font: normal normal normal 14px/1 ForkAwesome; - font-size: 1.125em; - content: "\f1ce"; - left: 1.45em; - top: 0.7em; - color: var(--info); - position: absolute; - animation: spin 2s linear infinite; - z-index: 100; - pointer-events: none; - } - - span.osicon { - width: 4.25em; - height: 3em; - padding: 0.25rem; - pointer-events: none; - - img { - display: block; - height: 1.75em; - margin: 0.2em auto 0; - pointer-events: none; - } - } -} - -.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-edit { - - fieldset ul li { - margin-top: 0.5em; - } - - select { - width: 15em; - } -} - -.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-confirm-reset { - p { - text-align: center; - } - - span[role="button"].svg-icon { - fill: var(--danger); - - svg { - width: 2.5em; - height: 2.5em; - } - } - -} diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/package-info.java b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/package-info.java deleted file mode 100644 index 745ded7..0000000 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/package-info.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023, 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.vmaccess; diff --git a/org.jdrupes.vmoperator.vmaccess/tsconfig.json b/org.jdrupes.vmoperator.vmaccess/tsconfig.json deleted file mode 100644 index d9dbb3f..0000000 --- a/org.jdrupes.vmoperator.vmaccess/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "es2015", - "module": "es2015", - "sourceMap": true, - "inlineSources": true, - "declaration": true, - "importHelpers": true, - "strict": true, - "moduleResolution": "node", - "experimentalDecorators": true, - "lib": ["DOM", "ES2020"], - "paths": { - "aash-plugin": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/aash-vue-components/lib/AashPlugin"], - "jgconsole": ["./build/unpacked/org/jgrapes/webconsole/base/JGConsole"], - "jgwc": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/jgwc-vue-components/jgwc-components"], - "l10nBundles": ["./src/org/jdrupes/vmoperator/vmaccess/browser/l10nBundles-stub"], - "vue": ["./build/unpacked/org/jgrapes/webconsole/provider/vue/vue/vue"] - } - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "l10nBundles-stub.ts"] -} diff --git a/org.jdrupes.vmoperator.vmaccess/.checkstyle b/org.jdrupes.vmoperator.vmconlet/.checkstyle similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.checkstyle rename to org.jdrupes.vmoperator.vmconlet/.checkstyle diff --git a/spice-squid/.eclipse-pmd b/org.jdrupes.vmoperator.vmconlet/.eclipse-pmd similarity index 76% rename from spice-squid/.eclipse-pmd rename to org.jdrupes.vmoperator.vmconlet/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/spice-squid/.eclipse-pmd +++ b/org.jdrupes.vmoperator.vmconlet/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.vmaccess/.eslintignore b/org.jdrupes.vmoperator.vmconlet/.eslintignore similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.eslintignore rename to org.jdrupes.vmoperator.vmconlet/.eslintignore diff --git a/org.jdrupes.vmoperator.vmaccess/.eslintrc.json b/org.jdrupes.vmoperator.vmconlet/.eslintrc.json similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.eslintrc.json rename to org.jdrupes.vmoperator.vmconlet/.eslintrc.json diff --git a/org.jdrupes.vmoperator.vmaccess/.gitignore b/org.jdrupes.vmoperator.vmconlet/.gitignore similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.gitignore rename to org.jdrupes.vmoperator.vmconlet/.gitignore diff --git a/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.buildship.core.prefs b/org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.buildship.core.prefs similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.buildship.core.prefs rename to org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.buildship.core.prefs diff --git a/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.core.resources.prefs b/org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.core.resources.prefs similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.core.resources.prefs rename to org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.core.resources.prefs diff --git a/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.core.runtime.prefs b/org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.core.runtime.prefs similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.core.runtime.prefs rename to org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.core.runtime.prefs diff --git a/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.jdt.ui.prefs b/org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.jdt.ui.prefs similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.jdt.ui.prefs rename to org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.jdt.ui.prefs diff --git a/org.jdrupes.vmoperator.vmmgmt/build.gradle b/org.jdrupes.vmoperator.vmconlet/build.gradle similarity index 95% rename from org.jdrupes.vmoperator.vmmgmt/build.gradle rename to org.jdrupes.vmoperator.vmconlet/build.gradle index 606c6cd..ab667f5 100644 --- a/org.jdrupes.vmoperator.vmmgmt/build.gradle +++ b/org.jdrupes.vmoperator.vmconlet/build.gradle @@ -5,7 +5,7 @@ plugins { dependencies { implementation project(':org.jdrupes.vmoperator.manager.events') - implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.1.0,3)' + implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.3.0,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)' diff --git a/org.jdrupes.vmoperator.vmaccess/package.json b/org.jdrupes.vmoperator.vmconlet/package.json similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/package.json rename to org.jdrupes.vmoperator.vmconlet/package.json diff --git a/org.jdrupes.vmoperator.vmconlet/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory b/org.jdrupes.vmoperator.vmconlet/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory new file mode 100644 index 0000000..5a22dc7 --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory @@ -0,0 +1 @@ +org.jdrupes.vmoperator.vmconlet.VmConletFactory diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-l10nBundles.ftl.js b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-l10nBundles.ftl.js similarity index 100% rename from org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-l10nBundles.ftl.js rename to org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-l10nBundles.ftl.js diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-preview.ftl.html b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html similarity index 88% rename from org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-preview.ftl.html rename to org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html index 8c9970a..0c6aa37 100644 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-preview.ftl.html +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html @@ -1,6 +1,6 @@ -
diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-view.ftl.html b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-view.ftl.html similarity index 55% rename from org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-view.ftl.html rename to org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-view.ftl.html index 3197440..913f45d 100644 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-view.ftl.html +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-view.ftl.html @@ -1,8 +1,7 @@ -
-