-
Notifications
You must be signed in to change notification settings - Fork 214
PMM-14993 Add one-step PMM client installer script and UI integration #5250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v3
Are you sure you want to change the base?
Changes from all commits
c7d1ce7
20d0717
690c7de
53b918d
3f3124b
a77c25a
c7c3f15
3abcf8b
7be45bd
d435861
242bb80
18a233e
4a8af16
1cd8fc7
cd0f425
83397cf
cedefd6
2d5a83b
b866025
e66aa83
4408ba3
0c90ca8
ace5dff
5ee0ae6
0432a70
36374c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # One-step PMM Client install from UI | ||
|
|
||
| Use the **Install PMM Client** wizard to generate a single command that installs `pmm-client`, registers the node with PMM Server, and adds one monitored service. | ||
|
|
||
| ## Before you start | ||
|
|
||
| - PMM Server must be reachable from the target node (default port `443`; whatever you set in **PMM host** is used in `PMM_SERVER_URL`). | ||
| - The node user running the command needs `sudo` access (or run it as `root`, e.g. inside a container). | ||
| - A short-lived service token is minted from the UI on demand — you do not need to provision one beforehand. The Grafana **Install PMM Client** service account is **Admin** org role and **expires 15 minutes after generation**; treat the URL like a password. | ||
|
|
||
| ## Generate the command | ||
|
|
||
| 1. In PMM UI, open **Inventory → Install PMM Client**. | ||
| 2. Choose technology: `MySQL`, `PostgreSQL`, `MongoDB`, or `Valkey`. | ||
| 3. Click **Generate short-lived token**. The countdown chip shows the remaining lifetime. | ||
| 4. Select the credentials mode: | ||
| - **Prompt on node (downloads script first, asks for DB user/password)** (**default**): the wizard renders a **two-step** command — `curl … -o /tmp/install-pmm-client.sh '<url>'` followed by `sudo -E bash /tmp/install-pmm-client.sh …`. Reading the script from disk (instead of piping it from `curl`) keeps stdin attached to your terminal, so the script can prompt you for the DB user and password. **`sudo -E`** preserves your environment into the root shell: if `DB_USER` / `DB_PASSWORD` (or per-tech `MYSQL_*`, `POSTGRESQL_*`, …) are already exported, the script uses them and **does not prompt**. Use this when you do not want credentials in the copied command line or process list from flags alone. | ||
| - **Include env variables (recommended for `curl | bash`)**: credentials are passed in the environment of the spawned shell. Use this when you want the classic one-liner pipeline. | ||
| - **Pass as script flags**: credentials are passed as `--db-*` script arguments instead of env vars. Same security profile as env mode, just a different surface. | ||
| 5. Fill in the optional fields you need (node name/address, DB host/port, service name, MongoDB auth DB, PostgreSQL database). | ||
| 6. Copy the generated command and run it on the target node before the token expires. | ||
|
|
||
| Example (env mode, matches what the wizard renders): | ||
|
|
||
| ```bash | ||
| curl -fsSLk 'https://<pmm-host>/pmm-static/install-pmm-client.sh' | sudo -E env \ | ||
| PMM_SERVER_URL='https://service_token:<TOKEN>@<pmm-host>' \ | ||
| TECH='mysql' \ | ||
| DB_USER='pmm' \ | ||
| DB_PASSWORD='secret' \ | ||
| bash -s -- \ | ||
| --pmm-server-insecure-tls | ||
| ``` | ||
|
|
||
| Example (prompt mode — credentials never appear in the rendered command): | ||
|
|
||
| ```bash | ||
| curl -fsSLk -o '/tmp/install-pmm-client.sh' 'https://<pmm-host>/pmm-static/install-pmm-client.sh' | ||
| sudo -E bash '/tmp/install-pmm-client.sh' \ | ||
| --pmm-server-url 'https://service_token:<TOKEN>@<pmm-host>' \ | ||
| --tech 'mysql' \ | ||
| --pmm-server-insecure-tls | ||
| ``` | ||
|
|
||
| The second line runs `bash` against a file (not against a pipe), so `sudo` keeps stdin connected to your terminal. The script then asks twice — once for the DB user, once for the DB password (silent input) — before running `pmm-admin add`. Optional fields you fill in the wizard (host, port, service name, MongoDB auth DB, PostgreSQL database) are still passed as `--db-*` flags so you only have to type two things. | ||
|
|
||
| Notes on the rendered command: | ||
|
|
||
| - `curl -fsSLk` (with `-k`) is emitted only when **Use insecure TLS** is on; with a properly signed PMM Server certificate the wizard drops the `-k`. | ||
| - TLS-skip on the PMM Server side is controlled by the `--pmm-server-insecure-tls` script flag (passed after `bash -s --`, or as an argument to `bash <path>` in prompt mode). The script also accepts `PMM_SERVER_INSECURE_TLS=1` as an env var if you build the command by hand. | ||
| - `sudo -E env VAR=... bash -s --` is the standard shape for env/flags modes; `-E` preserves your shell's exports while the explicit `VAR=...` list gets handed to `bash`'s environment (and therefore to the script). | ||
| - Prompt mode uses `sudo -E bash <path> ...` instead of `sudo -E env … bash -s --`: there is no inline env block in the copied command, but `-E` still forwards your shell exports (e.g. `DB_USER` / `DB_PASSWORD`) so credentials can be supplied without prompts or appearing in the command string. Stdin stays on your TTY the same way as plain `sudo bash`. | ||
|
|
||
| ## What the script does | ||
|
|
||
| The script available at `/pmm-static/install-pmm-client.sh` performs: | ||
|
|
||
| 1. Installs `pmm-client` using the OS package manager (RHEL-compatible or Debian-compatible hosts). | ||
| 2. Ensures `pmm-agent` is running (starts it via `systemd` when available, otherwise `nohup` in the background). | ||
| 3. Runs `pmm-admin config` against your PMM server to register the node and persist the agent identity. | ||
| 4. Runs `pmm-admin add <technology>` using your selected options. | ||
|
|
||
| ## Security notes | ||
|
|
||
| - Generated tokens are tied to Grafana service accounts minted as **Admin** org role and live for **15 minutes** — generate, run, done. There is no way to extend the lifetime from the UI. | ||
| - Env mode and flags mode put credentials into the shell command line and the spawned process environment. On a shared node, that may be visible in `ps`/`/proc` to other users for a moment. **Prompt mode** avoids this entirely: the rendered command never contains the DB user or password, and the script reads them straight from your terminal once it is running on the node. | ||
| - Avoid copy-pasting the command into chat/issue trackers; the embedded service token is a credential. (In prompt mode the DB credentials are not in the command, but the PMM service token still is.) | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| - **curl / browser returns `404`** on URLs like `/graph/…` — PMM Web UI lives under **`/pmm-ui/`**. Use paths such as `/pmm-ui/graph/inventory/nodes`, not `/graph/inventory/nodes`. This matches what the browser loads (see address bar vs. truncated copy-pastes). | ||
|
|
||
| - **TLS handshake errors against PMM Server** — turn on **Use insecure TLS** in the wizard (sets the `--pmm-server-insecure-tls` script flag). The wizard also adds `-k` to `curl` so the script download itself succeeds. | ||
| - **Package install fails** — verify outbound access to the Percona repositories (`repo.percona.com`). | ||
| - **`pmm-admin add` fails (auth, name conflict, etc.)** — the node was already registered by `pmm-admin config`. Re-run with **Force re-register node** enabled (this passes `--force` to `pmm-admin config`, which removes the previous node and its services on the server before re-registering). You may also have to fix the database credentials before retrying. | ||
| - **`pmm-agent is not running`** — happens in containers without `systemd`. The script auto-starts it via `nohup` and writes logs to `/var/log/pmm-agent.log`; check there. | ||
| - **`hostname: command not found`** — only on extremely minimal images; the script falls back to `$HOSTNAME`/`uname -n`/`/etc/hostname` and finally `node`. | ||
| - **Prompt mode does not actually prompt** — the script's noninteractive guard fires when stdin is not a TTY, e.g. when the prompt-mode command is invoked through `ssh host '<paste>'` (no allocated TTY) or through automation. Run it from an interactive shell on the node, or use **Include env variables** / **Pass as script flags** mode instead. | ||
| - **Cleanup after prompt mode** — the downloaded script lives at `/tmp/install-pmm-client.sh` after a successful install. It is harmless (no embedded secrets), but if you want it gone: `rm -f /tmp/install-pmm-client.sh`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import { grafanaApi } from './api'; | ||
|
|
||
| export interface CreateNodeInstallTokenResponse { | ||
| token: string; | ||
| expiresAt: string; | ||
| } | ||
|
|
||
| // Hard cap mirrors the previous server-side cap (15 min). Tokens longer than this | ||
| // shouldn't be in someone's terminal scrollback — re-run "Generate token" instead. | ||
| const MAX_TTL_SECONDS = 15 * 60; | ||
| const DEFAULT_TTL_SECONDS = 15 * 60; | ||
|
|
||
| const SUPPORTED_TECHNOLOGIES = new Set([ | ||
| 'mysql', | ||
| 'postgresql', | ||
| 'mongodb', | ||
| 'valkey', | ||
| ]); | ||
|
|
||
| // Shared SA per technology, created lazily on first use. Same naming scheme the | ||
| // removed backend endpoint used so previously-minted SAs are still reusable. | ||
| const SA_NAME_PREFIX = 'pmm-install-sa'; | ||
| const TOKEN_NAME_PREFIX = 'pmm-install-st'; | ||
|
|
||
| interface GrafanaServiceAccount { | ||
| id: number; | ||
| name: string; | ||
| } | ||
|
|
||
| interface GrafanaServiceAccountSearch { | ||
| totalCount: number; | ||
| serviceAccounts: GrafanaServiceAccount[]; | ||
| } | ||
|
|
||
| interface GrafanaTokenResponse { | ||
| id: number; | ||
| name: string; | ||
| key: string; | ||
| } | ||
|
|
||
| /** | ||
| * Mints a short-lived Grafana service-account token for a PMM Client install command. | ||
| * | ||
| * Implementation note: this calls Grafana's serviceaccounts API directly through the | ||
| * `/graph/api/` reverse proxy. The user must already be authenticated as a Grafana | ||
| * Admin (Grafana rejects the create/mint requests with 403 otherwise) — that's the | ||
| * same trust boundary the old backend endpoint had, just one hop shorter. | ||
| */ | ||
| export async function createNodeInstallToken( | ||
| technology: string, | ||
| ttlSeconds = 0 | ||
| ): Promise<CreateNodeInstallTokenResponse> { | ||
| if (!SUPPORTED_TECHNOLOGIES.has(technology)) { | ||
| throw new Error(`unsupported technology "${technology}"`); | ||
| } | ||
|
|
||
| let ttl = ttlSeconds > 0 ? ttlSeconds : DEFAULT_TTL_SECONDS; | ||
| if (ttl > MAX_TTL_SECONDS) { | ||
| ttl = MAX_TTL_SECONDS; | ||
| } | ||
|
|
||
| const saName = `${SA_NAME_PREFIX}-${technology}`; | ||
|
|
||
| let saId = await findServiceAccountIdByName(saName); | ||
| if (saId === null) { | ||
| saId = await createServiceAccount(saName); | ||
| } | ||
|
|
||
| // UUID-suffixed token name keeps concurrent calls from colliding on Grafana's | ||
| // per-SA unique-name constraint (Grafana returns 409 otherwise). | ||
| const tokenName = `${TOKEN_NAME_PREFIX}-${technology}-${crypto.randomUUID()}`; | ||
| const key = await mintToken(saId, tokenName, ttl); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Each Options, in increasing order of effort:
None of these are blockers, but the unbounded growth will surface eventually. |
||
|
|
||
| return { | ||
| token: key, | ||
| expiresAt: new Date(Date.now() + ttl * 1000).toISOString(), | ||
| }; | ||
| } | ||
|
|
||
| async function findServiceAccountIdByName(name: string): Promise<number | null> { | ||
| const res = await grafanaApi.get<GrafanaServiceAccountSearch>( | ||
| '/serviceaccounts/search', | ||
| { params: { query: name } } | ||
| ); | ||
| const match = res.data.serviceAccounts?.find((sa) => sa.name === name); | ||
| return match ? match.id : null; | ||
| } | ||
|
|
||
| async function createServiceAccount(name: string): Promise<number> { | ||
| // Admin role is required for `pmm-admin config`/inventory writes in real PMM setups. | ||
| const res = await grafanaApi.post<GrafanaServiceAccount>('/serviceaccounts', { | ||
| name, | ||
| role: 'Admin', | ||
| isDisabled: false, | ||
| }); | ||
| return res.data.id; | ||
| } | ||
|
|
||
| async function mintToken( | ||
| serviceAccountId: number, | ||
| tokenName: string, | ||
| ttlSeconds: number | ||
| ): Promise<string> { | ||
| // Only `name` + `secondsToLive` — extra fields (`role`) have been observed to | ||
| // make some Grafana versions ignore `secondsToLive` and fall back to a long default. | ||
| const res = await grafanaApi.post<GrafanaTokenResponse>( | ||
| `/serviceaccounts/${serviceAccountId}/tokens`, | ||
| { name: tokenName, secondsToLive: ttlSeconds } | ||
| ); | ||
| return res.data.key; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc comment says Grafana rejects with 403 for non-Admin callers — worth a quick manual confirmation that hitting
/api/serviceaccountsas a Viewer or Editor really returns 403. The entire security model of this page rests on that check, so it's the one thing worth verifying explicitly rather than trusting by inheritance from the old backend endpoint.