From 5a7e5183b7abd03d95bcf66e84f9ccaeb2e107a5 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 14 Oct 2026 16:58:15 +0200 Subject: [PATCH] Install ipmicli with one command: curl | sh, irm | iex (#163) Port Jawk's installers to the documentation site: - src/site/resources/get (POSIX sh): downloads the standalone jar of the latest release (or IPMICLI_VERSION), verifies its SHA-256 checksum in a temporary directory before touching an existing installation, installs it under ${XDG_DATA_HOME:-~/.local/share}/ipmicli and an ipmicli launcher in ~/.local/bin (IPMICLI_INSTALL_DIR), and prints the PATH line when needed. - src/site/resources/get.ps1 (PowerShell): the same under %LOCALAPPDATA%\ipmicli, with an ipmicli.cmd shim added to the user PATH. - The launchers find Java 8 or later at run time (IPMICLI_JAVA_HOME, JAVA_HOME, PATH, java_home on macOS, sdkman, /usr/lib/jvm), exit 127 with a pointer to adoptium.net without one, and export IPMICLI_PROGRAM_NAME so the usage names ipmicli. Unlike Jawk, the checksum is mandatory and there is no fallback for releases without the version-less alias: no release before 2.0.00 has the command-line client. release.yml gets a job after the shared release workflow that uploads the ipmi-java-standalone.jar alias and the .sha256 checksums of both jars. installers.yml runs both installers against the latest release on pull requests that touch them and weekly; it skips with a notice while the latest release has no standalone jar (1.2.02). Tested against a local copy of the release layout: WSL Ubuntu (dash) and Windows PowerShell 5.1 (install, --version, --help, pinned reinstall, tampered jar leaves the installation untouched, missing release, no Java). Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 4 + .github/workflows/installers.yml | 208 ++++++++++++++++++++++++++++++ .github/workflows/release.yml | 24 ++++ README.md | 23 +++- src/site/markdown/cli.md | 31 +++-- src/site/markdown/index.md | 7 +- src/site/markdown/installation.md | 67 +++++++++- src/site/markdown/upgrading.md | 3 +- src/site/resources/get | 179 +++++++++++++++++++++++++ src/site/resources/get.ps1 | 163 +++++++++++++++++++++++ 10 files changed, 682 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/installers.yml create mode 100644 src/site/resources/get create mode 100644 src/site/resources/get.ps1 diff --git a/.gitattributes b/.gitattributes index abf58b0..19b850e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -41,6 +41,10 @@ mvnw text eol=lf gradlew text eol=lf +# The sh installer served from the documentation site must keep Unix line endings +# everywhere, or a site built from a CRLF checkout would ship a script that sh rejects +src/site/resources/get text eol=lf + # These are explicitly windows files and should use crlf *.bat text eol=crlf *.cmd text eol=crlf diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml new file mode 100644 index 0000000..cd95dd5 --- /dev/null +++ b/.github/workflows/installers.yml @@ -0,0 +1,208 @@ +name: Installer Smoke Tests + +# Exercises the one-command installers served from the documentation site +# (src/site/resources/get and get.ps1) against the real latest GitHub release. +# The weekly run also guards against GitHub-side changes (release asset URL +# behavior) that the installers depend on. + +on: + pull_request: + branches: [ "main" ] + paths: + - src/site/resources/get + - src/site/resources/get.ps1 + - .github/workflows/installers.yml + push: + branches: [ "main" ] + paths: + - src/site/resources/get + - src/site/resources/get.ps1 + - .github/workflows/installers.yml + schedule: + # Weekly, Monday 06:37 UTC + - cron: "37 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + probe: + name: Probe the latest release + runs-on: ubuntu-latest + outputs: + version: ${{ steps.release.outputs.version }} + available: ${{ steps.release.outputs.available }} + steps: + - name: Look for the standalone jar alias in the latest release + id: release + env: + GH_TOKEN: ${{ github.token }} + run: | + release_json="$(gh api repos/${{ github.repository }}/releases/latest)" + version="$(printf '%s' "${release_json}" | jq -r '.tag_name | ltrimstr("v")')" + available="$(printf '%s' "${release_json}" | jq -r '[.assets[].name] | index("ipmi-java-standalone.jar") != null')" + echo "Latest release: ${version} (publishes ipmi-java-standalone.jar: ${available})" + if [ "${available}" != "true" ]; then + echo "::notice::Release v${version} publishes no ipmi-java-standalone.jar (the command-line client ships with 2.0.00 and later): skipping the installer smoke tests." + fi + echo "version=${version}" >> "${GITHUB_OUTPUT}" + echo "available=${available}" >> "${GITHUB_OUTPUT}" + + posix: + name: POSIX installer on ubuntu-latest + needs: probe + if: needs.probe.outputs.available == 'true' + runs-on: ubuntu-latest + env: + VERSION: ${{ needs.probe.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Set up Temurin JDK 17 + uses: actions/setup-java@v6 + with: + distribution: temurin + java-version: "17" + + - name: Install the latest release + run: | + sh src/site/resources/get 2>&1 | tee install.log + test -x "${HOME}/.local/bin/ipmicli" || { echo "ERROR: the launcher was not installed at ~/.local/bin/ipmicli" >&2; exit 1; } + grep -Fq 'Checksum verified.' install.log || { echo "ERROR: the installer did not verify the checksum." >&2; exit 1; } + + - name: Check the version and help output of the launcher + run: | + out="$("${HOME}/.local/bin/ipmicli" --version)" + [ "${out}" = "ipmi-java ${VERSION}" ] || { echo "ERROR: expected 'ipmi-java ${VERSION}' but got '${out}'." >&2; exit 1; } + # The launcher exports IPMICLI_PROGRAM_NAME so the usage names the + # ipmicli command instead of "java -jar ...". + help="$("${HOME}/.local/bin/ipmicli" --help)" + printf '%s\n' "${help}" | grep -q '^ ipmicli -h probe' \ + || { echo "ERROR: the help output does not name the ipmicli launcher." >&2; exit 1; } + if printf '%s\n' "${help}" | grep -Fq 'java -jar'; then + echo "ERROR: the help of the installed launcher mentions 'java -jar'." >&2 + exit 1 + fi + + - name: Reinstall in place a pinned version with IPMICLI_VERSION + run: | + IPMICLI_VERSION="${VERSION}" sh src/site/resources/get + out="$("${HOME}/.local/bin/ipmicli" --version)" + [ "${out}" = "ipmi-java ${VERSION}" ] || { echo "ERROR: expected 'ipmi-java ${VERSION}' but got '${out}'." >&2; exit 1; } + + - name: Exit 127 with the Adoptium message when no JRE is findable + run: | + # Hide every JRE the launcher can discover: IPMICLI_JAVA_HOME/JAVA_HOME + # and PATH go away with env -i, ~/.sdkman with a fake HOME, and the + # /usr/lib/jvm well-known root is moved aside for the duration. + mkdir -p "${RUNNER_TEMP}/nohome" + if [ -d /usr/lib/jvm ]; then sudo mv /usr/lib/jvm /usr/lib/jvm.hidden; fi + set +e + err="$(env -i HOME="${RUNNER_TEMP}/nohome" PATH=/nonexistent /bin/sh "${HOME}/.local/bin/ipmicli" --version 2>&1 >/dev/null)" + rc=$? + set -e + if [ -d /usr/lib/jvm.hidden ]; then sudo mv /usr/lib/jvm.hidden /usr/lib/jvm; fi + printf '%s\n' "${err}" + [ "${rc}" -eq 127 ] || { echo "ERROR: expected exit code 127 but got ${rc}." >&2; exit 1; } + printf '%s\n' "${err}" | grep -Fq 'adoptium.net' \ + || { echo "ERROR: the error message does not point to adoptium.net." >&2; exit 1; } + + windows: + name: Windows installer with ${{ matrix.shell }} + needs: probe + if: needs.probe.outputs.available == 'true' + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + # powershell is Windows PowerShell 5.1, pwsh is PowerShell 7. + shell: [ powershell, pwsh ] + defaults: + run: + shell: ${{ matrix.shell }} + env: + VERSION: ${{ needs.probe.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Set up Temurin JDK 17 + uses: actions/setup-java@v6 + with: + distribution: temurin + java-version: "17" + + - name: Install the latest release + run: | + iex (Get-Content -Raw src/site/resources/get.ps1) + $jarPath = Join-Path $env:LOCALAPPDATA 'ipmicli\ipmi-java-standalone.jar' + if (-not (Test-Path $jarPath)) { throw "The jar was not installed at $jarPath." } + $shimPath = Join-Path $env:LOCALAPPDATA 'ipmicli\bin\ipmicli.cmd' + if (-not (Test-Path $shimPath)) { throw "The ipmicli.cmd shim was not installed at $shimPath." } + $binDir = Join-Path $env:LOCALAPPDATA 'ipmicli\bin' + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + if (-not (($userPath -split ';') -contains $binDir)) { throw "$binDir was not added to the user PATH." } + + - name: Check the version and help output of the shim + run: | + $shim = Join-Path $env:LOCALAPPDATA 'ipmicli\bin\ipmicli.cmd' + $out = & $shim --version + if ($LASTEXITCODE -ne 0) { throw "ipmicli.cmd --version exited with $LASTEXITCODE." } + if ("$out" -ne "ipmi-java $env:VERSION") { throw "Expected 'ipmi-java $env:VERSION' but got '$out'." } + # The shim sets IPMICLI_PROGRAM_NAME so the usage names the ipmicli + # command instead of "java -jar ...". + $help = & $shim --help | ForEach-Object { "$_" } + if ($LASTEXITCODE -ne 0) { throw "ipmicli.cmd --help exited with $LASTEXITCODE." } + if (-not ($help | Where-Object { $_ -match '^ ipmicli -h probe' })) { throw 'The help output does not name the ipmicli launcher.' } + if (($help -join "`n") -match 'java -jar') { throw "The help of the installed shim mentions 'java -jar'." } + + - name: Honor IPMICLI_JAVA_HOME when no other JRE is findable + run: | + $shim = Join-Path $env:LOCALAPPDATA 'ipmicli\bin\ipmicli.cmd' + $realJavaHome = $env:JAVA_HOME + Remove-Item Env:JAVA_HOME -ErrorAction SilentlyContinue + $env:Path = "$env:SystemRoot\System32;$env:SystemRoot" + $ErrorActionPreference = 'Continue' + # Without any JRE hint the shim must fail with the Adoptium message... + $lines = & $shim --version 2>&1 | ForEach-Object { "$_" } + if ($LASTEXITCODE -ne 127) { throw "Expected exit code 127 without a JRE but got $LASTEXITCODE." } + if (($lines -join "`n") -notmatch 'adoptium\.net') { throw "The error message does not point to adoptium.net." } + # ...and must succeed through IPMICLI_JAVA_HOME alone. + $env:IPMICLI_JAVA_HOME = $realJavaHome + $out = & $shim --version + if ($LASTEXITCODE -ne 0) { throw "ipmicli.cmd exited with $LASTEXITCODE despite IPMICLI_JAVA_HOME." } + if ("$out" -ne "ipmi-java $env:VERSION") { throw "Expected 'ipmi-java $env:VERSION' through IPMICLI_JAVA_HOME but got '$out'." } + exit 0 + + - name: Reject a pre-8 JRE + run: | + # Compile a fake java.exe that reports version 1.7, the newest version + # the shim must reject. + $fakeHome = Join-Path $env:RUNNER_TEMP 'fake-java7' + New-Item -ItemType Directory -Force -Path (Join-Path $fakeHome 'bin') | Out-Null + $source = @' + using System; + class FakeJava { + static int Main(string[] args) { + Console.Error.WriteLine("java version \"1.7.0_80\""); + Console.Error.WriteLine("Java(TM) SE Runtime Environment (build 1.7.0_80-b15)"); + return 0; + } + } + '@ + $sourcePath = Join-Path $fakeHome 'FakeJava.cs' + Set-Content -Path $sourcePath -Value $source + & "$env:SystemRoot\Microsoft.NET\Framework64\v4.0.30319\csc.exe" /nologo /out:"$fakeHome\bin\java.exe" $sourcePath + if ($LASTEXITCODE -ne 0) { throw "Could not compile the fake java.exe." } + + $shim = Join-Path $env:LOCALAPPDATA 'ipmicli\bin\ipmicli.cmd' + Remove-Item Env:JAVA_HOME -ErrorAction SilentlyContinue + $env:Path = "$env:SystemRoot\System32;$env:SystemRoot" + $env:IPMICLI_JAVA_HOME = $fakeHome + $ErrorActionPreference = 'Continue' + $lines = & $shim --version 2>&1 | ForEach-Object { "$_" } + if ($LASTEXITCODE -ne 127) { throw "Expected exit code 127 with a Java 7 IPMICLI_JAVA_HOME but got $LASTEXITCODE." } + if (($lines -join "`n") -notmatch 'adoptium\.net') { throw "The error message does not point to adoptium.net." } + exit 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 189e1a9..5c8cd6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,3 +25,27 @@ jobs: autoRelease: false jdkVersion: "17" secrets: inherit + + # The shared release workflow uploads the versioned jars only. The version-less alias gives the installers of the + # documentation site (src/site/resources/get and get.ps1) a stable download URL + # (releases/latest/download/ipmi-java-standalone.jar), and the .sha256 files let them verify the download. + standalone-assets: + name: Upload the standalone jar alias and checksums + needs: release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Upload ipmi-java-standalone.jar and the SHA-256 checksums to v${{ inputs.releaseVersion }} + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_VERSION: ${{ inputs.releaseVersion }} + run: | + jar="ipmi-java-${RELEASE_VERSION}-standalone.jar" + gh release download "v${RELEASE_VERSION}" --pattern "${jar}" + cp "${jar}" ipmi-java-standalone.jar + sha256sum "${jar}" > "${jar}.sha256" + sha256sum ipmi-java-standalone.jar > ipmi-java-standalone.jar.sha256 + gh release upload "v${RELEASE_VERSION}" --clobber \ + ipmi-java-standalone.jar "${jar}.sha256" ipmi-java-standalone.jar.sha256 diff --git a/README.md b/README.md index b2a7cb6..c87281c 100644 --- a/README.md +++ b/README.md @@ -33,29 +33,36 @@ A client owns one RMCP+ session, opened by the first operation and closed by `cl ## Command-line client -Every build also produces a self-contained executable jar with a command-line client, `ipmicli`, that exposes the same operations ([download the latest](https://github.com/metricshub/ipmi-java/releases/latest), Java 8 or later, no other file needed): +The library comes with a command-line client, `ipmicli`, that exposes the same operations. Install it with one command (Java 8 or later required): -```text -target/ipmi-java-.jar -target/ipmi-java--standalone.jar +```shell +curl -fsSL https://metricshub.org/ipmi-java/get | sh ``` +On Windows (PowerShell): + +```powershell +irm https://metricshub.org/ipmi-java/get.ps1 | iex +``` + +See the [installation page](https://metricshub.org/ipmi-java/installation.html#command-line-client) for details. The client is a self-contained executable jar (`target/ipmi-java--standalone.jar`, also on the [releases page](https://github.com/metricshub/ipmi-java/releases/latest)), which `java -jar` runs as well. + What the BMC offers, before any login: ```bash -java -jar ipmi-java--standalone.jar -h bmc.example.com probe +ipmicli -h bmc.example.com probe ``` The sensors, with the password read from a file: ```bash -java -jar ipmi-java--standalone.jar -h bmc.example.com -u monitor -pf password.txt sensors +ipmicli -h bmc.example.com -u monitor -pf password.txt sensors ``` The FRU inventory as JSON Lines: ```bash -java -jar ipmi-java--standalone.jar -h bmc.example.com -u monitor -pf password.txt fru --json +ipmicli -h bmc.example.com -u monitor -pf password.txt fru --json ``` Subcommands: `probe`, `info` (the identity of the BMC), `chassis`, `power status|on|off|cycle|reset|soft|diag`, `identify`, `sensors`, `fru`, `sel` (the System Event Log, `--since ` for the new entries), `sol` (an interactive Serial over LAN console) and `raw` (any request, like `ipmitool raw`: `raw 0x2c 0x01 0xdc 0x01`). Diagnostics go to stderr, the exit code says what failed (77 authentication, 69 unreachable, 124 timeout...). See the [Command-Line Client](https://metricshub.org/ipmi-java/cli.html) page for the options, the output formats and the `ipmitool` equivalents. @@ -108,6 +115,8 @@ It is strongly recommended to only use [GitHub Actions "Release to Maven Central * Manually trigger the "Release to Maven Central" workflow * Specify the version being released and the next version number (SNAPSHOT) +* The workflow creates the GitHub release, then adds the `ipmi-java-standalone.jar` alias and the `.sha256` checksums + that the `ipmicli` installers download (if that last job fails, re-run it: the installers fail until it succeeds) * Publish the pending deployment on the [Central Portal](https://central.sonatype.com/publishing/deployments) (the workflow uploads and validates it, but does not publish it automatically) * Merge the PR that has been created to prepare the next version diff --git a/src/site/markdown/cli.md b/src/site/markdown/cli.md index 061d9ac..4c16c71 100644 --- a/src/site/markdown/cli.md +++ b/src/site/markdown/cli.md @@ -6,19 +6,31 @@ description: Manual of the ipmicli command-line client of the IPMI Java Client - Every release ships a self-contained executable jar that bundles the library and a command-line -client: download `${project.artifactId}-${project.version}-standalone.jar` from the -[latest release](https://github.com/metricshub/ipmi-java/releases/latest) and run it with Java 8 -or later. This page is its manual. +client, **`ipmicli`**. This page is its manual. Install it with one command (Java 8 or later is +required): + +> [!TABS] +> * Linux / macOS +> ```shell-session +> $ curl -fsSL https://metricshub.org/ipmi-java/get | sh +> ``` +> * Windows (PowerShell) +> ```powershell +> irm https://metricshub.org/ipmi-java/get.ps1 | iex +> ``` + +The installer verifies the checksum of the jar and puts an `ipmicli` launcher on the `PATH`; see +[Installation](installation.html#command-line-client) to inspect the script first, pin a version +or change the directories. ```text -java -jar ${project.artifactId}-${project.version}-standalone.jar [options] [subcommand options] +ipmicli [options] [subcommand options] ``` -The command is called **`ipmicli`** in this page and in its own usage. Give it that name with a -shell alias (`alias ipmicli='java -jar /path/to/${project.artifactId}-${project.version}-standalone.jar'`, -or a `doskey ipmicli=java -jar ... $*` macro on Windows) until the one-command installer -([#163](https://github.com/metricshub/ipmi-java/issues/163)) puts a launcher on the `PATH`. The -jar needs no other file: the library has no dependency. +Without the installer, download `${project.artifactId}-${project.version}-standalone.jar` from the +[latest release](https://github.com/metricshub/ipmi-java/releases/latest) and run it with +`java -jar ${project.artifactId}-${project.version}-standalone.jar` wherever this page says +`ipmicli`. The jar needs no other file: the library has no dependency. The CLI is a thin layer over the [`IpmiClient`](index.html#quick-start) API: every subcommand is one operation of the client, with the same defaults, the same @@ -481,6 +493,7 @@ The sysexits values of the MetricsHub command-line clients: | `74` | Local I/O failure (stdout closed). | | `77` | Authentication or authorization failure: unknown user, wrong password or BMC key, privilege level refused at login, insufficient privilege (`D4h`, with the `-L` hint). | | `124` | Timeout: the deadline of the subcommand expired, or a message of the session got no reply after all the tries. | +| `127` | The launcher of the [installer](installation.html#command-line-client) found no Java 8 or later (install one from [Adoptium](https://adoptium.net), or set `IPMICLI_JAVA_HOME`), or no jar (run the installer again). | The mapping follows the [exceptions](timeouts-and-errors.html#exceptions) of the client: `IpmiConnectionException` 69, `IpmiAuthenticationException` 77, `IpmiTimeoutException` 124, diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index b64cf82..b182e23 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -29,11 +29,12 @@ boards) over **IPMI 2.0 over LAN (RMCP+)**, on UDP port 623. It lets a Java appl OEM network functions included), or through the low-level connector ([Low-Level API](low-level-api.html)). -The same operations are available from the command line with the **`ipmicli`** client of the -standalone jar ([Command-Line Client](cli.html)): +The same operations are available from the command line with the **`ipmicli`** client +([Command-Line Client](cli.html)), installed with `curl -fsSL https://metricshub.org/ipmi-java/get | sh` +(`irm https://metricshub.org/ipmi-java/get.ps1 | iex` on Windows, see [Installation](installation.html#command-line-client)): ```text -java -jar ipmi-java-${project.version}-standalone.jar -h bmc.example.com -u monitor -pf pw.txt sensors +ipmicli -h bmc.example.com -u monitor -pf pw.txt sensors ``` The library is the IPMI engine of [MetricsHub](https://metricshub.com) hardware monitoring: its diff --git a/src/site/markdown/installation.md b/src/site/markdown/installation.md index ac7da78..f9b987a 100644 --- a/src/site/markdown/installation.md +++ b/src/site/markdown/installation.md @@ -1,5 +1,5 @@ -keywords: install, maven, gradle, dependency, jdk, java 8, no dependency, standalone jar, command line -description: Add the IPMI Java Client to your build, the supported JDKs and requirements, and the standalone jar of the command-line client; the library has no dependency and does not log. +keywords: install, maven, gradle, dependency, jdk, java 8, no dependency, standalone jar, command line, ipmicli, installer, curl, powershell +description: Add the IPMI Java Client to your build, the supported JDKs and requirements, and the one-command installation of the ipmicli command-line client; the library has no dependency and does not log. # Installation @@ -53,17 +53,70 @@ log what you need from there, with the logging framework of your application. ## Command-line client +Install the **`ipmicli`** command ([Command-Line Client](cli.html)) with one command: + +> [!TABS] +> * Linux / macOS +> ```shell-session +> $ curl -fsSL https://metricshub.org/ipmi-java/get | sh +> ``` +> * Windows (PowerShell) +> ```powershell +> irm https://metricshub.org/ipmi-java/get.ps1 | iex +> ``` + +The installer downloads the standalone jar of the latest release from GitHub, verifies its +SHA-256 checksum, and installs an `ipmicli` launcher, with no `sudo` or administrator rights: + +| | Linux / macOS | Windows | +| --- | --- | --- | +| Jar | `${XDG_DATA_HOME:-~/.local/share}/ipmicli/ipmi-java-standalone.jar` | `%LOCALAPPDATA%\ipmicli\ipmi-java-standalone.jar` | +| Launcher | `~/.local/bin/ipmicli` (the installer prints the line to add to your profile when the directory is not on the `PATH`) | `%LOCALAPPDATA%\ipmicli\bin\ipmicli.cmd` (the installer adds the directory to the user `PATH`: open a new terminal) | + +The launcher finds a Java runtime, 8 or later, each time it runs: `IPMICLI_JAVA_HOME`, then +`JAVA_HOME`, then `java` on the `PATH` (on Linux and macOS, then `/usr/libexec/java_home`, sdkman +and `/usr/lib/jvm`). Installing or upgrading Java needs no reinstallation. Without a suitable +runtime, it exits with code 127 and points to [Adoptium](https://adoptium.net). + +```shell-session +$ ipmicli --version +ipmi-java ${project.version} +``` + +Running the installer again upgrades in place. A checksum mismatch aborts the installation +before anything is replaced. These environment variables change what it does: + +| Variable | Effect | +| --- | --- | +| `IPMICLI_VERSION` | Install this release (`2.0.00` or later) instead of the latest. | +| `IPMICLI_INSTALL_DIR` | Linux and macOS: the directory of the launcher (default `~/.local/bin`). Windows: the installation directory (default `%LOCALAPPDATA%\ipmicli`). | + +```shell-session +$ curl -fsSL https://metricshub.org/ipmi-java/get | IPMICLI_VERSION=${project.version} sh +``` + +> [!TIP] +> Prefer to inspect the script before running it? Download it first: +> +> ```shell-session +> $ curl -fsSLO https://metricshub.org/ipmi-java/get +> $ less get +> $ sh get +> ``` + +### Manual download + Every release also publishes `${project.artifactId}-${project.version}-standalone.jar` on the -[releases page](https://github.com/metricshub/ipmi-java/releases/latest): the library with a -command-line client as its entry point, which runs on Java 8 or later with no other file. +[releases page](https://github.com/metricshub/ipmi-java/releases/latest), with its `.sha256` +checksum: the library with the command-line client as its entry point, which runs on Java 8 or +later with no other file. ```bash java -jar ${project.artifactId}-${project.version}-standalone.jar -h bmc.example.com -u monitor -pf pw.txt chassis ``` -See [Command-Line Client](cli.html). The same jar is published on Maven Central with the -`standalone` classifier; a build uses the regular artifact above, which has the same classes -without the entry point. +The same jar is published on Maven Central with the `standalone` classifier; a build uses the +regular artifact above, which has the same classes without the entry point. ## Building from source diff --git a/src/site/markdown/upgrading.md b/src/site/markdown/upgrading.md index 86f9824..f90e5d8 100644 --- a/src/site/markdown/upgrading.md +++ b/src/site/markdown/upgrading.md @@ -10,7 +10,8 @@ description: What changes when upgrading the IPMI Java Client — from 1.x to 2. Version 2.0.00 replaces the client API of 1.x with a fluent client, removes the SLF4J dependency, and simplifies the low-level commands. The protocol core and the decoders are the same. It also adds a command-line client, `ipmicli`, in a standalone jar -([Command-Line Client](cli.html)); `CipherSuite` gains `getAuthenticationAlgorithmCode()`, +([Command-Line Client](cli.html)), with one-command installers for Linux, macOS and Windows +([Installation](installation.html#command-line-client)); `CipherSuite` gains `getAuthenticationAlgorithmCode()`, `getIntegrityAlgorithmCode()` and `getConfidentialityAlgorithmCode()` for it. The semicolon-separated text output of 1.x (`IpmiResultConverter`) is MetricsHub's format, and moves to the [MetricsHub IPMI extension](https://github.com/MetricsHub/metricshub-community/issues/1391), where it stays byte-identical to 1.2.x for the same BMC data, except for the sensor and FRU names diff --git a/src/site/resources/get b/src/site/resources/get new file mode 100644 index 0000000..06b941f --- /dev/null +++ b/src/site/resources/get @@ -0,0 +1,179 @@ +#!/bin/sh +# ipmicli installer for Linux and macOS — https://metricshub.org/ipmi-java +# +# Copyright 2023 MetricsHub +# Distributed under the GNU Lesser General Public License, version 3 or +# later (LGPL-3.0-or-later); see . +# +# Usage: +# curl -fsSL https://metricshub.org/ipmi-java/get | sh +# +# Environment variables: +# IPMICLI_VERSION Pin a specific release (e.g. "2.0.00"); default: latest. +# IPMICLI_INSTALL_DIR Directory for the ipmicli launcher; default: ~/.local/bin. +# +# The installer downloads the standalone jar of the requested IPMI Java Client +# release from GitHub, verifies its SHA-256 checksum, and writes a small +# `ipmicli` launcher script that locates a JRE (Java 8 or later) at run time. +# The jar goes to $XDG_DATA_HOME/ipmicli (default: ~/.local/share/ipmicli). + +set -eu + +RELEASES_URL="https://github.com/metricshub/ipmi-java/releases" +VERSION="${IPMICLI_VERSION:-latest}" +BIN_DIR="${IPMICLI_INSTALL_DIR:-${HOME}/.local/bin}" +DATA_DIR="${XDG_DATA_HOME:-${HOME}/.local/share}/ipmicli" + +info() { printf '%s\n' "$*"; } +fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } + +if command -v curl >/dev/null 2>&1; then + fetch() { curl -fsSL -o "$2" "$1"; } +elif command -v wget >/dev/null 2>&1; then + fetch() { wget -qO "$2" "$1"; } +else + fail "Neither curl nor wget is available; install one of them and re-run." +fi + +# --- Resolve the download URL ----------------------------------------------- + +# Releases publish a version-less "ipmi-java-standalone.jar" alias, which +# makes "latest" resolvable without any API call. +if [ "${VERSION}" = "latest" ]; then + JAR_URL="${RELEASES_URL}/latest/download/ipmi-java-standalone.jar" +else + JAR_URL="${RELEASES_URL}/download/v${VERSION}/ipmi-java-${VERSION}-standalone.jar" +fi + +# --- Download the jar and verify its checksum -------------------------------- + +# Everything is downloaded and verified in a temporary directory first, so a +# failure leaves an existing installation untouched. +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "${TMP_DIR}"' EXIT +TMP_JAR="${TMP_DIR}/ipmi-java-standalone.jar" + +info "Downloading ${JAR_URL} ..." +fetch "${JAR_URL}" "${TMP_JAR}" \ + || fail "Download failed: check that you are online and that the release exists (the command-line client ships with 2.0.00 and later)." +fetch "${JAR_URL}.sha256" "${TMP_JAR}.sha256" \ + || fail "Could not download ${JAR_URL}.sha256 to verify the jar." + +EXPECTED_SUM="$(awk '{ print tolower($1); exit }' "${TMP_JAR}.sha256")" +if command -v sha256sum >/dev/null 2>&1; then + ACTUAL_SUM="$(sha256sum "${TMP_JAR}" | awk '{ print tolower($1) }')" +elif command -v shasum >/dev/null 2>&1; then + ACTUAL_SUM="$(shasum -a 256 "${TMP_JAR}" | awk '{ print tolower($1) }')" +else + fail "Neither sha256sum nor shasum is available to verify the jar; install one of them and re-run." +fi +[ "${ACTUAL_SUM}" = "${EXPECTED_SUM}" ] \ + || fail "Checksum mismatch: expected ${EXPECTED_SUM}, got ${ACTUAL_SUM}. Aborting." +info "Checksum verified." + +# --- Install the jar and the launcher ---------------------------------------- + +mkdir -p "${DATA_DIR}" "${BIN_DIR}" + +# Resolve the directories to absolute paths: the launcher must find the jar +# from any working directory, even when the overrides were given relative. +DATA_DIR="$(cd "${DATA_DIR}" && pwd)" +BIN_DIR="$(cd "${BIN_DIR}" && pwd)" +JAR_PATH="${DATA_DIR}/ipmi-java-standalone.jar" +LAUNCHER_PATH="${BIN_DIR}/ipmicli" + +mv "${TMP_JAR}" "${JAR_PATH}" + +# Serializes a value as a single-quoted shell literal, so paths containing +# $, backticks, or double quotes stay inert in generated or printed shell code. +shell_quote() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; } + +JAR_PATH_SQ="$(shell_quote "${JAR_PATH}")" + +cat > "${LAUNCHER_PATH}" <&2 + echo " curl -fsSL https://metricshub.org/ipmi-java/get | sh" >&2 + exit 127 +fi + +# A candidate qualifies if it is executable and reports version 8 or later +# ("1.8.x" in the legacy version scheme). +java_ok() { + [ -x "\$1" ] || return 1 + _v="\$("\$1" -version 2>&1 | awk -F '"' '/ version /{ print \$2; exit }')" + case "\${_v}" in + 1.[89]*) return 0 ;; + 1.*) return 1 ;; + [0-9]*) [ "\${_v%%.*}" -ge 8 ] 2>/dev/null ;; + *) return 1 ;; + esac +} + +JAVA="" +for _candidate in \\ + "\${IPMICLI_JAVA_HOME:+\${IPMICLI_JAVA_HOME}/bin/java}" \\ + "\${JAVA_HOME:+\${JAVA_HOME}/bin/java}" \\ + "\$(command -v java 2>/dev/null)"; do + if [ -n "\${_candidate}" ] && java_ok "\${_candidate}"; then + JAVA="\${_candidate}" + break + fi +done + +# macOS: ask the system locator for the best JDK. +if [ -z "\${JAVA}" ] && [ -x /usr/libexec/java_home ]; then + _home="\$(/usr/libexec/java_home 2>/dev/null)" \\ + && java_ok "\${_home}/bin/java" && JAVA="\${_home}/bin/java" +fi + +# Well-known installation roots (Linux distributions, sdkman). +if [ -z "\${JAVA}" ]; then + for _home in "\${HOME}/.sdkman/candidates/java/current" /usr/lib/jvm/*; do + if java_ok "\${_home}/bin/java"; then + JAVA="\${_home}/bin/java" + break + fi + done +fi + +if [ -z "\${JAVA}" ]; then + echo "ERROR: no suitable Java Runtime Environment found (ipmicli requires Java 8 or later)." >&2 + echo "Install one from https://adoptium.net and re-run, or set IPMICLI_JAVA_HOME." >&2 + exit 127 +fi + +exec "\${JAVA}" -jar "\${IPMICLI_JAR}" "\$@" +LAUNCHER +chmod +x "${LAUNCHER_PATH}" + +info "Installed the IPMI Java Client jar to ${JAR_PATH}" +info "Installed the ipmicli launcher to ${LAUNCHER_PATH}" + +# --- PATH check ---------------------------------------------------------------- + +case ":${PATH}:" in + *":${BIN_DIR}:"*) + info "Run 'ipmicli --help' to get started." + ;; + *) + info "" + info "NOTE: ${BIN_DIR} is not on your PATH. Add it with:" + info "" + info " export PATH=$(shell_quote "${BIN_DIR}"):\"\${PATH}\"" + info "" + info "and append that line to your shell profile (e.g. ~/.bashrc or ~/.zshrc)." + ;; +esac diff --git a/src/site/resources/get.ps1 b/src/site/resources/get.ps1 new file mode 100644 index 0000000..09b9456 --- /dev/null +++ b/src/site/resources/get.ps1 @@ -0,0 +1,163 @@ +# ipmicli installer for Windows — https://metricshub.org/ipmi-java +# +# Copyright 2023 MetricsHub +# Distributed under the GNU Lesser General Public License, version 3 or +# later (LGPL-3.0-or-later); see . +# +# Usage (PowerShell): +# irm https://metricshub.org/ipmi-java/get.ps1 | iex +# +# Environment variables: +# IPMICLI_VERSION Pin a specific release (e.g. "2.0.00"); default: latest. +# IPMICLI_INSTALL_DIR Installation directory; default: %LOCALAPPDATA%\ipmicli. +# +# The installer downloads the standalone jar of the requested IPMI Java Client +# release from GitHub, verifies its SHA-256 checksum, writes an ipmicli.cmd +# shim that locates a JRE (Java 8 or later) at run time, and adds the shim +# directory to the user PATH. + +# The advertised `irm ... | iex` invocation evaluates this script in the +# caller's scope: the whole body runs in a script block so preference and +# helper variables cannot leak into the user's session. +& { + +$ErrorActionPreference = 'Stop' + +$releasesUrl = 'https://github.com/metricshub/ipmi-java/releases' +$version = if ($env:IPMICLI_VERSION) { $env:IPMICLI_VERSION } else { 'latest' } +$installDir = if ($env:IPMICLI_INSTALL_DIR) { $env:IPMICLI_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'ipmicli' } + +# The user PATH entry must never depend on a working directory, so resolve +# relative, drive-relative (C:ipmicli), and root-relative (\ipmicli) overrides +# with GetFullPath, aligning the process working directory with the +# PowerShell location for the call (and restoring it right after). +$savedCwd = [Environment]::CurrentDirectory +[Environment]::CurrentDirectory = (Get-Location).ProviderPath +try { + $installDir = [System.IO.Path]::GetFullPath($installDir) +} finally { + [Environment]::CurrentDirectory = $savedCwd +} + +$binDir = Join-Path $installDir 'bin' +$jarPath = Join-Path $installDir 'ipmi-java-standalone.jar' +$shimPath = Join-Path $binDir 'ipmicli.cmd' + +# Releases publish a version-less "ipmi-java-standalone.jar" alias, which +# makes "latest" resolvable without any API call. +if ($version -eq 'latest') { + $jarUrl = "$releasesUrl/latest/download/ipmi-java-standalone.jar" +} else { + $jarUrl = "$releasesUrl/download/v$version/ipmi-java-$version-standalone.jar" +} + +# --- Download the jar and verify its checksum -------------------------------- + +# The jar is downloaded and verified in the temporary directory first, so a +# failure leaves an existing installation untouched. +$tmpJar = Join-Path ([System.IO.Path]::GetTempPath()) "ipmi-java-standalone-$PID.jar" +Write-Host "Downloading $jarUrl ..." +try { + Invoke-WebRequest -UseBasicParsing -Uri $jarUrl -OutFile $tmpJar +} catch { + throw "Download failed: check that you are online and that the release exists (the command-line client ships with 2.0.00 and later)." +} + +# Transient failures (Windows PowerShell can drop a reused connection) are +# retried before giving up. +$sumUrl = "$jarUrl.sha256" +for ($attempt = 1; $attempt -le 3; $attempt++) { + try { + $expectedSum = (Invoke-RestMethod -UseBasicParsing -Uri $sumUrl) -split '\s+' | Select-Object -First 1 + break + } catch { + if ($attempt -eq 3) { + Remove-Item -Force $tmpJar + throw "Could not download $sumUrl to verify the jar: $($_.Exception.Message)" + } + Start-Sleep -Seconds 1 + } +} +$actualSum = (Get-FileHash -Algorithm SHA256 -Path $tmpJar).Hash +if ($actualSum -ne $expectedSum) { + Remove-Item -Force $tmpJar + throw "Checksum mismatch: expected $expectedSum, got $actualSum. Aborting." +} +Write-Host 'Checksum verified.' + +# --- Install the jar and the shim ----------------------------------------------- + +New-Item -ItemType Directory -Force -Path $binDir | Out-Null +Move-Item -Force $tmpJar $jarPath + +# The shim is a literal here-string so cmd metacharacters (%%, $PATH) survive. +# It locates the jar relative to its own directory (%~dp0), so the content is +# pure ASCII regardless of the install path (non-ASCII user names included) +# and the installation can be relocated as a whole. +$shim = @' +@echo off +rem ipmicli launcher - https://metricshub.org/ipmi-java +rem Locates a JRE (Java 8 or later) and runs the IPMI Java Client standalone jar. +rem Set IPMICLI_JAVA_HOME to force a specific Java installation. + +setlocal +set "IPMICLI_JAR=%~dp0..\ipmi-java-standalone.jar" + +rem Pass the launcher's own invocation name so the CLI's help names the +rem command the user actually typed instead of "java -jar ...". +set "IPMICLI_PROGRAM_NAME=%~n0" + +if not exist "%IPMICLI_JAR%" ( + echo ERROR: %IPMICLI_JAR% not found; re-run the installer: 1>&2 + echo irm https://metricshub.org/ipmi-java/get.ps1 ^| iex 1>&2 + exit /b 127 +) + +set "IPMICLI_JAVA=" +if defined IPMICLI_JAVA_HOME call :try_java "%IPMICLI_JAVA_HOME%\bin\java.exe" +if not defined IPMICLI_JAVA if defined JAVA_HOME call :try_java "%JAVA_HOME%\bin\java.exe" +if not defined IPMICLI_JAVA for %%J in (java.exe) do if not "%%~$PATH:J" == "" call :try_java "%%~$PATH:J" + +if not defined IPMICLI_JAVA ( + echo ERROR: no suitable Java Runtime Environment found ^(ipmicli requires Java 8 or later^). 1>&2 + echo Install one from https://adoptium.net, or set IPMICLI_JAVA_HOME. 1>&2 + exit /b 127 +) + +"%IPMICLI_JAVA%" -jar "%IPMICLI_JAR%" %* +exit /b %ERRORLEVEL% + +rem A candidate qualifies if it runs and does not report a 1.0-1.7 version +rem ("1.8" is Java 8 in the legacy version scheme). The findstr result is +rem read through errorlevel in a separate statement: an exit /b appended to +rem the pipeline would run in the pipe's child cmd, not in this subroutine. +:try_java +if not exist "%~1" exit /b 0 +"%~1" -version >nul 2>&1 || exit /b 0 +"%~1" -version 2>&1 | findstr /r /c:"version .1\.[0-7]\." >nul +if not errorlevel 1 exit /b 0 +set "IPMICLI_JAVA=%~1" +exit /b 0 +'@ +# cmd only finds "call :label" targets in CRLF files, and the here-string +# carries whatever line endings this script was downloaded with: normalize. +$shim = ($shim -replace "`r`n", "`n") -replace "`n", "`r`n" +$shim | Set-Content -Path $shimPath -Encoding ASCII + +Write-Host "Installed the IPMI Java Client jar to $jarPath" +Write-Host "Installed the ipmicli launcher to $shimPath" + +# --- PATH registration ----------------------------------------------------------- + +$userPath = [Environment]::GetEnvironmentVariable('Path', 'User') +$onPath = ($userPath -split ';' | Where-Object { $_ -eq $binDir }).Count -gt 0 +if (-not $onPath) { + $newPath = if ($userPath) { "$userPath;$binDir" } else { $binDir } + [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') + Write-Host "Added $binDir to your user PATH." + Write-Host "Open a new terminal, then run 'ipmicli --help' to get started." +} else { + Write-Host "Run 'ipmicli --help' to get started." +} + +}