Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c7d1ce7
Add one-step PMM client installer script and UI integration
theTibi Apr 14, 2026
20d0717
Refactor access control and actions API files
theTibi Apr 14, 2026
690c7de
Refactor createNodeInstallToken function in installToken.ts
theTibi Apr 15, 2026
53b918d
Update PMM client installation script and UI components
theTibi Apr 16, 2026
3f3124b
Refactor OS detection in install-pmm-client.sh
theTibi Apr 16, 2026
a77c25a
Enhance Install Client Page and Command Generation
theTibi Apr 16, 2026
c7c3f15
Add PMM agent runtime configuration and startup logic
theTibi Apr 29, 2026
3abcf8b
Enhance Install Token Management and UI Experience
theTibi Apr 29, 2026
7be45bd
Enhance hostname detection in install-pmm-client.sh
theTibi Apr 29, 2026
d435861
Enhance Node Install Token Management and Testing
theTibi Apr 29, 2026
242bb80
Update PMM Client Installation Documentation and Token Management
theTibi Apr 29, 2026
18a233e
Enhance PMM Client Installation Script and UI for Prompt Mode
theTibi May 4, 2026
4a8af16
Update PMM Client Installation Documentation and UI for Credentials M…
theTibi May 4, 2026
1cd8fc7
feat: add 'Preview' badge on navigation
mattiasimonato May 6, 2026
cd0f425
chore: regenerate the code
ademidoff May 6, 2026
83397cf
refactor: remove CreateNodeInstallTokenRequest and CreateNodeInstallT…
theTibi May 12, 2026
cedefd6
Merge branch 'v3' into one_step_install
theTibi May 13, 2026
2d5a83b
PMM-14993 Optimize post-build.yml
ademidoff May 14, 2026
b866025
PMM-14993 Restore a generated file
ademidoff May 14, 2026
e66aa83
Merge branch 'v3' into one_step_install
mattiasimonato May 14, 2026
4408ba3
PMM-14993 Restore client.go
ademidoff May 14, 2026
0c90ca8
Merge branch 'one_step_install' of ssh://github.com/percona/pmm into …
ademidoff May 14, 2026
ace5dff
PMM-14993 Leave a comment for the static folder
ademidoff May 14, 2026
5ee0ae6
PMM-14993 Consistent use of single square brackets
ademidoff May 14, 2026
0432a70
PMM-14993 Fix permissions on the script
ademidoff May 15, 2026
36374c6
Merge branch 'v3' into one_step_install
ademidoff May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
680 changes: 680 additions & 0 deletions build/ansible/roles/nginx/files/install-pmm-client.sh

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions build/ansible/roles/nginx/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,11 @@
group: root
owner: pmm
mode: 0644

- name: Copy one-step PMM client installer script
copy:
src: install-pmm-client.sh
dest: /usr/share/pmm-server/static/install-pmm-client.sh
group: root
owner: pmm
mode: 0755
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`.
1 change: 1 addition & 0 deletions documentation/mkdocs-base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ nav:
- Install PMM Client:
- Client installation overview: install-pmm/install-pmm-client/index.md
- install-pmm/install-pmm-client/prerequisites.md
- One-step install from UI: install-pmm/install-pmm-client/one-step-ui-install.md
- Deployment options:
- Install with Package Manager: install-pmm/install-pmm-client/package_manager.md
- Install from binaries: install-pmm/install-pmm-client/binary_package.md
Expand Down
111 changes: 111 additions & 0 deletions ui/apps/pmm/src/api/installToken.ts
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(
Copy link
Copy Markdown
Member

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/serviceaccounts as 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.

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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each Generate token click creates a new token on the shared per-tech SA (pmm-install-sa-mysql etc.). Tokens auto-expire in Grafana, but they stay in the SA's token list until manually deleted. After heavy use the SA's tokens table grows unbounded.

Options, in increasing order of effort:

  • Delete expired tokens for this SA right before minting the new one (GET /api/serviceaccounts/{id}/tokens → filter → DELETE …/tokens/{id}).
  • A scheduled cleanup elsewhere.
  • At minimum, a comment here pointing operators at where to clean up if it ever becomes a problem.

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;
}
11 changes: 11 additions & 0 deletions ui/apps/pmm/src/contexts/navigation/navigation.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,17 @@ export const NAV_INVENTORY: NavItem = {
url: `${PMM_NEW_NAV_GRAFANA_PATH}/inventory/nodes`,
matches: ['*'],
},
{
id: 'install-pmm-client',
text: 'Install PMM Client',
url: `${PMM_NEW_NAV_PATH}/install-client`,
matches: ['*'],
badge: {
label: 'Preview',
color: 'default',
variant: 'filled',
},
},
],
};

Expand Down
Loading
Loading