Include grafana for PM dashboards 74/14374/1
authorRavi Pendurty <ravi.pendurty@highstreet-technologies.com>
Mon, 5 May 2025 12:38:04 +0000 (18:08 +0530)
committerRavi Pendurty <ravi.pendurty@highstreet-technologies.com>
Mon, 5 May 2025 12:39:24 +0000 (18:09 +0530)
Integrate grafana with keycloak

Issue-ID: OAM-453
Change-Id: I0559cc7caf26c23c6c9bac748d6d1dfa5e678996
Signed-off-by: Ravi Pendurty <ravi.pendurty@highstreet-technologies.com>
solution/smo/common/identity/authentication.json
solution/smo/common/identity/config.py
solution/smo/common/identity/o-ran-sc-realm.json
solution/smo/oam/pm/config/pmpr/token-cache/jwt.txt
solution/smo/oam/pm/docker-compose-grafana.yaml [new file with mode: 0644]
solution/smo/oam/pm/setup.sh
solution/smo/oam/pm/tear-down.sh

index 7797110..9ff99b6 100644 (file)
             ],
             "requiredActions": [
                 "UPDATE_PASSWORD"
-            ]
+            ],
+            "clientRoles" : {
+                "grafana-ui.app" : [ "grafanaadmin" ]
+            }
         },
         {
             "firstName": "Luke",
             ],
             "requiredActions": [
                 "UPDATE_PASSWORD"
-            ]
+            ],
+            "clientRoles" : {
+                "grafana-ui.app" : [ "editor" ]
+            }
         },
         {
             "firstName": "Jargo",
             ],
             "requiredActions": [
                 "UPDATE_PASSWORD"
-            ]
+            ],
+            "clientRoles" : {
+                "grafana-ui.app" : [ "viewer" ]
+            }
         },
         {
             "firstName": "Martin",
             ],
             "requiredActions": [
                 "UPDATE_PASSWORD"
-            ]
+            ],
+            "clientRoles" : {
+                "grafana-ui.app" : [ "grafanaadmin" ]
+            }
         }
     ],
     "grants": [
index 37967e1..6e2207a 100644 (file)
@@ -198,6 +198,9 @@ def createUser(token, realmConfig, user):
 
     if response.status_code >= 200 and response.status_code < 300:
         print('User', user['username'], 'created!')
+        for client, roles in user.get("clientRoles", {}).items():
+            #print(f"Adding Client Role(s): {roles} from Client: {client} to User: {user['username']}")
+            assignClientRoleToUser(token, realmId, user['username'], client, roles)
     else:
         print('User creation', user['username'], 'failed!\n', response.text)
 
@@ -207,7 +210,7 @@ def createUsers(token, realmConfig, authConfig):
     for user in authConfig['users']:
         createUser(token, realmConfig, user)
 
-    # create a user based on system user
+    # create a user based on unix system user
     systemUser = {
         "firstName": getpass.getuser(),
         "lastName": "",
@@ -244,6 +247,114 @@ def addUserRole(user: dict, role: list, options: dict):
         # catastrophic error. bail.
         raise SystemExit(e)
 
+# Assigns a Client Role to User
+def assignClientRoleToUser(token, realmId, userName, clientName, roles):
+    url =  base + '/admin/realms/' + realmId + '/users'
+    auth = 'bearer ' + token
+    headers = {
+        'content-type': 'application/json',
+        'accept': 'application/json',
+        'authorization': auth
+    }
+    # 1. Query userId by userName
+    userId = getUserIdbyName(token, realmId, userName)
+    # 2. Query clientId by clientName
+    clientId = getClientIdByName(token, realmId, clientName)
+    # 3. Iterate through roles and Query roleId for each role as follows - https://identity.smo.o-ran-sc.org/admin/realms/onap/clients/5543ba76-c3bc-42e8-ae79-63a82d6dc2ee/roles?search=grafanaadmin
+    for roleName in roles:
+        roleId = getRoleIdByName(token, realmId, clientId, roleName)
+        # 4. Form a JSON body as - {"id": <roleId>, "name": <roleName>}
+        data = [
+            {
+                "id": roleId,
+                "name": roleName
+            }
+        ]
+        # 5. Form URL as - <url>/<userId>/role-mappings/clients/<clientId>
+        roleAssignUrl = url + '/' + userId + '/role-mappings/clients/' + clientId
+        # 6. Send POST request to assign role to user
+        try:
+            response = requests.post(roleAssignUrl, verify=False, json=data, headers=headers)
+            if response.status_code >= 200 and response.status_code < 300:
+                print('Successfully Assigned role', roleName, 'to user', userName)
+            else:
+                print('Failed to Assign role', roleName, 'to user', userName, response.text)
+        except requests.exceptions.Timeout:
+            sys.exit('HTTP request failed, please check you internet connection.')
+        except requests.exceptions.TooManyRedirects:
+            sys.exit('HTTP request failed, please check your proxy settings.')
+        except requests.exceptions.RequestException as e:
+            # catastrophic error. bail.
+            raise SystemExit(e)
+
+def getRoleIdByName(token, realmId, clientId, roleName) -> str:
+    url =  base + '/admin/realms/' + realmId + '/clients/' +clientId + '/roles?search=' + roleName
+    auth = 'bearer ' + token
+    headers = {
+        'content-type': 'application/json',
+        'accept': 'application/json',
+        'authorization': auth
+    }
+    try:
+        response = requests.get(url, verify=False, headers=headers)
+    except requests.exceptions.Timeout:
+        sys.exit('HTTP request failed, please check you internet connection.')
+    except requests.exceptions.TooManyRedirects:
+        sys.exit('HTTP request failed, please check your proxy settings.')
+    except requests.exceptions.RequestException as e:
+        # catastrophic error. bail.
+        raise SystemExit(e)
+    
+    if response.status_code >= 200 and response.status_code < 300:
+        role = response.json()
+        roleId = role[0]["id"]
+        return roleId
+    
+def getClientIdByName(token, realmId, clientName) -> str:
+    url =  base + '/admin/realms/' + realmId + '/clients?clientId=' + clientName
+    auth = 'bearer ' + token
+    headers = {
+        'content-type': 'application/json',
+        'accept': 'application/json',
+        'authorization': auth
+    }
+    try:
+        response = requests.get(url, verify=False, headers=headers)
+    except requests.exceptions.Timeout:
+        sys.exit('HTTP request failed, please check you internet connection.')
+    except requests.exceptions.TooManyRedirects:
+        sys.exit('HTTP request failed, please check your proxy settings.')
+    except requests.exceptions.RequestException as e:
+        # catastrophic error. bail.
+        raise SystemExit(e)
+    
+    if response.status_code >= 200 and response.status_code < 300:
+        client = response.json()
+        clientId = client[0]["id"]
+        return clientId
+    
+def getUserIdbyName(token, realmId, userName) -> str:
+    url =  base + '/admin/realms/' + realmId + '/users?username=' + userName
+    auth = 'bearer ' + token
+    headers = {
+        'content-type': 'application/json',
+        'accept': 'application/json',
+        'authorization': auth
+    }
+    try:
+        response = requests.get(url, verify=False, headers=headers)
+    except requests.exceptions.Timeout:
+        sys.exit('HTTP request failed, please check you internet connection.')
+    except requests.exceptions.TooManyRedirects:
+        sys.exit('HTTP request failed, please check your proxy settings.')
+    except requests.exceptions.RequestException as e:
+        # catastrophic error. bail.
+        raise SystemExit(e)
+    
+    if response.status_code >= 200 and response.status_code < 300:
+        user = response.json()
+        userId = user[0]["id"]
+        return userId
 
 # searches for the role of a given user
 def findRole(username: str, authConfig: dict, realmConfig: dict) -> dict:
index 14ea1c9..27cc221 100644 (file)
       ],
       "odlux.app": [],
       "kafka-ui.app": [],
+      "grafana-ui.app": [
+        {
+          "id" : "b072ad1a-818e-4ff9-b98c-3179bd7f4228",
+          "name" : "editor",
+          "description" : "Grafana Read Write Role",
+          "composite" : false,
+          "clientRole" : true,
+          "containerId" : "9fc6cecf-f3a8-48a8-8065-b2fc80b8b2f5",
+          "attributes" : { }
+        }, {
+          "id" : "09436bef-901c-44a5-b38d-508273d730ba",
+          "name" : "viewer",
+          "description" : "Read only access Role",
+          "composite" : false,
+          "clientRole" : true,
+          "containerId" : "9fc6cecf-f3a8-48a8-8065-b2fc80b8b2f5",
+          "attributes" : { }
+        }, {
+          "id" : "37e3d5fc-41d6-4926-a9c9-e3d96f7f4d6a",
+          "name" : "grafanaadmin",
+          "description" : "Grafana Administrator Role",
+          "composite" : false,
+          "clientRole" : true,
+          "containerId" : "9fc6cecf-f3a8-48a8-8065-b2fc80b8b2f5",
+          "attributes" : { }
+        }
+      ],
       "security-admin-console": [],
       "admin-cli": [],
       "account-console": [],
           "manage": true
       }
     },
+    {
+      "clientId": "grafana-ui.app",
+      "name": "Grafana UI",
+      "description": "",
+      "rootUrl": "https://grafana.smo.o-ran-sc.org",
+      "adminUrl": "https://grafana.smo.o-ran-sc.org",
+      "baseUrl": "",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "secret": "lVPuFWZlOV7yAbV1FIuaM0FOodD7cLTm",
+      "redirectUris": [
+        "https://grafana.smo.o-ran-sc.org/login/generic_oauth"
+      ],
+      "webOrigins": [
+        "https://grafana.smo.o-ran-sc.org"
+      ],
+      "notBefore": 0,
+      "bearerOnly": false,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": true,
+      "serviceAccountsEnabled": false,
+      "publicClient": false,
+      "frontchannelLogout": true,
+      "protocol": "openid-connect",
+      "attributes": {
+        "realm_client": "false",
+        "oidc.ciba.grant.enabled": "false",
+        "client.secret.creation.time": "1745992471",
+        "backchannel.logout.session.required": "true",
+        "frontchannel.logout.session.required": "true",
+        "oauth2.device.authorization.grant.enabled": "false",
+        "display.on.consent.screen": "false",
+        "backchannel.logout.revoke.offline.tokens": "false"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": true,
+      "nodeReRegistrationTimeout": -1,
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "roles",
+        "profile",
+        "basic",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ],
+      "access": {
+        "view": true,
+        "configure": true,
+        "manage": true
+      }
+    },
     {
       "id": "048a9bfc-077a-42a2-afe8-1ec13d3a43a3",
       "clientId": "realm-management",
           "protocolMapper": "oidc-usermodel-client-role-mapper",
           "consentRequired": false,
           "config": {
+            "introspection.token.claim" : "true",
+            "userinfo.token.claim" : "true",
+            "id.token.claim" : "true",
             "user.attribute": "foo",
             "access.token.claim": "true",
             "claim.name": "resource_access.${client_id}.roles",
           "protocolMapper": "oidc-usermodel-realm-role-mapper",
           "consentRequired": false,
           "config": {
+            "introspection.token.claim" : "true",
+            "userinfo.token.claim" : "true",
+            "id.token.claim" : "true",
+            "lightweight.claim" : "true",
             "user.attribute": "foo",
             "access.token.claim": "true",
             "claim.name": "realm_access.roles",
index 8c6b1c8..3c71de9 100644 (file)
@@ -1 +1 @@
-eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJQQTBmYmN1RUNvU1BkZlFCVjgxaE5kVHJwY0wtaE9LS0pGVkFDdEc5bUFrIn0.eyJleHAiOjE3NDI5NjYwODksImlhdCI6MTc0Mjk2NTc4OSwianRpIjoiMWU1OWVmY2EtYWYwZS00Zjc0LTgwMTktNGQ4NzE3YjBhYTZhIiwiaXNzIjoiaHR0cDovL2lkZW50aXR5OjgwODAvcmVhbG1zL25vbnJ0cmljLXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjQ4NTQyYmQzLWUxNmEtNGYwYS05NzY4LWM4NDVhM2Y2ZThjYiIsInR5cCI6IkJlYXJlciIsImF6cCI6InBtLXByb2R1Y2VyLWpzb24ya2Fma2EiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtbm9ucnRyaWMtcmVhbG0iLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwiY2xpZW50SG9zdCI6IjE3Mi4xOC4wLjE5IiwiY2xpZW50SWQiOiJwbS1wcm9kdWNlci1qc29uMmthZmthIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtcG0tcHJvZHVjZXItanNvbjJrYWZrYSIsImNsaWVudEFkZHJlc3MiOiIxNzIuMTguMC4xOSJ9.Y5ynDzNr9PRhsXNRDrWyi6HiIvWiA0vS-zP53Iq0NORSqNJTTdqC8oH1yuxlJYFqbs888uQ46CfyF_w7SocuvhvOHY_fouB8H4d9ENE3VAhK8SzfFFKUlRMLrReY9cgzjy64oT3jcJIfdbUpJzRn08eaozfq2WD-9pRqJHxN_9HhZgqKwaCoJS0u22WHEom-pmp0UNRN2o_W9xjEnNcvbX79-DLTWaHL5Bn98Vih4BmKPmDHYf43nmpXuMYPe2O8pwmaYpfAAPpjGXFs_hDGm_B-dPOt-RjHm_zSnh4I4DtKMuPKm6rFbs6hD-boFQUT2SlAfd1XSMdxdQxWzUWiIw
\ No newline at end of file
+eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWeDlCMUJNbjBLQlZtdHJXQ0xCTVRFWXloNFhiaDNOT2NYbXpudVlZUDZBIn0.eyJleHAiOjE3NDY0NDc4NDUsImlhdCI6MTc0NjQ0NzU0NSwianRpIjoiYTViNzg4ZWUtM2RkNy00NmM3LTk4ZTktMzIxZTc5NmFjNTEyIiwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5zbW8uby1yYW4tc2Mub3JnL3JlYWxtcy9ub25ydHJpYy1yZWFsbSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIwNTUyMzJjYi1lMmE1LTQ2M2QtYTQxMi03ZGYwYjQ0ODhlOTYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJwbS1wcm9kdWNlci1qc29uMmthZmthIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW5vbnJ0cmljLXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImNsaWVudEhvc3QiOiIxNzIuMjAuMC4xOSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LXBtLXByb2R1Y2VyLWpzb24ya2Fma2EiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjIwLjAuMTkiLCJjbGllbnRfaWQiOiJwbS1wcm9kdWNlci1qc29uMmthZmthIn0.kC4WihXFgbs-tHNK4FmxgbCoXPnJfCvoxhg-gTObfPYWCLnakdJCn-arC-RjOYHNulJGr5broTCvh336whVc4fK6kJMNR2mZ1LcRCm8Dt9qr_bYcJCp2PFqVjzybsOcP4rxjZFGIL7BNvSmc55ST4iXYSwtGeejuQ9iarAxPnU4dWSUJsEX4cO9XgL9P9QriQuZbqFa1YP_bDUahz_Y7mwncq4k_cp2_-weZmRGIRkxF3HiQDD-sOXt7CO8_mMXTJvASQnMipjl21H8f_6etMEgIRz5m1kfsjFvyWI3dogzS3L6BVaOFOO_iqTRIvB7IhsLoIOi2rylVgRbdSh8ztw
\ No newline at end of file
diff --git a/solution/smo/oam/pm/docker-compose-grafana.yaml b/solution/smo/oam/pm/docker-compose-grafana.yaml
new file mode 100644 (file)
index 0000000..1658a74
--- /dev/null
@@ -0,0 +1,38 @@
+services:
+  grafana:
+      image: grafana/grafana:10.0.0
+      container_name: grafana
+      ports:
+        - "3000:3000"
+      environment:
+        GF_AUTH_GENERIC_OAUTH_ENABLED: "true"
+        GF_AUTH_GENERIC_OAUTH_NAME: "Keycloak"
+        GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP: "true"
+        GF_AUTH_GENERIC_OAUTH_CLIENT_ID: "grafana-ui.app"
+        GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: "lVPuFWZlOV7yAbV1FIuaM0FOodD7cLTm"
+        GF_AUTH_GENERIC_OAUTH_SCOPES: "openid profile email offline_access roles"
+        GF_AUTH_GENERIC_OAUTH_AUTH_URL: "https://identity.${HTTP_DOMAIN}/realms/onap/protocol/openid-connect/auth"
+        GF_AUTH_GENERIC_OAUTH_TOKEN_URL: "https://identity.${HTTP_DOMAIN}/realms/onap/protocol/openid-connect/token"
+        GF_AUTH_GENERIC_OAUTH_API_URL: "https://identity.${HTTP_DOMAIN}/realms/onap/protocol/openid-connect/userinfo"
+        GF_SERVER_ROOT_URL: "https://grafana.${HTTP_DOMAIN}"
+        GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE: role
+        GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH: contains(resource_access."grafana-ui.app".roles[*], 'grafanaadmin') && 'GrafanaAdmin' || contains(resource_access."grafana-ui.app".roles[*], 'admin') && 'Admin' || contains(resource_access."grafana.app".roles[*], 'editor') && 'Editor' || 'Viewer'
+        GF_AUTH_GENERIC_OAUTH_ALLOW_ASSIGN_GRAFANA_ROLES: true
+        GF_AUTH_GENERIC_OAUTH_ALLOW_ASSIGN_GRAFANA_ADMIN: true
+        GF_AUTH_GENERIC_OAUTH_TLS_SKIP_VERIFY_INSECURE: true
+        GF_LOG_LEVEL: debug
+      labels:
+        traefik.enable: true
+        traefik.http.routers.grafana.entrypoints: websecure
+        traefik.http.routers.grafana.rule: Host(`grafana.${HTTP_DOMAIN}`)
+        traefik.http.routers.grafana.tls: true
+        traefik.http.services.grafana.loadbalancer.server.port: 3000
+        app: "grafana"
+        deploy: "o-ran-sc-smo-oam-pm"
+        solution: "o-ran-sc-smo"
+      networks:
+        - dmz
+
+networks:
+  dmz:
+    external: true
index 6c7a4cf..9eeb0da 100755 (executable)
@@ -76,6 +76,11 @@ setup_influx() {
     docker compose -p influx -f docker-compose-influxdb_gen.yaml up -d
 }
 
+setup_grafana() {
+    envsubst < docker-compose-grafana.yaml > docker-compose-grafana_gen.yaml
+    docker compose -p grafana -f docker-compose-grafana.yaml up -d
+}
+
 create_topics() {
 echo "Creating topics: $TOPICS, may take a while ..."            
 for t in $TOPICS; do
@@ -101,7 +106,7 @@ for net in $DNETWORKS; do
     if [ $? -ne 0 ]; then
         docker network create $net
     else
-        echo "  Network: $net exits"
+        echo "  Network: $net exists"
     fi
 done
 }
@@ -141,4 +146,6 @@ export INFLUXDB2_TOKEN
 
 setup_pm
 check_error $?
+setup_grafana
+check_error $?
             
\ No newline at end of file
index a725eb1..baad2ba 100755 (executable)
@@ -2,6 +2,7 @@ echo "Stop and remove all containers in the project"
 
 docker compose -p influx -f docker-compose-influxdb_gen.yaml down
 docker compose -p pm -f docker-compose_gen.yaml down
+docker compose -p grafana -f docker-compose-grafana_gen.yaml down
 
 echo "Removing influxdb2 config..."
 rm -rf ./config/influxdb2