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.

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like

Stored procedure to Find database objects

This procedure lists available database objects under passed database name. It lists present Tables, Views, Stored Procedures, Functions and Triggers under particular database. It also lists storage engine of tables.…
View Post