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..161b7c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,4 +37,4 @@ jobs: java-version: '21' 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 -Pwebsite.push.token=${{ secrets.WEBSITE_PUSH_TOKEN }} -Pdocker.registry=ghcr.io/${{ github.actor }} stage pushImages diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..a8673aa --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,76 @@ +stages: + - build + - test + - publish + - deploy + +.any-job: + rules: + - if: $CI_SERVER_HOST == "gitlab.mnl.de" + +.gradle-job: + extends: .any-job + image: registry.mnl.de/org/jgrapes/jdk21-builder:v2 + cache: + - key: dependencies + policy: pull-push + paths: + - .gradle + - node_modules + - key: "$CI_COMMIT_SHA" + policy: pull-push + paths: + - build + - "*/build" + before_script: + - echo -n $CI_REGISTRY_PASSWORD | podman login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY + - git switch $CI_COMMIT_REF_NAME + - git pull + - git reset --hard $CI_COMMIT_SHA + +build-jars: + stage: build + extends: .gradle-job + script: + - ./gradlew -Pdocker.registry=$CI_REGISTRY_IMAGE build apidocs + +publish-images: + stage: publish + extends: .gradle-job + script: + - ./gradlew -Pdocker.registry=$CI_REGISTRY_IMAGE pushImage + +.pages-job: + extends: .any-job + image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/ruby:3.2 + variables: + JEKYLL_ENV: production + LC_ALL: C.UTF-8 + before_script: + - git fetch origin gh-pages + - git checkout gh-pages + - gem install bundler + - bundle install + +test-pages: + stage: test + extends: .pages-job + rules: + - if: $CI_COMMIT_BRANCH == "gh-pages" + script: + - bundle exec jekyll build -d test + artifacts: + paths: + - test + +#publish-pages: +# stage: publish +# extends: .pages-job +# rules: +# - if: $CI_COMMIT_BRANCH == "gh-pages" +# script: +# - bundle exec jekyll build -d public +# artifacts: +# paths: +# - public +# environment: production 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/.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..52a2fa8 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,10 @@ ![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://jdrupes.org/vm-operator/) for details. diff --git a/build.gradle b/build.gradle index eb8e59a..d0ebc71 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_21) { + // 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-doc-conventions.gradle b/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle index 6af8fa7..5eed550 100644 --- a/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle +++ b/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle @@ -118,3 +118,33 @@ if (System.properties['org.ajoberstar.grgit.auth.username'] == null) { System.setProperty('org.ajoberstar.grgit.auth.username', project.rootProject.properties['website.push.token'] ?: "nouser") } + +gitPublish { + repoUri = 'https://github.com/mnlipp/jdrupes.org.git' + branch = 'main' + contents { + from("${rootProject.projectDir}/webpages") { + include '_layouts/vm-operator.html' + include 'vm-operator/**' + } + from("${rootProject.buildDir}/javadoc") { + into 'vm-operator/javadoc' + } + if (!findProject(':org.jdrupes.vmoperator.runner.qemu').isSnapshot + && !findProject(':org.jdrupes.vmoperator.manager').isSnapshot) { + from("${rootProject.buildDir}/javadoc") { + into 'vm-operator/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..a9e8dfe 100644 --- a/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle +++ b/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle @@ -19,7 +19,6 @@ if (shortened == "manager") { 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('/', '-') + "-" 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..bfe3985 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -994,10 +994,6 @@ spec: 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: >- @@ -1021,21 +1017,8 @@ spec: - 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,15 +1410,6 @@ 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: @@ -1470,10 +1444,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 +1454,12 @@ 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 +1470,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..b7467d4 100644 --- a/deploy/vmop-deployment.yaml +++ b/deploy/vmop-deployment.yaml @@ -20,32 +20,23 @@ spec: containers: - 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 + ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:3.1.1 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..0b0e94a 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,9 @@ 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..516fb7e 100644 --- a/dev-example/Readme.md +++ b/dev-example/Readme.md @@ -1,11 +1,11 @@ # Example setup for development The CRD must be deployed independently. Apart from that, the -`kustomize.yaml` +`kustomize.yaml` - * creates a small cdrom image repository and +* creates a small cdrom image repository and - * deploys the operator in namespace `vmop-dev` with a replica of 0. +* deploys the operator in namespace `vmop-dev` with a replica of 0. This allows you to run the manager in your IDE. diff --git a/dev-example/config.yaml b/dev-example/config.yaml index 2a72bc8..579103d 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,33 +17,18 @@ "/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" + - name: admin + fullName: Administrator + password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." + - name: 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: + test: - user # All users have role other "*": @@ -74,16 +39,13 @@ # Admins can use all conlets admin: - "*" - operator: - - org.jdrupes.vmoperator.vmmgmt.VmMgmt - - org.jdrupes.vmoperator.vmaccess.VmAccess user: - - org.jdrupes.vmoperator.vmaccess.VmAccess + - org.jdrupes.vmoperator.vmviewer.VmViewer # Others cannot use any conlet (except login conlet to log out) other: - org.jgrapes.webconlet.oidclogin.LoginConlet "/ComponentCollector": - "/VmAccess": + "/VmViewer": displayResource: preferredIpVersion: ipv4 syncPreviewsFor: 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..19b6295 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,28 +43,18 @@ 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: + test: - user # All users have role other "*": @@ -89,7 +71,7 @@ patches: other: - org.jgrapes.webconlet.locallogin.LoginConlet "/ComponentCollector": - "/VmAccess": + "/VmViewer": displayResource: preferredIpVersion: ipv4 syncPreviewsFor: 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-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..e874ef8 100644 --- a/dev-example/test-vm.yaml +++ b/dev-example/test-vm.yaml @@ -5,13 +5,18 @@ 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 + version: latest pullPolicy: Always permissions: - - user: admin - may: - - "*" + - user: admin + may: + - "*" + - user: test + may: + - "accessConsole" resources: requests: @@ -32,9 +37,8 @@ spec: currentCpus: 4 networks: - # No bridge on test cluster - - user: {} - + - tap: + mac: "02:16:3e:33:58:10" disks: - volumeClaimTemplate: metadata: @@ -58,5 +62,3 @@ spec: 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/misc/javadoc.bottom.txt b/misc/javadoc.bottom.txt index d5589ac..abf54f3 100644 --- a/misc/javadoc.bottom.txt +++ b/misc/javadoc.bottom.txt @@ -16,21 +16,18 @@ var _paq = _paq || []; /* tracker methods like "setCustomDimension" should be called before "trackPageView" */ _paq.push(["setDocumentTitle", document.domain + "/" + document.title]); - _paq.push(["setCookieDomain", "*.mnlipp.github.io"]); - _paq.push(["setDomains", ["*.mnlipp.github.io", "*.jdrupes.org", "kubernetes-vm-operator.readthedocs.io"]]); + _paq.push(["setCookieDomain", "*.jdrupes.org"]); _paq.push(['disableCookies']); _paq.push(['trackPageView']); _paq.push(['enableLinkTracking']); (function() { - var u="https://piwik.mnl.de/"; + var u="//jdrupes.org/"; _paq.push(['setTrackerUrl', u+'piwik.php']); - _paq.push(['setSiteId', '17']); + _paq.push(['setSiteId', '15']); var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); })(); - + - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/build.gradle b/org.jdrupes.vmoperator.common/build.gradle index e72cb14..42c05ae 100644 --- a/org.jdrupes.vmoperator.common/build.gradle +++ b/org.jdrupes.vmoperator.common/build.gradle @@ -10,8 +10,6 @@ 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]' } 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..150bb3b 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 @@ -27,101 +27,15 @@ public class Constants { /** The Constant APP_NAME. */ public static final String APP_NAME = "vm-runner"; + /** The Constant COMP_DISPLAY_SECRETS. */ + public static final String COMP_DISPLAY_SECRET = "display-secret"; + /** 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/K8s.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java index 3870337..481f724 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 @@ -47,7 +47,8 @@ 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 { /** @@ -112,6 +113,7 @@ public class K8s { public static JsonObject yamlToJson(ApiClient client, Reader yaml) { // Avoid Yaml.load due to // https://github.com/kubernetes-client/java/issues/2741 + @SuppressWarnings("PMD.UseConcurrentHashMap") Map yamlData = new Yaml(new SafeConstructor(new LoaderOptions())).load(yaml); @@ -155,6 +157,27 @@ public class K8s { return Optional.of(apiRes); } + /** + * Get an object from its metadata. + * + * @param the generic type + * @param the generic type + * @param api the api + * @param meta the meta + * @return the object + */ + @Deprecated + @SuppressWarnings("PMD.GenericsNaming") + public static + Optional + get(GenericKubernetesApi api, V1ObjectMeta meta) { + var response = api.get(meta.getNamespace(), meta.getName()); + if (response.isSuccess()) { + return Optional.of(response.getObject()); + } + return Optional.empty(); + } + /** * Apply the given patch data. * 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 index 272da2b..37b0b97 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java @@ -48,7 +48,8 @@ import okhttp3.Response; * A client with some additional properties. */ @SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyMethods", - "checkstyle:LineLength", "PMD.CouplingBetweenObjects", "PMD.GodClass" }) + "PMD.LinguisticNaming", "checkstyle:LineLength", + "PMD.CouplingBetweenObjects", "PMD.GodClass" }) public class K8sClient extends ApiClient { private ApiClient apiClient; @@ -230,6 +231,7 @@ public class K8sClient extends ApiClient { * @return the api client * @see ApiClient#setKeyManagers(javax.net.ssl.KeyManager[]) */ + @SuppressWarnings("PMD.UseVarargs") @Override public ApiClient setKeyManagers(KeyManager[] managers) { return apiClient().setKeyManagers(managers); @@ -636,6 +638,7 @@ public class K8sClient extends ApiClient { * @return the string * @see ApiClient#selectHeaderAccept(java.lang.String[]) */ + @SuppressWarnings("PMD.UseVarargs") @Override public String selectHeaderAccept(String[] accepts) { return apiClient().selectHeaderAccept(accepts); @@ -648,6 +651,7 @@ public class K8sClient extends ApiClient { * @return the string * @see ApiClient#selectHeaderContentType(java.lang.String[]) */ + @SuppressWarnings("PMD.UseVarargs") @Override public String selectHeaderContentType(String[] contentTypes) { return apiClient().selectHeaderContentType(contentTypes); @@ -814,7 +818,7 @@ public class K8sClient extends ApiClient { * @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" }) + @SuppressWarnings({ "rawtypes", "PMD.ExcessiveParameterList" }) @Override public Call buildCall(String path, String method, List queryParams, List collectionQueryParams, Object body, @@ -843,7 +847,7 @@ public class K8sClient extends ApiClient { * @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" }) + @SuppressWarnings({ "rawtypes", "PMD.ExcessiveParameterList" }) @Override public Request buildRequest(String path, String method, List queryParams, List collectionQueryParams, 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 index 59b4d12..81a4eab 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java @@ -45,7 +45,7 @@ import java.util.function.Function; * @param the generic type * @param the generic type */ -@SuppressWarnings({ "PMD.CouplingBetweenObjects" }) +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class K8sClusterGenericStub { protected final K8sClient client; @@ -239,7 +239,6 @@ public class K8sClusterGenericStub the object list type * @param the result type */ - @FunctionalInterface public interface GenericSupplier> { @@ -254,6 +253,7 @@ public class K8sClusterGenericStub objectClass, Class objectListClass, K8sClient client, APIResource context, String name); } @@ -282,6 +282,7 @@ public class K8sClusterGenericStub> R get(Class objectClass, Class objectListClass, @@ -312,6 +313,8 @@ public class K8sClusterGenericStub> R get(Class objectClass, Class objectListClass, @@ -336,6 +339,8 @@ public class K8sClusterGenericStub> R create(Class objectClass, Class objectListClass, @@ -368,7 +373,7 @@ public class K8sClusterGenericStub> Collection list(Class objectClass, Class objectListClass, - K8sClient client, APIResource context, + K8sClient client, APIResource context, ListOptions options, GenericSupplier provider) throws ApiException { var result = new ArrayList(); 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 index 2392d3e..6a4410f 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java @@ -29,6 +29,7 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta; * notably the metadata, is made available through the methods * defined by {@link KubernetesObject}. */ +@SuppressWarnings("PMD.DataClass") public class K8sDynamicModel implements KubernetesObject { private final V1ObjectMeta metadata; @@ -101,7 +102,7 @@ public class K8sDynamicModel implements KubernetesObject { * * @return the JSON object describing the status */ - public JsonObject statusJson() { + public JsonObject status() { return data.getAsJsonObject("status"); } 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 index c0303c2..afed802 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java @@ -31,6 +31,7 @@ import java.util.Collection; * state and can therefore be used for any kind of object, especially * custom objects. */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class K8sDynamicStub extends K8sDynamicStubBase { @@ -63,6 +64,8 @@ public class K8sDynamicStub * @return the stub if the object exists * @throws ApiException the api exception */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) public static K8sDynamicStub get(K8sClient client, GroupVersionKind gvk, String namespace, String name) throws ApiException { @@ -80,6 +83,8 @@ public class K8sDynamicStub * @return the stub if the object exists * @throws ApiException the api exception */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) public static K8sDynamicStub get(K8sClient client, APIResource context, String namespace, String name) { return new K8sDynamicStub(client, context, namespace, name); 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 index ae3f012..44f419c 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java @@ -26,6 +26,7 @@ import io.kubernetes.client.Discovery.APIResource; * state and can therefore be used for any kind of object, especially * custom objects. */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public abstract class K8sDynamicStubBase> extends K8sGenericStub { @@ -39,6 +40,7 @@ public abstract class K8sDynamicStubBase objectClass, Class objectListClass, DynamicTypeAdapterFactory taf, K8sClient client, APIResource context, String namespace, 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 index 9ba376f..f118a17 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java @@ -27,7 +27,6 @@ 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; @@ -48,7 +47,7 @@ import java.util.function.Function; * @param the generic type * @param the generic type */ -@SuppressWarnings({ "PMD.TooManyMethods" }) +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class K8sGenericStub { protected final K8sClient client; @@ -193,92 +192,30 @@ public class K8sGenericStub updateStatus(O object, Function updater) - throws ApiException { - return K8s.optional(api.updateStatus(object, updater)); + public Optional updateStatus(O object, + Function status) throws ApiException { + return K8s.optional(api.updateStatus(object, status)); } /** - * 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. + * Updates the status. * - * @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 + * @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 updater, O current) + public Optional updateStatus(Function status) 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); + return updateStatus( + api.get(namespace, name).throwsApiException().getObject(), status); } /** @@ -287,7 +224,7 @@ public class K8sGenericStub patch(String patchType, V1Patch patch, @@ -302,7 +239,7 @@ public class K8sGenericStub @@ -311,21 +248,6 @@ public class K8sGenericStub 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. * @@ -357,7 +279,6 @@ public class K8sGenericStub the object list type * @param the result type */ - @FunctionalInterface public interface GenericSupplier> { @@ -369,6 +290,7 @@ public class K8sGenericStub> R create(Class objectClass, Class objectListClass, 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 index 9e22382..2545a30 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java @@ -27,11 +27,9 @@ 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 @@ -50,6 +48,7 @@ public class K8sObserver objectClass, Class objectListClass, K8sClient client, APIResource context, String namespace, ListOptions options) { @@ -85,47 +85,39 @@ public class K8sObserver(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); + thread = new Thread(() -> { + try { + logger.config(() -> "Watching " + context.getResourcePlural() + + " (" + context.getPreferredVersion() + ")" + + " 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); + // Watch sometimes terminates without apparent reason. + while (!Thread.currentThread().isInterrupted()) { + Instant startedAt = Instant.now(); + try { + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var changed = api.watch(namespace, options).iterator(); + while (changed.hasNext()) { + handler.accept(client, changed.next()); } - } - 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); + } catch (ApiException e) { + logger.log(Level.FINE, e, () -> "Problem watching" + + " (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); + } + } + }); + thread.setDaemon(true); } @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") @@ -230,6 +222,7 @@ public class K8sObserver { 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 index 9075a84..16e5c82 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java @@ -29,6 +29,7 @@ import java.util.Optional; /** * A stub for pods (v1). */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class K8sV1DeploymentStub extends K8sGenericStub { 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 index ea1237d..050c593 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java @@ -29,6 +29,7 @@ import java.util.List; /** * A stub for nodes (v1). */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class K8sV1NodeStub extends K8sClusterGenericStub { public static final APIResource CONTEXT = new APIResource("", List.of("v1"), @@ -73,7 +74,8 @@ public class K8sV1NodeStub extends K8sClusterGenericStub { /** * Provide {@link GenericSupplier}. */ - @SuppressWarnings({ "PMD.UnusedFormalParameter" }) + @SuppressWarnings({ "PMD.UnusedFormalParameter", + "PMD.UnusedPrivateMethod" }) private static K8sV1NodeStub getGeneric(Class objectClass, Class objectListClass, K8sClient client, APIResource context, String name) { 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 index f21bb47..21ac931 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java @@ -29,6 +29,7 @@ import java.util.List; /** * A stub for pods (v1). */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class K8sV1PodStub extends K8sGenericStub { /** The pods' context. */ 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 index 9c1c086..a847d36 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java @@ -29,6 +29,7 @@ import java.util.List; /** * A stub for secrets (v1). */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class K8sV1SecretStub extends K8sGenericStub { public static final APIResource CONTEXT = new APIResource("", List.of("v1"), 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 index 863f86f..2157a1d 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java @@ -29,6 +29,7 @@ import java.util.List; /** * A stub for secrets (v1). */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class K8sV1ServiceStub extends K8sGenericStub { public static final APIResource CONTEXT = new APIResource("", List.of("v1"), 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 index be30b00..b918725 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java @@ -26,6 +26,7 @@ import java.util.List; /** * A stub for stateful sets (v1). */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class K8sV1StatefulSetStub extends K8sGenericStub { 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/VmDefinitionModel.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModel.java new file mode 100644 index 0000000..5e1ebb0 --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModel.java @@ -0,0 +1,124 @@ +/* + * 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 com.google.gson.JsonPrimitive; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.jdrupes.vmoperator.util.GsonPtr; + +/** + * Represents a VM definition. + */ +@SuppressWarnings("PMD.DataClass") +public class VmDefinitionModel extends K8sDynamicModel { + + /** + * Permissions for accessing and manipulating the VM. + */ + public enum Permission { + START("start"), STOP("stop"), RESET("reset"), + ACCESS_CONSOLE("accessConsole"); + + @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)); + } + + @Override + public String toString() { + return repr; + } + } + + /** + * Instantiates a new model from the JSON representation. + * + * @param delegate the gson instance to use for extracting structured data + * @param json the JSON + */ + public VmDefinitionModel(Gson delegate, JsonObject json) { + super(delegate, json); + } + + /** + * 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 GsonPtr.to(data()) + .getAsListOf(JsonObject.class, "spec", "permissions") + .stream().filter(p -> GsonPtr.to(p).getAsString("user") + .map(u -> u.equals(user)).orElse(false) + || GsonPtr.to(p).getAsString("role").map(roles::contains) + .orElse(false)) + .map(p -> GsonPtr.to(p).getAsListOf(JsonPrimitive.class, "may") + .stream()) + .flatMap(Function.identity()).map(p -> p.getAsString()) + .map(Permission::parse).map(Set::stream) + .flatMap(Function.identity()).collect(Collectors.toSet()); + } + + /** + * Get the display password serial. + * + * @return the optional + */ + public Optional displayPasswordSerial() { + return GsonPtr.to(status()) + .get(JsonPrimitive.class, "displayPasswordSerial") + .map(JsonPrimitive::getAsLong); + } +} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModels.java similarity index 79% rename from org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java rename to org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModels.java index c79654e..5ac412f 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModels.java @@ -22,10 +22,10 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; /** - * Represents a list of {@link VmDefinition}s. + * Represents a list of {@link VmDefinitionModel}s. */ -public class VmDefinitions - extends K8sDynamicModelsBase { +public class VmDefinitionModels + extends K8sDynamicModelsBase { /** * Initialize the object list using the given JSON data. @@ -33,7 +33,7 @@ public class VmDefinitions * @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); + public VmDefinitionModels(Gson delegate, JsonObject data) { + super(VmDefinitionModel.class, delegate, data); } } 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 index 377220a..49da3e0 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java @@ -31,11 +31,12 @@ import java.util.Collection; * state and can therefore be used for any kind of object, especially * custom objects. */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class VmDefinitionStub - extends K8sDynamicStubBase { + extends K8sDynamicStubBase { - private static DynamicTypeAdapterFactory taf = new VmDefintionModelTypeAdapterFactory(); + private static DynamicTypeAdapterFactory taf = new VmDefintionModelTypeAdapterFactory(); /** * Instantiates a new stub for VM defintions. @@ -47,7 +48,7 @@ public class VmDefinitionStub */ public VmDefinitionStub(K8sClient client, APIResource context, String namespace, String name) { - super(VmDefinition.class, VmDefinitions.class, taf, client, + super(VmDefinitionModel.class, VmDefinitionModels.class, taf, client, context, namespace, name); } @@ -63,6 +64,8 @@ public class VmDefinitionStub * @return the stub if the object exists * @throws ApiException the api exception */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) public static VmDefinitionStub get(K8sClient client, GroupVersionKind gvk, String namespace, String name) throws ApiException { @@ -80,6 +83,8 @@ public class VmDefinitionStub * @return the stub if the object exists * @throws ApiException the api exception */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) public static VmDefinitionStub get(K8sClient client, APIResource context, String namespace, String name) { return new VmDefinitionStub(client, context, namespace, name); @@ -96,10 +101,10 @@ public class VmDefinitionStub */ public static VmDefinitionStub createFromYaml(K8sClient client, APIResource context, Reader yaml) throws ApiException { - var model = new VmDefinition(client.getJSON().getGson(), + var model = new VmDefinitionModel(client.getJSON().getGson(), K8s.yamlToJson(client, yaml)); - return K8sGenericStub.create(VmDefinition.class, - VmDefinitions.class, client, context, model, + return K8sGenericStub.create(VmDefinitionModel.class, + VmDefinitionModels.class, client, context, model, (c, ns, n) -> new VmDefinitionStub(c, context, ns, n)); } @@ -116,8 +121,8 @@ public class VmDefinitionStub 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, + return K8sGenericStub.list(VmDefinitionModel.class, + VmDefinitionModels.class, client, context, namespace, options, (c, ns, n) -> new VmDefinitionStub(c, context, ns, n)); } @@ -139,13 +144,13 @@ public class VmDefinitionStub * A factory for creating VmDefinitionModel(s) objects. */ public static class VmDefintionModelTypeAdapterFactory extends - DynamicTypeAdapterFactory { + DynamicTypeAdapterFactory { /** * Instantiates a new dynamic model type adapter factory. */ public VmDefintionModelTypeAdapterFactory() { - super(VmDefinition.class, VmDefinitions.class); + super(VmDefinitionModel.class, VmDefinitionModels.class); } } 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/build.gradle b/org.jdrupes.vmoperator.manager.events/build.gradle index bb4b8d8..566e200 100644 --- a/org.jdrupes.vmoperator.manager.events/build.gradle +++ b/org.jdrupes.vmoperator.manager.events/build.gradle @@ -9,5 +9,7 @@ plugins { } dependencies { + api 'org.jgrapes:org.jgrapes.core:[1.19.0,2)' api project(':org.jdrupes.vmoperator.common') + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]' } 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/ChannelTracker.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelCache.java similarity index 52% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelTracker.java rename to org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelCache.java index 8a41908..1e6d031 100644 --- 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/ChannelCache.java @@ -19,7 +19,6 @@ 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; @@ -28,30 +27,20 @@ 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. + * A channel manager that tracks mappings from a key to a channel using + * "add/remove" (or "open/close") events and the channels on which they + * are delivered. * * @param the key type * @param the channel type * @param the type of the associated data */ -public class ChannelTracker - implements ChannelDictionary { +public class ChannelCache { - private final Map> entries = new ConcurrentHashMap<>(); + private final Map> channels = new ConcurrentHashMap<>(); /** - * Combines the channel and associated data. - * - * @param the generic type - * @param the generic type + * Helper */ @SuppressWarnings("PMD.ShortClassName") private static class Data { @@ -68,24 +57,32 @@ public class ChannelTracker } } - @Override - public Set keys() { - return entries.keySet(); - } + /** + * Combines the channel and the associated data. + * + * @param the generic type + * @param the generic type + */ + @SuppressWarnings("PMD.ShortClassName") + public static class Both { - @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)); + /** The channel. */ + public C channel; + + /** The associated. */ + public A associated; + + /** + * Instantiates a new both. + * + * @param channel the channel + * @param associated the associated + */ + public Both(C channel, A associated) { + super(); + this.channel = channel; + this.associated = associated; } - return result; } /** @@ -95,18 +92,20 @@ public class ChannelTracker * @param key the key * @return the result */ - public Optional> value(K key) { - var value = entries.get(key); - if (value == null) { - return Optional.empty(); + public Optional> both(K key) { + synchronized (channels) { + var value = channels.get(key); + if (value == null) { + return Optional.empty(); + } + var channel = value.channel.get(); + if (channel == null) { + // Cleanup old reference + channels.remove(key); + return Optional.empty(); + } + return Optional.of(new Both<>(channel, value.associated)); } - 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)); } /** @@ -117,10 +116,10 @@ public class ChannelTracker * @param associated the associated * @return the channel manager */ - public ChannelTracker put(K key, C channel, A associated) { + public ChannelCache put(K key, C channel, A associated) { Data data = new Data<>(channel); data.associated = associated; - entries.put(key, data); + channels.put(key, data); return this; } @@ -131,11 +130,22 @@ public class ChannelTracker * @param channel the channel * @return the channel manager */ - public ChannelTracker put(K key, C channel) { + public ChannelCache put(K key, C channel) { put(key, channel, null); return this; } + /** + * Returns the channel registered for the key or an empty optional + * if no mapping exists. + * + * @param key the key + * @return the optional + */ + public Optional channel(K key) { + return both(key).map(b -> b.channel); + } + /** * Associate the entry for the channel with the given data. The entry * for the channel must already exist. @@ -144,18 +154,54 @@ public class ChannelTracker * @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); + public ChannelCache associate(K key, A data) { + synchronized (channels) { + Optional.ofNullable(channels.get(key)) + .ifPresent(v -> v.associated = data); + } return this; } + /** + * Return the data associated with the entry for the channel. + * + * @param key the key + * @return the data + */ + public Optional associated(K key) { + return both(key).map(b -> b.associated); + } + + /** + * Returns all associated data. + * + * @return the collection + */ + public Collection associated() { + synchronized (channels) { + return channels.values().stream() + .filter(v -> v.channel.get() != null && v.associated != null) + .map(v -> v.associated).toList(); + } + } + /** * Removes the channel with the given name. * * @param name the name */ public void remove(String name) { - entries.remove(name); + synchronized (channels) { + channels.remove(name); + } + } + + /** + * Returns all known keys. + * + * @return the sets the + */ + public Set keys() { + return channels.keySet(); } } 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 index da36123..eb27ea0 100644 --- 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 @@ -27,24 +27,53 @@ import java.util.function.Function; import org.jgrapes.core.Channel; /** - * Provides an actively managed implementation of the {@link ChannelDictionary}. + * A channel manager that maintains mappings from a key to a channel. + * As a convenience, it is possible to additionally associate arbitrary + * data with the entry (and thus with the channel). * - * 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. + * The manager should be used by a component that defines channels for + * housekeeping. It can be shared between this component and another + * component, preferably using the {@link #fixed()} view for the + * second component. Alternatively, the second component can use a + * {@link ChannelCache} 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 { +public class ChannelManager { - private final Map> entries = new ConcurrentHashMap<>(); + private final Map> channels = new ConcurrentHashMap<>(); private final Function supplier; + private ChannelManager readOnly; + + /** + * Combines the channel and the associated data. + * + * @param the generic type + * @param the generic type + */ + @SuppressWarnings("PMD.ShortClassName") + public static class Both { + + /** The channel. */ + public C channel; + + /** The associated. */ + public A associated; + + /** + * Instantiates a new both. + * + * @param channel the channel + * @param associated the associated + */ + public Both(C channel, A associated) { + super(); + this.channel = channel; + this.associated = associated; + } + } /** * Instantiates a new channel manager. @@ -62,26 +91,6 @@ public class 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. @@ -89,8 +98,10 @@ public class ChannelManager * @param key the key * @return the result */ - public Optional> value(K key) { - return Optional.ofNullable(entries.get(key)); + public Optional> both(K key) { + synchronized (channels) { + return Optional.ofNullable(channels.get(key)); + } } /** @@ -102,7 +113,7 @@ public class ChannelManager * @return the channel manager */ public ChannelManager put(K key, C channel, A associated) { - entries.put(key, new Value<>(channel, associated)); + channels.put(key, new Both<>(channel, associated)); return this; } @@ -119,15 +130,14 @@ public class ChannelManager } /** - * 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)}. + * Returns the channel registered for the key or an empty optional + * if no mapping exists. * * @param key the key - * @return the c + * @return the optional */ - public C createChannel(K key) { - return supplier.apply(key); + public Optional channel(K key) { + return both(key).map(b -> b.channel); } /** @@ -137,8 +147,8 @@ public class ChannelManager * @param key the key * @return the channel */ - public C channelGet(K key) { - return computeIfAbsent(key, supplier); + public Optional getChannel(K key) { + return getChannel(key, supplier); } /** @@ -149,9 +159,19 @@ public class ChannelManager * @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(); + @SuppressWarnings({ "PMD.AssignmentInOperand", + "PMD.DataflowAnomalyAnalysis" }) + public Optional getChannel(K key, Function supplier) { + synchronized (channels) { + return Optional + .of(Optional.ofNullable(channels.get(key)) + .map(v -> v.channel) + .orElseGet(() -> { + var channel = supplier.apply(key); + channels.put(key, new Both<>(channel, null)); + return channel; + })); + } } /** @@ -163,17 +183,121 @@ public class ChannelManager * @return the channel manager */ public ChannelManager associate(K key, A data) { - Optional.ofNullable(entries.computeIfPresent(key, - (k, existing) -> new Value<>(existing.channel(), data))); + synchronized (channels) { + Optional.ofNullable(channels.get(key)) + .ifPresent(v -> v.associated = data); + } return this; } + /** + * Return the data associated with the entry for the channel. + * + * @param key the key + * @return the data + */ + public Optional associated(K key) { + return both(key).map(b -> b.associated); + } + + /** + * Returns all associated data. + * + * @return the collection + */ + public Collection associated() { + synchronized (channels) { + return channels.values().stream() + .filter(v -> v.associated != null) + .map(v -> v.associated).toList(); + } + } + /** * Removes the channel with the given name. * * @param name the name */ public void remove(String name) { - entries.remove(name); + synchronized (channels) { + channels.remove(name); + } + } + + /** + * Returns all known keys. + * + * @return the sets the + */ + public Set keys() { + return channels.keySet(); + } + + /** + * Returns a read only view of this channel manager. The methods + * that usually create a new entry refrain from doing so. The + * methods that change the value of channel and {@link #remove(String)} + * do nothing. The associated data, however, can still be changed. + * + * @return the channel manager + */ + public ChannelManager fixed() { + if (readOnly == null) { + readOnly = new ChannelManager<>(supplier) { + + @Override + public Optional> both(K key) { + return ChannelManager.this.both(key); + } + + @Override + public ChannelManager put(K key, C channel, + A associated) { + return associate(key, associated); + } + + @Override + public Optional getChannel(K key) { + return ChannelManager.this.channel(key); + } + + @Override + public Optional getChannel(K key, Function supplier) { + return ChannelManager.this.channel(key); + } + + @Override + public ChannelManager associate(K key, A data) { + return ChannelManager.this.associate(key, data); + } + + @Override + public Optional associated(K key) { + return ChannelManager.this.associated(key); + } + + @Override + public Collection associated() { + return ChannelManager.this.associated(); + } + + @Override + public void remove(String name) { + // Do nothing + } + + @Override + public Set keys() { + return ChannelManager.this.keys(); + } + + @Override + public ChannelManager fixed() { + return ChannelManager.this.fixed(); + } + + }; + } + return readOnly; } } 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/GetDisplayPassword.java similarity index 50% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java rename to org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplayPassword.java index b4fcf56..37eddec 100644 --- 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/GetDisplayPassword.java @@ -18,43 +18,45 @@ package org.jdrupes.vmoperator.manager.events; -import org.jdrupes.vmoperator.common.VmPool; +import java.util.Optional; +import org.jdrupes.vmoperator.common.VmDefinitionModel; import org.jgrapes.core.Event; /** - * Note the assignment to a user in the VM status. + * Gets the current display secret and optionally updates it. */ -public class UpdateAssignment extends Event { +@SuppressWarnings("PMD.DataClass") +public class GetDisplayPassword extends Event { - private final VmPool fromPool; - private final String toUser; + private final VmDefinitionModel vmDef; /** - * Instantiates a new event. + * Instantiates a new returns the display secret. * - * @param fromPool the pool from which the VM was assigned - * @param toUser the to user + * @param vmDef the vm name */ - public UpdateAssignment(VmPool fromPool, String toUser) { - this.fromPool = fromPool; - this.toUser = toUser; + public GetDisplayPassword(VmDefinitionModel vmDef) { + this.vmDef = vmDef; } /** - * Gets the pool from which the VM was assigned. + * Gets the vm definition. * - * @return the pool + * @return the vm definition */ - public VmPool fromPool() { - return fromPool; + public VmDefinitionModel vmDefinition() { + return vmDef; } /** - * Gets the user to whom the VM was assigned. + * Return the password. May only be called when the event is completed. * - * @return the to user + * @return the optional */ - public String toUser() { - return toUser; + public Optional password() { + if (!isDone()) { + throw new IllegalStateException("Event is not done."); + } + return currentResults().stream().findFirst(); } } 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/ResetVm.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ResetVm.java index 778820e..f3320c8 100644 --- 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 @@ -23,6 +23,7 @@ import org.jgrapes.core.Event; /** * Triggers a reset of the VM. */ +@SuppressWarnings("PMD.DataClass") public class ResetVm extends Event { private final String vmName; 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/ServiceChanged.java similarity index 65% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PodChanged.java rename to org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ServiceChanged.java index 8bbcfe8..a8008e0 100644 --- 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/ServiceChanged.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023 Michael N. Lipp + * 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 @@ -18,38 +18,30 @@ package org.jdrupes.vmoperator.manager.events; -import io.kubernetes.client.openapi.models.V1Pod; -import org.jdrupes.vmoperator.common.K8sObserver; +import io.kubernetes.client.openapi.models.V1Service; +import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; 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. + * Indicates that a service has changed. */ -public class PodChanged extends Event { +@SuppressWarnings("PMD.DataClass") +public class ServiceChanged extends Event { - private final V1Pod pod; - private final K8sObserver.ResponseType type; + private final ResponseType type; + private final V1Service service; /** - * Instantiates a new VM changed event. + * Initializes a new service changed event. * - * @param pod the pod * @param type the type + * @param service the service */ - public PodChanged(V1Pod pod, K8sObserver.ResponseType type) { - this.pod = pod; + public ServiceChanged(ResponseType type, V1Service service) { this.type = type; - } - - /** - * Gets the pod. - * - * @return the pod - */ - public V1Pod pod() { - return pod; + this.service = service; } /** @@ -57,15 +49,24 @@ public class PodChanged extends Event { * * @return the type */ - public K8sObserver.ResponseType type() { + public ResponseType type() { return type; } + /** + * Gets the service. + * + * @return the service + */ + public V1Service service() { + return service; + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(Components.objectName(this)).append(" [") - .append(pod.getMetadata().getName()).append(' ').append(type); + .append(service.getMetadata().getName()).append(' ').append(type); if (channels() != null) { builder.append(", channels=").append(Channel.toString(channels())); } 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..46861ce 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 @@ -19,20 +19,20 @@ package org.jdrupes.vmoperator.manager.events; import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.VmDefinition; +import org.jdrupes.vmoperator.common.VmDefinitionModel; 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 VmDefinitionModel vmDefinition; private long generation = -1; /** @@ -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(VmDefinitionModel 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 VmDefinitionModel 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,19 +104,6 @@ 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. * 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 64% 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..a2bafb7 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 @@ -19,41 +19,37 @@ package org.jdrupes.vmoperator.manager.events; import org.jdrupes.vmoperator.common.K8sObserver; -import org.jdrupes.vmoperator.common.VmDefinition; +import org.jdrupes.vmoperator.common.VmDefinitionModel; 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; private final boolean specChanged; - private final boolean podChanged; + private final VmDefinitionModel vmDef; /** * Instantiates a new VM changed event. * * @param type the type - * @param vmDefinition the VM definition * @param specChanged the spec part changed + * @param vmDefinition the VM definition */ - public VmResourceChanged(K8sObserver.ResponseType type, - VmDefinition vmDefinition, boolean specChanged, - boolean podChanged) { + public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged, + VmDefinitionModel vmDefinition) { this.type = type; - this.vmDefinition = vmDefinition; this.specChanged = specChanged; - this.podChanged = podChanged; + this.vmDef = vmDefinition; } /** @@ -65,15 +61,6 @@ public class VmResourceChanged extends Event { return type; } - /** - * Return the VM definition. - * - * @return the VM definition - */ - public VmDefinition vmDefinition() { - return vmDefinition; - } - /** * Indicates if the "spec" part changed. */ @@ -82,17 +69,19 @@ public class VmResourceChanged extends Event { } /** - * Indicates if the pod status changed. + * Returns the object. + * + * @return the object. */ - public boolean podChanged() { - return podChanged; + public VmDefinitionModel 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())); } 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/.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/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle index 4ce4ed0..a956a00 100644 --- a/org.jdrupes.vmoperator.manager/build.gradle +++ b/org.jdrupes.vmoperator.manager/build.gradle @@ -13,14 +13,15 @@ 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.34.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.webconsole.base:[1.7.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.5.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.3.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.6.0,2)' implementation 'org.jgrapes:org.jgrapes.webconlet.markdowndisplay:[1.2.0,2)' runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.4.0,2)' @@ -31,8 +32,8 @@ 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') + runtimeOnly project(':org.jdrupes.vmoperator.vmviewer') } application { @@ -45,8 +46,6 @@ application { } 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 @@ -62,47 +61,39 @@ task pushImage(type: Exec) { // Don't push without testing first dependsOn test + def registry = "${project.rootProject.properties['docker.registry']}" commandLine 'podman', 'push', '--tls-verify=false', \ - "${project.name}:${project.gitBranch}", \ + "localhost/${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}" + + if (!project.version.contains("SNAPSHOT")) { + commandLine 'podman', 'tag', \ + "${registry}/${project.name}:${project.gitBranch}",\ + "${registry}/${project.name}:${project.version}" + } } task tagAsLatest(type: Exec) { - dependsOn tagWithVersion + dependsOn pushImage - 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}",\ + def registry = "${project.rootProject.properties['docker.registry']}" + commandLine 'podman', 'tag', \ + "${registry}/${project.name}:${project.version}",\ "${registry}/${project.name}:latest" } -task publishImage { - dependsOn pushImage - dependsOn tagWithVersion - dependsOn tagAsLatest -} - task pushForTest(type: Exec) { dependsOn buildImage commandLine 'podman', 'push', '--tls-verify=false', \ - "${project.name}:${project.gitBranch}", \ + "localhost/${project.name}:${project.gitBranch}", \ "${project.rootProject.properties['docker.testRegistry']}" \ + "/${project.name}:test" } 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/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..253f9b7 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,141 @@ 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 } + resetCounter: ${ cr.resetCount } # 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 +147,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 +175,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 +187,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??> + streaming-video: "${ 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..3d4a316 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml @@ -0,0 +1,194 @@ +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 + projected: + sources: + - configMap: + name: ${ cr.metadata.name.asString } + <#if displaySecret??> + - secret: + name: ${ displaySecret } + + - 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 index c10752e..e78a5e0 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AbstractMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AbstractMonitor.java @@ -27,11 +27,14 @@ 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.Optional; 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.common.K8sObserver.ResponseType; +import org.jdrupes.vmoperator.manager.events.ChannelManager; import org.jdrupes.vmoperator.manager.events.Exit; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; @@ -42,15 +45,12 @@ 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. + * A base class for monitoring VM related resources. * * @param the object type for the context * @param the object list type for the context */ +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis" }) public abstract class AbstractMonitor extends Component { @@ -61,17 +61,16 @@ public abstract class AbstractMonitor channelManager; + private boolean channelManagerMaster; /** * 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) { + protected AbstractMonitor(Channel componentChannel, Class objectClass, + Class objectListClass) { super(componentChannel); this.objectClass = objectClass; this.objectListClass = objectListClass; @@ -157,6 +156,27 @@ public abstract class AbstractMonitor channelManager() { + return channelManager; + } + + /** + * Sets the channel manager. + * + * @param channelManager the channel manager + * @return the abstract monitor + */ + public AbstractMonitor + channelManager(ChannelManager channelManager) { + this.channelManager = channelManager; + return this; + } + /** * Looks for a key "namespace" in the configuration and, if found, * sets the namespace to its value. @@ -174,12 +194,13 @@ public abstract class AbstractMonitor "Observing " + K8s.toString(context) + + " objects in " + namespace); // Monitor all versions for (var version : context.getVersions()) { @@ -215,7 +238,13 @@ public abstract class AbstractMonitor(objectClass, objectListClass, client, K8s.preferred(context, version), namespace, options) - .handler(this::handleChange).onTerminated((o, t) -> { + .handler((c, r) -> { + handleChange(c, r); + if (ResponseType.valueOf(r.type) == ResponseType.DELETED + && channelManagerMaster) { + channelManager.remove(r.object.getMetadata().getName()); + } + }).onTerminated((o, t) -> { if (observerCounter.decrementAndGet() == 0) { unregisterAsGenerator(); } @@ -228,8 +257,7 @@ public abstract class AbstractMonitor change); + + /** + * Returns the {@link Channel} for the given name. + * + * @param name the name + * @return the channel used for events related to the specified object + */ + protected Optional channel(String name) { + return channelManager.getChannel(name); + } } 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..4219e53 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,12 @@ 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.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -56,6 +43,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()); @@ -75,70 +63,31 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; * * @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(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; } /** @@ -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/Constants.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java index 2ef4199..7de839b 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java @@ -21,8 +21,18 @@ package org.jdrupes.vmoperator.manager; /** * Some constants. */ +@SuppressWarnings("PMD.DataClass") public class Constants extends org.jdrupes.vmoperator.common.Constants { + /** The Constant COMP_DISPLAY_SECRET. */ + public static final String COMP_DISPLAY_SECRET = "display-secret"; + + /** The Constant DATA_DISPLAY_PASSWORD. */ + public static final String DATA_DISPLAY_PASSWORD = "display-password"; + + /** The Constant DATA_PASSWORD_EXPIRY. */ + public static final String DATA_PASSWORD_EXPIRY = "password-expiry"; + /** The Constant STATE_RUNNING. */ public static final String STATE_RUNNING = "Running"; 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..86e3751 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,25 @@ 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 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 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.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.common.K8sDynamicStub; import org.jdrupes.vmoperator.manager.events.ChannelManager; 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; @@ -62,7 +48,7 @@ import org.jgrapes.util.events.ConfigurationUpdate; * * 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 + * 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,7 +81,6 @@ import org.jgrapes.util.events.ConfigurationUpdate; public class Controller extends Component { private String namespace; - private final ChannelManager chanMgr; /** * Creates a new instance. @@ -104,24 +89,24 @@ public class Controller extends Component { 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)); + ChannelManager 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()).channelManager(chanMgr)); + attach(new DisplaySecretMonitor(channel()) + .channelManager(chanMgr.fixed())); // 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 Reconciler(channel())); - attach(new PoolMonitor(channel())); - attach(new PodMonitor(channel(), chanMgr)); } /** @@ -183,146 +168,40 @@ 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 onModifyVm(ModifyVm event, VmChannel channel) + throws ApiException, IOException { + patchVmDef(channel.client(), event.name(), "spec/vm/" + 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; - } - }; + private void patchVmDef(K8sClient client, String name, String path, + Object value) throws ApiException, IOException { + var vmStub = K8sDynamicStub.get(client, + new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), namespace, + name); - /** - * 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()) { - 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 + 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()); } } } 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 index b094b79..1ea766c 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2025 Michael N. Lipp + * 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 @@ -26,44 +26,80 @@ 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.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.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 static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET; +import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD; +import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY; +import org.jdrupes.vmoperator.manager.events.GetDisplayPassword; import org.jdrupes.vmoperator.manager.events.VmChannel; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jgrapes.core.Channel; +import org.jgrapes.core.CompletionLock; +import org.jgrapes.core.Event; +import org.jgrapes.core.annotation.Handler; +import org.jgrapes.util.events.ConfigurationUpdate; +import org.jose4j.base64url.Base64; /** - * 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. + * Watches for changes of display secrets. */ +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyStaticImports" }) public class DisplaySecretMonitor extends AbstractMonitor { - private final ChannelDictionary channelDictionary; + private int passwordValidity = 10; + private final List pendingGets + = Collections.synchronizedList(new LinkedList<>()); /** * Instantiates a new display secrets monitor. * * @param componentChannel the component channel - * @param channelDictionary the channel dictionary */ - public DisplaySecretMonitor(Channel componentChannel, - ChannelDictionary channelDictionary) { + public DisplaySecretMonitor(Channel componentChannel) { 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); + + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET); options(options); } + /** + * On configuration update. + * + * @param event the event + */ + @Handler + @Override + public void onConfigurationUpdate(ConfigurationUpdate event) { + super.onConfigurationUpdate(event); + event.structured(componentPath()).ifPresent(c -> { + try { + if (c.containsKey("passwordValidity")) { + passwordValidity = Integer + .parseInt((String) c.get("passwordValidity")); + } + } catch (ClassCastException e) { + logger.config("Malformed configuration: " + e.getMessage()); + } + }); + } + @Override protected void prepareMonitoring() throws IOException, ApiException { client(new K8sClient()); @@ -76,7 +112,7 @@ public class DisplaySecretMonitor if (vmName == null) { return; } - var channel = channelDictionary.channel(vmName).orElse(null); + var channel = channel(vmName).orElse(null); if (channel == null || channel.vmDefinition() == null) { return; } @@ -118,4 +154,134 @@ public class DisplaySecretMonitor + "\"}]"), patchOpts); } + + /** + * On get display secrets. + * + * @param event the event + * @param channel the channel + * @throws ApiException the api exception + */ + @Handler + @SuppressWarnings("PMD.StringInstantiation") + public void onGetDisplaySecrets(GetDisplayPassword event, VmChannel channel) + throws ApiException { + ListOptions options = new ListOptions(); + options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," + + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + "," + + "app.kubernetes.io/instance=" + + event.vmDefinition().metadata().getName()); + var stubs = K8sV1SecretStub.list(client(), + event.vmDefinition().metadata().getNamespace(), options); + if (stubs.isEmpty()) { + return; + } + var stub = stubs.iterator().next(); + + // Check validity + var model = stub.model().get(); + @SuppressWarnings("PMD.StringInstantiation") + var expiry = Optional.ofNullable(model.getData() + .get(DATA_PASSWORD_EXPIRY)).map(b -> new String(b)).orElse(null); + if (model.getData().get(DATA_DISPLAY_PASSWORD) != null + && stillValid(expiry)) { + event.setResult( + new String(model.getData().get(DATA_DISPLAY_PASSWORD))); + return; + } + updatePassword(stub, event); + } + + @SuppressWarnings("PMD.StringInstantiation") + private void updatePassword(K8sV1SecretStub stub, GetDisplayPassword event) + throws ApiException { + 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); + var model = stub.model().get(); + model.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password, + DATA_PASSWORD_EXPIRY, + Long.toString(Instant.now().getEpochSecond() + passwordValidity))); + event.setResult(password); + + // Prepare wait for confirmation (by VM status change) + var pending = new PendingGet(event, + event.vmDefinition().displayPasswordSerial().orElse(0L) + 1, + new CompletionLock(event, 1500)); + pendingGets.add(pending); + Event.onCompletion(event, e -> { + pendingGets.remove(pending); + }); + + // Update, will (eventually) trigger confirmation + stub.update(model).getObject(); + } + + 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 + public void onVmDefChanged(VmDefChanged event, Channel channel) { + synchronized (pendingGets) { + String vmName = event.vmDefinition().metadata().getName(); + for (var pending : pendingGets) { + if (pending.event.vmDefinition().metadata().getName() + .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. + */ + @SuppressWarnings("PMD.DataClass") + private static class PendingGet { + public final GetDisplayPassword event; + public final long expectedSerial; + public final CompletionLock lock; + + /** + * Instantiates a new pending get. + * + * @param event the event + * @param expectedSerial the expected serial + */ + public PendingGet(GetDisplayPassword 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/DisplaySecretReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java index 1e3eb0f..17456aa 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java @@ -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 Affero General Public License as @@ -18,9 +18,8 @@ package org.jdrupes.vmoperator.manager; -import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; 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; @@ -28,143 +27,66 @@ 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 static org.jdrupes.vmoperator.manager.Constants.APP_NAME; +import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET; +import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD; +import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY; 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.jdrupes.vmoperator.manager.events.VmDefChanged; +import org.jdrupes.vmoperator.util.GsonPtr; 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. + * Delegee for reconciling the display secret */ -public class DisplaySecretReconciler extends Component { +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +/* default */ class DisplaySecretReconciler { 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. + * Reconcile. * * @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) + public void reconcile(VmDefChanged event, + Map model, VmChannel channel) 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)) { + var display = GsonPtr.to(event.vmDefinition().data()).to("spec", "vm", + "display"); + if (!display.get(JsonPrimitive.class, "spice", "generateSecret") + .map(JsonPrimitive::getAsBoolean).orElse(true)) { return; } // Check if exists + var metadata = event.vmDefinition().getMetadata(); 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); + + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + "," + + "app.kubernetes.io/instance=" + metadata.getName()); + var stubs = K8sV1SecretStub.list(channel.client(), + metadata.getNamespace(), 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) + secret.setMetadata(new V1ObjectMeta().namespace(metadata.getNamespace()) + .name(metadata.getName() + "-" + COMP_DISPLAY_SECRET) .putLabelsItem("app.kubernetes.io/name", APP_NAME) - .putLabelsItem("app.kubernetes.io/component", DisplaySecret.NAME) - .putLabelsItem("app.kubernetes.io/instance", vmDef.name())); + .putLabelsItem("app.kubernetes.io/component", COMP_DISPLAY_SECRET) + .putLabelsItem("app.kubernetes.io/instance", metadata.getName())); secret.setType("Opaque"); SecureRandom random = null; try { @@ -176,167 +98,9 @@ public class DisplaySecretReconciler extends Component { byte[] bytes = new byte[16]; random.nextBytes(bytes); var password = Base64.encode(bytes); - secret.setStringData(Map.of(DisplaySecret.PASSWORD, password, - DisplaySecret.EXPIRY, "now")); + secret.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password, + DATA_PASSWORD_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..85158c7 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.common.K8sDynamicModel; 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,24 +68,18 @@ 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" }) + @SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "unchecked" }) var lbsDef = Optional.of(model) .map(m -> (Map) m.get("reconciler")) .map(c -> c.get(LOAD_BALANCER_SERVICE)).orElse(Boolean.FALSE); @@ -98,17 +92,14 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; 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)) { - return; + JsonObject cfgMeta = new JsonObject(); + if (lbsDef instanceof Map) { + var json = channel.client().getJSON(); + cfgMeta + = json.deserialize(json.serialize(lbsDef), JsonObject.class); } // 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 +107,53 @@ 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, cfgMeta, event.vmDefinition()); // 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()); + private void mergeMetadata(DynamicKubernetesObject svcDef, + JsonObject cfgMeta, K8sDynamicModel vmDefinition) { + // Get metadata from VM definition + var vmMeta = GsonPtr.to(vmDefinition.data()).to("spec") + .get(JsonObject.class, LOAD_BALANCER_SERVICE) + .map(JsonObject::deepCopy).orElseGet(() -> new JsonObject()); - // 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))); + // 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..9d291cf 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 @@ -81,7 +81,7 @@ 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; @@ -97,8 +97,8 @@ public class Manager extends Component { * @throws IOException Signals that an I/O exception has occurred. * @throws URISyntaxException */ - @SuppressWarnings({ "PMD.NcssCount", - "PMD.ConstructorCallsOverridableMethod" }) + @SuppressWarnings({ "PMD.TooFewBranchesForASwitchStatement", + "PMD.NcssCount", "PMD.ConstructorCallsOverridableMethod" }) public Manager(CommandLine cmdLine) throws IOException, URISyntaxException { super(new NamedChannel("manager")); // Prepare component tree @@ -217,6 +217,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 +264,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 +291,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. 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..1984f89 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,44 @@ 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 io.kubernetes.client.util.generic.options.ListOptions; 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 static org.jdrupes.vmoperator.common.Constants.APP_NAME; import org.jdrupes.vmoperator.common.Convertions; +import org.jdrupes.vmoperator.common.K8sClient; +import org.jdrupes.vmoperator.common.K8sDynamicModel; 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.common.K8sV1SecretStub; +import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET; import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; 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 +72,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 +128,16 @@ 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 +147,6 @@ public class Reconciler extends Component { * * @param componentChannel the component channel */ - @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") public Reconciler(Channel componentChannel) { super(componentChannel); @@ -173,9 +161,8 @@ 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); + dsReconciler = new DisplaySecretReconciler(); + stsReconciler = new StatefulSetReconciler(fmConfig); lbReconciler = new LoadBalancerReconciler(fmConfig); } @@ -201,27 +188,29 @@ public class Reconciler extends Component { * @throws IOException Signals that an I/O exception has occurred. */ @Handler - public void onVmResourceChanged(VmResourceChanged event, VmChannel channel) + @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() == K8sObserver.ResponseType.DELETED) { + logger.fine(() -> "VM \"" + defMeta.getName() + "\" 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()); + // Reconcile, use "augmented" vm definition for model + Map model + = prepareModel(channel.client(), patchCr(event.vmDefinition())); + var configMap = cmReconciler.reconcile(model, channel); + model.put("cm", configMap.getRaw()); + dsReconciler.reconcile(event, model, channel); + stsReconciler.reconcile(event, model, channel); + lbReconciler.reconcile(event, model, channel); } /** @@ -237,91 +226,111 @@ public class Reconciler extends Component { @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); + var defRoot + = GsonPtr.to(channel.vmDefinition().data()).get(JsonObject.class); + defRoot.addProperty("resetCount", + defRoot.get("resetCount").getAsLong() + 1); Map model - = prepareModel(channel.vmDefinition()); - cmReconciler.reconcile(model, channel, true); + = prepareModel(channel.client(), patchCr(channel.vmDefinition())); + cmReconciler.reconcile(model, channel); } - private Map prepareModel(VmDefinition vmDef) + private DynamicKubernetesObject patchCr(K8sDynamicModel vmDef) { + var json = vmDef.data().deepCopy(); + // Adjust cdromImage path + adjustCdRomPaths(json); + + // Adjust cloud-init data + adjustCloudInitData(json); + + return new DynamicKubernetesObject(json); + } + + @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") + 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(K8sClient client, + DynamicKubernetesObject vmDef) throws TemplateModelException, ApiException { @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); + + // Check if we have a display secret + ListOptions options = new ListOptions(); + options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," + + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + "," + + "app.kubernetes.io/instance=" + vmDef.getMetadata().getName()); + var dsStub = K8sV1SecretStub + .list(client, vmDef.getMetadata().getNamespace(), options).stream() + .findFirst(); + if (dsStub.isPresent()) { + dsStub.get().model().ifPresent(m -> { + model.put("displaySecret", m.getMetadata().getName()); + }); + } // 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 +339,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 +367,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/ServiceMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ServiceMonitor.java new file mode 100644 index 0000000..bd5635e --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ServiceMonitor.java @@ -0,0 +1,74 @@ +/* + * 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.openapi.ApiException; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.util.Watch.Response; +import io.kubernetes.client.util.generic.options.ListOptions; +import java.io.IOException; +import static org.jdrupes.vmoperator.common.Constants.APP_NAME; +import org.jdrupes.vmoperator.common.K8sClient; +import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; +import org.jdrupes.vmoperator.common.K8sV1ServiceStub; +import org.jdrupes.vmoperator.manager.events.ServiceChanged; +import org.jdrupes.vmoperator.manager.events.VmChannel; +import org.jgrapes.core.Channel; + +/** + * Watches for changes of services. + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class ServiceMonitor + extends AbstractMonitor { + + /** + * Instantiates a new display secrets monitor. + * + * @param componentChannel the component channel + */ + public ServiceMonitor(Channel componentChannel) { + super(componentChannel, V1Service.class, V1ServiceList.class); + context(K8sV1ServiceStub.CONTEXT); + ListOptions options = new ListOptions(); + options.setLabelSelector("app.kubernetes.io/name=" + APP_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 = channel(vmName).orElse(null); + if (channel == null || channel.vmDefinition() == null) { + return; + } + channel.pipeline().fire(new ServiceChanged( + ResponseType.valueOf(change.type), change.object), channel); + } +} 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..baf833c --- /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.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.K8sV1StatefulSetStub; +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 { + 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 stsStub = K8sV1StatefulSetStub.get(channel.client(), + metadata.getNamespace(), metadata.getName()); + var stsModel = stsStub.model().orElse(null); + if (stsModel != null) { + var current = stsModel.getSpec().getReplicas(); + 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"); + if (stsStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML, + new V1Patch(channel.client().getJSON().serialize(stsDef)), opts) + .isEmpty()) { + logger.warning( + () -> "Could not patch stateful set for " + stsStub.name()); + } + } + +} 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 index 22f083c..e049b17 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp + * 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 @@ -18,76 +18,51 @@ package org.jdrupes.vmoperator.manager; +import com.google.gson.JsonArray; 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.models.V1ObjectMeta; 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.logging.Level; import java.util.stream.Collectors; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.Status; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; 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.K8sV1PodStub; import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub; -import org.jdrupes.vmoperator.common.VmDefinition; +import org.jdrupes.vmoperator.common.VmDefinitionModel; +import org.jdrupes.vmoperator.common.VmDefinitionModels; 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_KIND_VM; 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.manager.events.VmDefChanged; 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. + * Watches for changes of VM definitions. */ +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" }) public class VmMonitor extends - AbstractMonitor { - - private final ChannelManager channelManager; + AbstractMonitor { /** * 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; + public VmMonitor(Channel componentChannel) { + super(componentChannel, VmDefinitionModel.class, + VmDefinitionModels.class); } @Override @@ -95,7 +70,7 @@ public class VmMonitor extends client(new K8sClient()); // Get all our API versions - var ctx = K8s.context(client(), Crd.GROUP, "", Crd.KIND_VM); + var ctx = K8s.context(client(), VM_OP_GROUP, "", VM_OP_KIND_VM); if (ctx.isEmpty()) { logger.severe(() -> "Cannot get CRD context."); return; @@ -106,6 +81,7 @@ public class VmMonitor extends purge(); } + @SuppressWarnings("PMD.CognitiveComplexity") private void purge() throws ApiException { // Get existing CRs (VMs) var known = K8sDynamicStub.list(client(), context(), namespace()) @@ -129,19 +105,13 @@ public class VmMonitor extends @Override protected void handleChange(K8sClient client, - Watch.Response response) { - var name = response.object.getMetadata().getName(); + Watch.Response response) { + V1ObjectMeta metadata = response.object.getMetadata(); + VmChannel channel = channel(metadata.getName()).orElse(null); + if (channel == null) { + return; + } - // 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) { @@ -149,39 +119,30 @@ public class VmMonitor extends // 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()); + addDynamicData(channel.client(), vmDef, channel.vmDefinition()); channel.setVmDefinition(vmDef); } else { - // Reuse cached (e.g. if deleted) + // Reuse cached vmDef = channel.vmDefinition(); } if (vmDef == null) { - logger.warning(() -> "Cannot get defintion for " - + response.object.getMetadata()); + logger.warning( + () -> "Cannot get model 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); + // Create and fire event + channel.pipeline() + .fire(new VmDefChanged(ResponseType.valueOf(response.type), + channel.setGeneration( + response.object.getMetadata().getGeneration()), + vmDef), channel); } - private VmDefinition getModel(K8sClient client, VmDefinition vmDef) { + private VmDefinitionModel getModel(K8sClient client, + VmDefinitionModel vmDef) { try { return VmDefinitionStub.get(client, context(), namespace(), vmDef.metadata().getName()).model().orElse(null); @@ -190,137 +151,55 @@ public class VmMonitor extends } } - private void addExtraData(VmDefinition vmDef, VmDefinition prevState) { - var extra = new VmExtraData(vmDef); - var prevExtra = Optional.ofNullable(prevState).map(VmDefinition::extra); + private void addDynamicData(K8sClient client, VmDefinitionModel vmState, + VmDefinitionModel prevState) { + var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class); // Maintain (or initialize) the resetCount - extra.resetCount(prevExtra.map(VmExtraData::resetCount).orElse(0L)); + rootNode.addProperty("resetCount", Optional.ofNullable(prevState) + .map(ps -> GsonPtr.to(ps.data())) + .flatMap(d -> d.getAsLong("resetCount")).orElse(0L)); - // Maintain node info - prevExtra - .ifPresent(e -> extra.nodeInfo(e.nodeName(), e.nodeAddresses())); - } + // Add defaults in case the VM is not running + rootNode.addProperty("nodeName", ""); + rootNode.addProperty("nodeAddress", ""); - /** - * 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()); + // 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; } - - // 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 { + var podSearch = new ListOptions(); + podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + + ",app.kubernetes.io/component=" + APP_NAME + + ",app.kubernetes.io/instance=" + vmState.getMetadata().getName()); 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); + var podList + = K8sV1PodStub.list(client, namespace(), podSearch); + for (var podStub : podList) { + var nodeName = podStub.model().get().getSpec().getNodeName(); + rootNode.addProperty("nodeName", nodeName); + logger.fine(() -> "Added node name " + nodeName + + " to VM info for " + vmState.getMetadata().getName()); + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var addrs = new JsonArray(); + podStub.model().get().getStatus().getPodIPs().stream() + .map(ip -> ip.getIp()).forEach(addrs::add); + rootNode.add("nodeAddresses", addrs); + logger.fine(() -> "Added node addresses " + addrs + + " to VM info for " + vmState.getMetadata().getName()); } } catch (ApiException e) { - // Log exceptions except for conflict, which can be expected - if (HttpURLConnection.HTTP_CONFLICT != e.getCode()) { - throw e; - } + logger.log(Level.WARNING, e, + () -> "Cannot access node information: " + e.getMessage()); } - event.setResult(false); } - } 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..337b5e3 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 @@ -83,18 +83,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] 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..13a93e1 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,18 @@ 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 java.io.IOException; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; 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 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.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.jdrupes.vmoperator.common.K8sV1StatefulSetStub; import org.junit.jupiter.api.AfterAll; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.BeforeAll; @@ -40,9 +26,6 @@ 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(); @BeforeAll static void setUpBeforeClass() throws Exception { @@ -52,40 +35,23 @@ class BasicTests { // 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(); - // Context for working with our CR - var apiRes = K8s.context(client, Crd.GROUP, null, Crd.KIND_VM); + var apiRes = K8s.context(client, VM_OP_GROUP, null, VM_OP_KIND_VM); assertTrue(apiRes.isPresent()); vmsContext = apiRes.get(); // Cleanup existing VM - K8sDynamicStub.get(client, vmsContext, "vmop-test", VM_NAME) + K8sDynamicStub.get(client, vmsContext, "vmop-dev", "unittest-vm") .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(); - // 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 + mgrDeployment + = K8sV1DeploymentStub.get(client, "vmop-dev", "vm-operator"); + mgrDeployment.scale(0); + mgrDeployment.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() .stream().filter(c -> "Available".equals(c.getType())).findAny() @@ -97,245 +63,60 @@ 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); } @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, ApiException { + // Load from Yaml + var rdr = new FileReader("test-resources/unittest-vm.yaml"); + var vmStub = K8sDynamicStub.createFromYaml(client, vmsContext, rdr); + assertTrue(vmStub.model().isPresent()); + + // Wait for created resources + assertTrue(waitForConfigMap(client)); + assertTrue(waitForStatefulSet(client)); + // 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); - - 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())) + var config = K8sV1ConfigMapStub.get(client, "vmop-dev", "unittest-vm") + .model().get(); + var yaml = new Yaml(new SafeConstructor(new LoaderOptions())) .load(config.getData().get("config.yaml")); - checkProps(cm, toCheck); + @SuppressWarnings("unchecked") + var maximumRam = ((Map>>) yaml) + .get("/Runner").get("vm").get("maximumRam"); + assertEquals("4 GiB", maximumRam); + + // Cleanup + K8sDynamicStub.get(client, vmsContext, "vmop-dev", "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; - for (int i = 0; i < 10; i++) { - secrets = K8sV1SecretStub.list(client, "vmop-test", listOpts); - if (secrets.size() > 0) { - break; - } - 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"))); - } - - @Test - void testRunnerPvc() throws ApiException, InterruptedException { - var stub - = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-runner-data"); + private boolean waitForConfigMap(K8sClient client) + throws InterruptedException, ApiException { + var stub = K8sV1ConfigMapStub.get(client, "vmop-dev", "unittest-vm"); for (int i = 0; i < 10; i++) { if (stub.model().isPresent()) { - break; + 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"))); + return false; } - @Test - void testSystemDiskPvc() throws ApiException, InterruptedException { - var stub - = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-system-disk"); + private boolean waitForStatefulSet(K8sClient client) + throws InterruptedException, ApiException { + var stub = K8sV1StatefulSetStub.get(client, "vmop-dev", "unittest-vm"); for (int i = 0; i < 10; i++) { if (stub.model().isPresent()) { - break; + 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, - 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/build.gradle b/org.jdrupes.vmoperator.runner.qemu/build.gradle index 695c815..00bf7ea 100644 --- a/org.jdrupes.vmoperator.runner.qemu/build.gradle +++ b/org.jdrupes.vmoperator.runner.qemu/build.gradle @@ -9,10 +9,10 @@ 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' @@ -32,10 +32,8 @@ application { } 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' @@ -44,40 +42,38 @@ task buildImageArch(type: Exec) { '-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch', '.' } -task pushImageArch(type: Exec) { - dependsOn buildImageArch +task pushArchImage(type: Exec) { + dependsOn buildArchImage + def registry = "${project.rootProject.properties['docker.registry']}" commandLine 'podman', 'push', '--tls-verify=false', \ - "${project.name}-arch:${project.gitBranch}", \ + "localhost/${project.name}-arch:${project.gitBranch}", \ "${registry}/${project.name}-arch:${project.gitBranch}" -} - -task tagWithVersionArch(type: Exec) { - dependsOn pushImageArch - - enabled = !rootVersion.contains("SNAPSHOT") - - commandLine 'podman', 'push', \ - "${project.name}-arch:${project.gitBranch}",\ - "${registry}/${project.name}-arch:${project.version}" + + if (!project.version.contains("SNAPSHOT")) { + commandLine 'podman', 'tag', \ + "${registry}/${project.name}-arch:${project.gitBranch}",\ + "${registry}/${project.name}-arch:${project.version}" + } } task tagAsLatestArch(type: Exec) { - dependsOn tagWithVersionArch + dependsOn pushArchImage - 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}",\ + def registry = "${project.rootProject.properties['docker.registry']}" + commandLine 'podman', 'tag', \ + "${registry}/${project.name}-arch:${project.version}",\ "${registry}/${project.name}-arch:latest" } -task buildImageAlpine(type: Exec) { +task buildAlpineImage(type: Exec) { dependsOn installDist inputs.files 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine' @@ -86,44 +82,44 @@ task buildImageAlpine(type: Exec) { '-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine', '.' } -task pushImageAlpine(type: Exec) { - dependsOn buildImageAlpine +task pushAlpineImage(type: Exec) { + dependsOn buildAlpineImage + def registry = "${project.rootProject.properties['docker.registry']}" 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") - - commandLine 'podman', 'push', \ - "${project.name}-alpine:${project.gitBranch}",\ - "${registry}/${project.name}-alpine:${project.version}" + + if (!project.version.contains("SNAPSHOT")) { + commandLine 'podman', 'tag', \ + "${registry}/${project.name}-alpine:${project.gitBranch}",\ + "${registry}/${project.name}-alpine:${project.version}" + } } task tagAsLatestAlpine(type: Exec) { - dependsOn tagWithVersionAlpine + dependsOn pushAlpineImage - 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}",\ + def registry = "${project.rootProject.properties['docker.registry']}" + commandLine 'podman', 'tag', \ + "${registry}/${project.name}-alpine:${project.version}",\ "${registry}/${project.name}-alpine:latest" } -task publishImage { - dependsOn pushImageArch - dependsOn tagWithVersionArch +task pushImage { + dependsOn pushArchImage + dependsOn pushAlpineImage +} + +task tagAsLatest { dependsOn tagAsLatestArch - dependsOn pushImageAlpine - dependsOn tagWithVersionAlpine dependsOn tagAsLatestAlpine } + diff --git a/org.jdrupes.vmoperator.runner.qemu/password-expiry b/org.jdrupes.vmoperator.runner.qemu/password-expiry index 8a606d5..c42fe6e 100644 --- a/org.jdrupes.vmoperator.runner.qemu/password-expiry +++ b/org.jdrupes.vmoperator.runner.qemu/password-expiry @@ -1 +1 @@ -+1800 \ No newline at end of file ++30 \ 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..0a8971c 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 @@ -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,7 +66,8 @@ public class CdMediaController extends Component { * @param event the event */ @Handler - @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" }) + @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", + "PMD.AvoidInstantiatingObjectsInLoops" }) public void onConfigureQemu(ConfigureQemu event) { if (event.runState() == RunState.TERMINATING) { return; 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..4e89944 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,9 +39,11 @@ 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. */ @@ -93,12 +95,15 @@ public class Configuration implements Dto { 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; } @@ -240,12 +245,6 @@ public class Configuration implements Dto { */ 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; } @@ -294,6 +293,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 +313,7 @@ public class Configuration implements Dto { } } + @SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts") private boolean checkRuntimeDir() { // Runtime directory (sockets etc.) if (runtimeDir == null) { @@ -348,6 +349,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/CpuController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java index 440da91..b0abfd4 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 @@ -41,6 +41,7 @@ import org.jgrapes.core.annotation.Handler; /** * The Class CpuController. */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class CpuController extends Component { private Integer currentCpus; 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 index c3bec93..1f9833c 100644 --- 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 @@ -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 @@ -22,20 +22,14 @@ 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; @@ -43,14 +37,14 @@ import org.jgrapes.util.events.WatchFile; /** * The Class DisplayController. */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class DisplayController extends Component { + public static final String DISPLAY_PASSWORD_FILE = "display-password"; + public static final String PASSWORD_EXPIRY_FILE = "password-expiry"; private String currentPassword; private String protocol; private final Path configDir; - private boolean canBeUpdated; - private boolean vmopAgentConnected; - private String loggedInUser; /** * Instantiates a new Display controller. @@ -58,11 +52,12 @@ public class DisplayController extends Component { * @param componentChannel the component channel * @param configDir */ - @SuppressWarnings({ "PMD.ConstructorCallsOverridableMethod" }) + @SuppressWarnings({ "PMD.AssignmentToNonFinalStatic", + "PMD.ConstructorCallsOverridableMethod" }) public DisplayController(Channel componentChannel, Path configDir) { super(componentChannel); this.configDir = configDir; - fire(new WatchFile(configDir.resolve(DisplaySecret.PASSWORD))); + fire(new WatchFile(configDir.resolve(DISPLAY_PASSWORD_FILE))); } /** @@ -77,33 +72,7 @@ public class DisplayController extends Component { } 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); + updatePassword(); } /** @@ -112,16 +81,15 @@ public class DisplayController extends Component { * @param event the event */ @Handler + @SuppressWarnings("PMD.EmptyCatchBlock") public void onFileChanged(FileChanged event) { - if (event.path().equals(configDir.resolve(DisplaySecret.PASSWORD))) { - logger.fine(() -> "Display password updated"); - if (canBeUpdated) { - configurePassword(); - } + if (event.path().equals(configDir.resolve(DISPLAY_PASSWORD_FILE))) { + updatePassword(); } } - private void configurePassword() { + @SuppressWarnings("PMD.DataflowAnomalyAnalysis") + private void updatePassword() { if (protocol == null) { return; } @@ -131,41 +99,47 @@ public class DisplayController extends Component { } private boolean setDisplayPassword() { - return readFromFile(DisplaySecret.PASSWORD).map(password -> { - if (Objects.equals(this.currentPassword, password)) { - return true; + String password; + Path dpPath = configDir.resolve(DISPLAY_PASSWORD_FILE); + if (dpPath.toFile().canRead()) { + logger.finer(() -> "Found display password"); + try { + password = Files.readString(dpPath); + } catch (IOException e) { + logger.log(Level.WARNING, e, () -> "Cannot read display" + + " password: " + e.getMessage()); + return false; } - this.currentPassword = password; - logger.fine(() -> "Updating display password"); - fire(new MonitorCommand( - new QmpSetDisplayPassword(protocol, password))); + } else { + logger.finer(() -> "No display password"); + return false; + } + + if (Objects.equals(this.currentPassword, password)) { return true; - }).orElse(false); + } + this.currentPassword = password; + logger.fine(() -> "Updating display password"); + fire(new MonitorCommand(new QmpSetDisplayPassword(protocol, password))); + return true; } private void setPasswordExpiry() { - readFromFile(DisplaySecret.EXPIRY).ifPresent(expiry -> { - logger.fine(() -> "Updating expiry time to " + expiry); - fire( - new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry))); - }); + Path pePath = configDir.resolve(PASSWORD_EXPIRY_FILE); + if (!pePath.toFile().canRead()) { + return; + } + logger.finer(() -> "Found expiry time"); + String expiry; + try { + expiry = Files.readString(pePath); + } catch (IOException e) { + logger.log(Level.WARNING, e, () -> "Cannot read expiry" + + " time: " + e.getMessage()); + return; + } + logger.fine(() -> "Updating expiry time"); + 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..f59375c 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,15 +19,19 @@ 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; @@ -38,14 +42,24 @@ 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.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,23 +68,30 @@ 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. */ + @SuppressWarnings({ "PMD.AssignmentToNonFinalStatic", + "PMD.ConstructorCallsOverridableMethod" }) public QemuMonitor(Channel componentChannel, Path configDir) throws IOException { super(componentChannel); @@ -90,45 +111,121 @@ 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 + */ + @SuppressWarnings("resource") + @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 +239,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 +257,30 @@ 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 + @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") + public void onExecQmpCommand(MonitorCommand event) { var command = event.command(); - logger.fine(() -> "QMP handles: " + event.toString()); + logger.fine(() -> "monitor(out): " + command.toString()); String asText; try { asText = command.asText(); - logger.finer(() -> "monitor(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); - } + 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 +290,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,53 +329,26 @@ 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) { int newTimeout = event.configuration().vm.powerdownTimeout; if (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..9cdc2b5 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 @@ -39,6 +39,7 @@ public class RamController extends Component { * * @param componentChannel the component channel */ + @SuppressWarnings("PMD.AssignmentToNonFinalStatic") public RamController(Channel componentChannel) { super(componentChannel); } 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..c837537 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,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 @@ -41,29 +41,24 @@ 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.RunnerStateChange; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; @@ -157,15 +152,6 @@ import org.jgrapes.util.events.WatchFile; * * 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 +187,13 @@ import org.jgrapes.util.events.WatchFile; * */ @SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace", - "PMD.TooManyMethods", "PMD.CouplingBetweenObjects" }) + "PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods", + "PMD.CouplingBetweenObjects" }) 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,23 +202,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; @@ -240,7 +227,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 +235,8 @@ 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", + "PMD.ConstructorCallsOverridableMethod" }) public Runner(CommandLine cmdLine) throws IOException { yamlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); @@ -285,8 +273,6 @@ public class Runner extends Component { 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(new StatusUpdater(channel())); attach(new YamlConfigurationStore(channel(), configFile, false)); fire(new WatchFile(configFile.toPath())); @@ -307,84 +293,67 @@ 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); + Path dsPath + = configDir.resolve(DisplayController.DISPLAY_PASSWORD_FILE); 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 ConfigureQemu(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 +364,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 +377,7 @@ public class Runner extends Component { } } - private JsonNode dataFromTemplate(Configuration config) + private JsonNode dataFromTemplate() throws IOException, TemplateNotFoundException, MalformedTemplateNameException, ParseException, TemplateException, JsonProcessingException, JsonMappingException { @@ -442,32 +405,15 @@ public class Runner extends Component { model.put("hasDisplayPassword", config.hasDisplayPassword); model.put("cloudInit", config.cloudInit); model.put("vm", config.vm); - logger.finest(() -> "Processing template with model: " + model); // 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 +421,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 +432,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) { @@ -525,18 +472,17 @@ public class Runner extends Component { "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,7 +495,7 @@ public class Runner extends Component { } } - private void generateCloudInitImg(Configuration config) { + private void generateCloudInitImg() { try { var cloudInitDir = config.dataDir.resolve("cloud-init"); cloudInitDir.toFile().mkdir(); @@ -586,7 +532,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,7 +546,7 @@ 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); } @@ -615,13 +561,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,50 +602,44 @@ 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 ConfigureQemu(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; fire(new MonitorCommand(new QmpCont())); + state = RunState.RUNNING; rep.fire(new RunnerStateChange(state, "VmStarted", "Qemu has been configured and is continuing")); } } /** - * Receiving the OSinfo means that the OS has been booted. + * On configure qemu. * * @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.")); + public void onConfigureQemu(ConfigureQemu event) { + if (state == RunState.RUNNING) { + if (resetCounter != null + && event.configuration().resetCounter != null + && event.configuration().resetCounter > resetCounter) { + fire(new MonitorCommand(new QmpReset())); + } + resetCounter = event.configuration().resetCounter; } } @@ -716,7 +658,6 @@ public class Runner extends Component { mayBeStartQemu(QemuPreps.CloudInit); return; } - // No other process(es) may exit during startup if (state == RunState.STARTING) { logger.severe(() -> "Process " + procDef.name @@ -725,9 +666,7 @@ public class Runner extends Component { 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 == RunState.RUNNING) { rep.fire(new Exit(event.exitValue())); } logger.info(() -> "Process " + procDef.name @@ -771,6 +710,7 @@ public class Runner extends Component { "The VM has been shut down")); } + @SuppressWarnings("PMD.ConfusingArgumentToVarargsMethod") private void shutdown() { if (!Set.of(RunState.TERMINATING, RunState.STOPPED).contains(state)) { fire(new Stop()); @@ -781,7 +721,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 +745,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..412681f 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,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 @@ -18,80 +18,81 @@ 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.models.EventsV1Event; 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.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.K8sClient; +import org.jdrupes.vmoperator.common.K8sDynamicModel; +import org.jdrupes.vmoperator.common.VmDefinitionModel; 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.RunnerStateChange; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; 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(RunState.RUNNING, RunState.TERMINATING); + private String namespace; + private String vmName; + private K8sClient apiClient; + 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. * * @param componentChannel the component channel */ + @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") public StatusUpdater(Channel componentChannel) { super(componentChannel); - attach(new ConsoleTracker(componentChannel)); + 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)); + } } /** @@ -108,6 +109,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. * @@ -122,19 +160,10 @@ public class StatusUpdater extends VmDefUpdater { } try { vmStub = VmDefinitionStub.get(apiClient, - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), + new GroupVersionKind(VM_OP_GROUP, "", VM_OP_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; + vmStub.model().ifPresent(model -> { + observedGeneration = model.getMetadata().getGeneration(); }); } catch (ApiException e) { logger.log(Level.SEVERE, e, @@ -151,28 +180,38 @@ public class StatusUpdater extends VmDefUpdater { * @throws ApiException */ @Handler + @SuppressWarnings("PMD.AvoidDuplicateLiterals") public void onConfigureQemu(ConfigureQemu 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) { return; } - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); + + // A change of the runner configuration is typically caused + // by a new version of the CR. So we update only if we have + // a new version of the CR. There's one exception: the display + // password is configured by a file, not by the CR. + var vmDef = vmStub.model(); + if (vmDef.isPresent() + && vmDef.get().metadata().getGeneration() == observedGeneration + && (event.configuration().hasDisplayPassword + || vmDef.get().status().getAsJsonPrimitive( + "displayPasswordSerial").getAsInt() == -1)) { + return; + } + vmStub.updateStatus(vmDef.get(), from -> { + JsonObject status = from.status(); if (!event.configuration().hasDisplayPassword) { - status.addProperty(Status.DISPLAY_PASSWORD_SERIAL, -1); + status.addProperty("displayPasswordSerial", -1); } 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,46 +223,33 @@ public class StatusUpdater extends VmDefUpdater { * @throws ApiException */ @Handler - @SuppressWarnings({ "PMD.AssignmentInOperand" }) + @SuppressWarnings({ "PMD.AssignmentInOperand", + "PMD.AvoidLiteralsInIfCondition" }) public void onRunnerStateChanged(RunnerStateChange event) throws ApiException { - VmDefinition vmDef; + VmDefinitionModel vmDef; if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == 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()); + vmStub.updateStatus(vmDef, from -> { + JsonObject status = from.status(); + status.getAsJsonArray("conditions").asList().stream() + .map(cond -> (JsonObject) cond) + .forEach(cond -> { + if ("Running".equals(cond.get("type").getAsString())) { + updateRunningCondition(event, from, cond); + } + }); if (event.runState() == RunState.STARTING) { - status.addProperty(Status.RAM, GsonPtr.to(from.data()) + status.addProperty("ram", GsonPtr.to(from.data()) .getAsString("spec", "vm", "maximumRam").orElse("0")); - status.addProperty(Status.CPUS, 1); + status.addProperty("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("ram", "0"); + status.addProperty("cpus", 0); } return status; - }, vmDef); + }); // Maybe stop VM if (event.runState() == RunState.TERMINATING && !event.failed() @@ -241,38 +267,36 @@ public class StatusUpdater extends VmDefUpdater { // Log event var evt = new EventsV1Event() - .reportingController(Crd.GROUP + "/" + APP_NAME) + .reportingController(VM_OP_GROUP + "/" + APP_NAME) .action("StatusUpdate").reason(event.reason()) .note(event.message()); K8s.createEvent(apiClient, vmDef, evt); } - 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, + K8sDynamicModel from, JsonObject cond) { + boolean reportedRunning + = "True".equals(cond.get("status").getAsString()); + if (RUNNING_STATES.contains(event.runState()) + && !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.runState()) + && 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 @@ -282,45 +306,10 @@ public class StatusUpdater extends VmDefUpdater { if (vmStub == 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) + JsonObject status = from.status(); + status.addProperty("ram", + new Quantity(new BigDecimal(event.size()), Format.BINARY_SI) .toSuffixedString()); return status; }); @@ -338,8 +327,8 @@ public class StatusUpdater extends VmDefUpdater { return; } vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.CPUS, event.usedCpus().size()); + JsonObject status = from.status(); + status.addProperty("cpus", event.usedCpus().size()); return status; }); } @@ -357,9 +346,9 @@ public class StatusUpdater extends VmDefUpdater { return; } vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.DISPLAY_PASSWORD_SERIAL, - status.get(Status.DISPLAY_PASSWORD_SERIAL).getAsLong() + 1); + JsonObject status = from.status(); + status.addProperty("displayPasswordSerial", + status.get("displayPasswordSerial").getAsLong() + 1); return status; }); } @@ -374,76 +363,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/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..f91d702 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 @@ -30,7 +30,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(); /** 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 index 5364811..0bcffc4 100644 --- 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 @@ -25,7 +25,8 @@ import com.fasterxml.jackson.databind.JsonNode; */ public class QmpReset extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"system_reset\" }"); 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/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/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/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/RunnerStateChange.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java index 261eebf..bb6ab10 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,31 +25,14 @@ 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); - } + INITIALIZING, STARTING, RUNNING, TERMINATING, STOPPED } private final RunState state; 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/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..d100554 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/> 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..8b84ed3 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()); } @@ -325,18 +289,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 +336,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/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-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/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/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/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/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/.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/org.jdrupes.vmoperator.vmmgmt/.eclipse-pmd b/org.jdrupes.vmoperator.vmconlet/.eclipse-pmd similarity index 100% rename from org.jdrupes.vmoperator.vmmgmt/.eclipse-pmd rename to org.jdrupes.vmoperator.vmconlet/.eclipse-pmd 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 58% 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..708a1a3 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 @@ -
-