How to migrate PMM (Grafana) users

pmm-to-pmm-migration

You’ve got a shiny new Percona Monitoring & Management instance standing by and want to move existing users over. This blog is a quick work around for migrating PMM users to another PMM (Grafana) instance “manually”.

Note: This blog is only about user migration trick. A tool for exporting performance metrics and QAN data is pmm_dump. If you’re looking for a complete migration from PMM 2.x to 3.x, you have a guide here.

PMM User Migration

The requirement here was to migrate only PMM users to a new environment but not the data or queries.

All PMM users are actually Grafana users under the hood, stored in a Postgres DB (by default). So it is possible to get those credentials and move them to the new instance without doing much is what I think!

Verify what’s the Grafana’s backend database

# Inside PMM container
[root@1167d2c03dc2 ~] # grep type  /etc/grafana/grafana.ini
# You can configure the database connection by specifying type, host, name, user and password
type = postgres
[root@1167d2c03dc2 ~] #

Connect to database and review users

psql -h localhost -p 5432 -U grafana -d grafana

SELECT
  ou.id AS org_user_id,
  ou.org_id,
  ou.role,
  u.id AS user_id,
  u.login,
  u.email,
  u.name,
  u.is_admin,
  u.salt,
  u.password
FROM
  org_user ou
JOIN
  "user" u ON ou.user_id = u.id
ORDER BY ou.id;
grafana=> SELECT ou.id AS org_user_id, ou.org_id, ou.role, u.id AS user_id, u.login, u.email, u.name, u.is_admin,u.salt, u.password FROM org_user ou JOIN "user" u ON ou.user_id = u.id ORDER BY ou.id;
 org_user_id | org_id |  role  | user_id | login |      email      | name  | is_admin |    salt    |                                               p
assword
-------------+--------+--------+---------+-------+-----------------+-------+----------+------------+------------------------------------------------
------------------------------------------------------
           1 |      1 | Admin  |       1 | admin | admin@localhost |       | t        | k4XvdKrvBv | ba4af563a729c06fc95b1c921a598b6de2
           2 |      1 | Viewer |       2 | kedar | kedar           | kedar | f        | k4XvdKrvBv | 5b1c921a598b6de2c55d906e900db04c82f

One may think that we can copy username and password hash but it is not that simple. We also need to add a pinch of NaCl AKA Salt.

In password authentication, “salt” is a unique random string of characters added to a password before it’s hashed to provide additional security. Thus we have to make sure to copy the “salt” values.

The Magic happens here

grafana=>  \x auto
Expanded display is used automatically.
grafana=> SELECT
  'INSERT INTO "user" (id, version, email, name, login, password, salt, email_verified, theme, created, updated, is_admin, is_disabled, last_seen_at, org_id) VALUES (' ||
  u.id || ', ' || u.version || ', ''' || REPLACE(u.email, '''', '''''') || ''', ''' || REPLACE(u.name, '''', '''''') || ''', ''' || REPLACE(u.login, '''', '''''') || ''', ''' || u.password || ''', ''' || u.salt || ''', ' || u.email_verified || ', ''' || u.theme || ''', ''' || u.created || ''', ''' || u.updated || ''', ' || u.is_admin || ', ' || u.is_disabled || ', ''' || u.last_seen_at || ''', ' || u.org_id || ');' AS insert_user_command,
  CASE
    WHEN ou.user_id IS NOT NULL THEN
      'INSERT INTO org_user (id, org_id, user_id, role, created, updated) VALUES (' ||
      ou.id || ', ' || ou.org_id || ', ' || ou.user_id || ', ''' || ou.role || ''', ''' || ou.created || ''', ''' || ou.updated || ''');'
    ELSE
      '-- User ' || u.login || ' has no org_user entry to migrate.'
  END AS insert_org_user_command
FROM "user" u
LEFT JOIN org_user ou ON u.id = ou.user_id
ORDER BY u.id, ou.org_id;
-[ RECORD 1 ]-----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
insert_user_command     | INSERT INTO "user" (id, version, email, name, login, password, salt, email_verified, theme, created, updated, is_admin, is_disabled, last_seen_at, org_id) VALUES (1, 0, 'admin@localhost', '', 'admin', '65c0a138b91f2793ec9b545352355a7b658dkedar.nitty-witty.com0a03b736918ba4af563a729c06fc9d906e900db04c82f', 'k4XvdKrvBv', false, '', '2025-01-01 11:14:30', '2025-07-30 08:16:29', true, false, '2025-07-30 08:38:40', 1);
insert_org_user_command | INSERT INTO org_user (id, org_id, user_id, role, created, updated) VALUES (1, 1, 1, 'Admin', '2025-01-01 11:14:30', '2025-01-01 11:14:30');
-[ RECORD 2 ]-----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
insert_user_command     | INSERT INTO "user" (id, version, email, name, login, password, salt, email_verified, theme, created, updated, is_admin, is_disabled, last_seen_at, org_id) VALUES (2, 0, 'kedar', 'kedar', 'kedar', '2e2bc60603c77811b4fe141271bc49aecekedar.nitty-witty.comdb0b11d1f921a0f19c7777dae68ebeb2d2f8bce643911', 'lwcv2cuO70', false, '', '2025-07-30 08:18:53', '2025-07-30 08:18:53', false, false, '2025-07-30 08:28:24', 1);
insert_org_user_command | INSERT INTO org_user (id, org_id, user_id, role, created, updated) VALUES (2, 1, 2, 'Viewer', '2025-07-30 08:18:53', '2025-07-30 08:18:53');
-[ RECORD 3 ]-----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
insert_user_command     | INSERT INTO "user" (id, version, email, name, login, password, salt, email_verified, theme, created, updated, is_admin, is_disabled, last_seen_at, org_id) VALUES (5, 0, 'aarya', 'aarya', 'aarya', '1a9ecc6b962db8028fe0a8ac1319208a1d29110c1017f2a79908f78a111fb86067c3b051ae3af9dde07a09cd49080c7c5d8b', 'N2nFw762t3', false, '', '2025-07-30 08:30:11', '2025-07-30 08:30:11', false, false, '2015-06-30 08:30:11', 1);
insert_org_user_command | INSERT INTO org_user (id, org_id, user_id, role, created, updated) VALUES (5, 1, 5, 'Viewer', '2025-07-30 08:30:11', '2025-07-30 08:30:11');
-[ RECORD 4 ]-----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
insert_user_command     | INSERT INTO "user" (id, version, email, name, login, password, salt, email_verified, theme, created, updated, is_admin, is_disabled, last_seen_at, org_id) VALUES (6, 0, 'rudra', 'rudra', 'rudra', '99752b5c4ca5b20a049d193551d566105f5ekedar.nitty-witty.com2491ff058f1e8b808d8bd9a555687655118c0081ba7', 'nLOfMsnE4m', false, '', '2025-07-30 08:30:29', '2025-07-30 08:30:29', false, false, '2015-06-30 08:30:29', 1);
insert_org_user_command | INSERT INTO org_user (id, org_id, user_id, role, created, updated) VALUES (6, 1, 6, 'Viewer', '2025-07-30 08:30:29', '2025-07-30 08:30:29');

I believe you’ve understood the next step! Connect to the target PMM, execute these insert statements and you’re done.

Hope someone somewhere finds this post helpful.

Exit mobile version