As there still is no option to create a proper dashboard/panel playlist for templated dashboards (see this issue "Support for playlist with templating variables" starting 2014(!)) I searched for quite a bit and found a nice HTML/JavaScript page that could do that. But unfortunately, as browsers make someone’s life harder and harder, basic-auth credentials do not work anymore in URLs in modern browsers. So I dug in further and found this complicated but working solutions.

Prerequisites

An account in Grafana with proper permissions (in this example we use app_grafana_tv)

Step 1 – Enable JWT-Authentication and allow embedding

Edit /etc/grafana/grafana.ini and add for example the following lines

#################################### Auth JWT ###########################
[auth.jwt]
enabled = true
enable_login_token = true
header_name = X-JWT-Assertion
email_claim = email
username_claim = login
jwk_set_file = /etc/grafana/jwks.json
cache_ttl = 60m
expect_claims = {"iss": "https://my-server.home.arpa/"}
auto_sign_up = true
url_login = true

Further find the [security]-section and add

# allow embedding in iframes - unfortunately needed for rotation-iframe HTML pages :|
allow_embedding = true

Step 2 – Create jwks.json and auth_token

create a new folder download the super nifty Bash-script and edit it so it looks similar like:

#!/bin/sh
## Requires openssl, nodejs, jq
## FIXES for jwt.io compliance
# use base64Url encoding
# use echo -n in pack function

header='
{
  "kid": "for-grafana-tvs",
  "alg": "RS256"
}'

# iat ... epoch time, after which datetime key should be active
# exp ... epoch time, when it should expire
payload='
{
  "sub": "app_grafana_tv",
  "login": "app_grafana_tv",
  "email": "app_grafana_tv",
  "isAdmin": false,
  "iat": 1681992628,
  "iss": "https://my-server.home.arpa/"
}'

function pack() {
  # Remove line breaks and spaces
  echo $1 | sed -e "s/[\r\n]\+//g" | sed -e "s/ //g"
}

function base64url_encode {
  (if [ -z "$1" ]; then cat -; else echo -n "$1"; fi) |
    openssl base64 -e -A |
      sed s/\\+/-/g |
      sed s/\\//_/g |
      sed -E s/=+$//
}

# just for debugging
function base64url_decode {
  INPUT=$(if [ -z "$1" ]; then echo -n $(cat -); else echo -n "$1"; fi)
  MOD=$(($(echo -n "$INPUT" | wc -c) % 4))
  PADDING=$(if [ $MOD -eq 2 ]; then echo -n '=='; elif [ $MOD -eq 3 ]; then echo -n '=' ; fi)
  echo -n "$INPUT$PADDING" |
    sed s/-/+/g |
    sed s/_/\\//g |
    openssl base64 -d -A
}

if [ ! -f private-key.pem ]; then
  # Private and Public keys
  openssl genrsa 2048 > private-key.pem
  openssl rsa -in private-key.pem -pubout -out public-key.pem
fi

# Base64 Encoding
b64_header=$(pack "$header" | base64url_encode)
b64_payload=$(pack "$payload" | base64url_encode)
signature=$(echo -n $b64_header.$b64_payload | openssl dgst -sha256 -sign private-key.pem | base64url_encode)

# Export JWT
echo $b64_header.$b64_payload.$signature > jwt.txt

# Create JWK from public key
if [ ! -d ./node_modules/pem-jwk ]; then
  # A tool to convert PEM to JWK
  npm install pem-jwk
fi

jwk=$(./node_modules/.bin/pem-jwk public-key.pem)

# Add additional fields
jwk=$(echo '{"use":"sig"}' $jwk $header | jq -cs add)

# Export JWK
echo '{"keys":['$jwk']}'| jq . > jwks.json

echo "--- JWT ---"
cat jwt.txt
echo -e "\n--- JWK ---"
jq . jwks.json

make it executable and execute it – the output should look similar to:

writing RSA key

added 6 packages in 314ms
--- JWT ---
eyJraWQiOiJ[...]FBnnz6h94Mo3IK3X63pZrbA

--- JWK ---
{
  "keys": [
    {
      "use": "sig",
      "kty": "RSA",
      "n": "znsQhes[...]JlXWY5bw",
      "e": "AQAB",
      "kid": "for-grafana-tvs",
      "alg": "RS256"
    }
  ]
}

Copy the JWK-json output into /etc/grafana/jwks.json on the Grafana-server and restart the daemon.

systemctl restart grafana-server && journalctl -fu grafana-server  # for checking if it's really working after grafana-server was restarted

Step 3 – Prepare the playlist HTML-page

If on the Grafana-server you use Apache as reverse proxy we need to add an additional ProxyPass and Alias similar to the virtualhost-config

ignore the SSL-stuff – balancer in front is handling the correct certificates

<VirtualHost 10.123.123.123:443>
    ServerName my-server.home.arpa
    ServerAlias grafana.example.com grafana-tv.example.com

    Header set Content-Security-Policy "frame-ancestors 'self' checkmk.example.com my-server.home.arpa;"

    ErrorLog /var/log/httpd/443_grafana-error.log
    CustomLog /var/log/httpd/443_grafana-access.log combinedio_2022

    SSLEngine on
    SSLProxyEngine on
    SSLProxyCheckPeerCN off
    SSLProxyCheckPeerName off
    SSLProtocol all -SSLv2 -SSLv3
    SSLHonorCipherOrder On
    SSLCipherSuite ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5

    SSLCertificateFile /etc/pki/tls/certs/localhost.crt
    SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
    RequestHeader unset Authorization
    ProxyPreserveHost Off

    # we exclude /rotation -- order is important!
    ProxyPass /rotation !
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    # the path where we put the playlist
    Alias /rotation /var/www/html/grafana/rotation
</VirtualHost>

Now get some URLs that should be shown in the playlist, e.g. from single panels.

  • open a dashboard
  • click on the panel and select "view"
  • change variables if there are any
  • save the URL shown in the browser address-bar starting from the the first / – e.g. from https://grafana.example.com/d/6RRBSu5Vk/mydashboard?orgId=1&viewPanel=86 copy /d/6RRBSu5Vk/mydashboard?orgId=1&viewPanel=86 – note it down somewhere in the meantime or immediately copy it into the mentioned HTML-page
  • repeat with as many panels/dashboards as you want

Then copy/update your playlist HTML-page with the URLs – see comments in file/code below – so it looks similar to this:


// original from https://gist.github.com/czerasz/569ed1f923b610efba91375eeb35d5ab - slightly edited
<html>
  <head>
    <meta charset="utf-8">
    <style type="text/css">
      body, html {
        margin: 0; padding: 0;
        height: 100%;
        overflow: hidden;

        background: #141414;
      }

      .content {
        position: absolute;
        left: 0; right: 0;
        bottom: 0; top: 0px;
      }
    </style>
  </head>
  <body>
      <iframe class="content" id="iframe1" src="" width="100%" height="100%" frameborder="0"></iframe>
      <iframe class="content" id="iframe2" src="" width="100%" height="100%" frameborder="0"></iframe>
  </body>
  <script type="text/javascript">
    const TIMEOUT = 15000; // 15 seconds
    const BASE_URL = "https://my-server.home.arpa";
    // replace the token accordingly with the output from the bash-script described in the howto
    const AUTH_TOKEN = "&auth_token=eyJraWQiOiJ[...]FBnnz6h94Mo3IK3X63pZrbA"
    const ENABLE_KIOSK = "&kiosk"
    // add your panel/dashboard URLs you want to rotate through in "Kiosk"-mode
    const dashboards_list = [
      "/d/SoOWs5Wmk/mydashboard?orgId=1&refresh=30s&viewPanel=30&var-cluster=elk&var-name=All&var-instance=elk-node-1.home.arpa:9114&var-interval=5m",
      "/d/SoOWs5Wmk/mydashboard?orgId=1&refresh=30s&var-cluster=elk&var-name=All&var-instance=elk-node-1.home.arpa:9114&var-interval=5m&viewPanel=88"
    ];

    const iframe_1 = document.getElementById("iframe1");
    const iframe_2 = document.getElementById("iframe2");
    var index = 0;

    // Set initial values
    iframe_1.src = BASE_URL + dashboards_list[index] + AUTH_TOKEN + ENABLE_KIOSK;
    iframe_1.style.zIndex = 1;
    iframe_2.style.zIndex = 0;

    function load_next_url() {
      index = (index + 1) % dashboards_list.length;

      select_bottom().src = BASE_URL + dashboards_list[index] + AUTH_TOKEN + ENABLE_KIOSK;

      setTimeout(load_next_url, TIMEOUT);
      // Show next slide after 3 seconds
      setTimeout(change_order, 3000);
    }
    /**
     * Return iFrame which currently hidden - iFrame which has a lower z-index.
     *
     * @return {Element Object} iFrame DOM element
     */
    function select_bottom() {
      if ( parseInt(iframe_1.style.zIndex, 10) > parseInt(iframe_2.style.zIndex, 10) ) {
        return iframe_2;
      } else {
        return iframe_1;
      }
    }
    /**
     * Switch order of the iFrames.
     * The iFrame from bottom will move on top.
     */
    function change_order() {
      if ( parseInt(iframe_1.style.zIndex, 10) > parseInt(iframe_2.style.zIndex, 10) ) {
        iframe_1.style.zIndex = 0;
        iframe_2.style.zIndex = 1;
      } else {
        iframe_1.style.zIndex = 1;
        iframe_2.style.zIndex = 0;
      }
    }
    setTimeout(load_next_url, TIMEOUT);
  </script>
</html>

Save your changes and open the URL e.g. https://grafana-tv.example.com/rotation/rotate_test_1.html in a browser 🙂

Caution

Not sure how secure this whole setup is, so it should not be used for general public usage from the internet without any authentication ;). We use this for only from internal networks.

Zuletzt bearbeitet: Oktober 29, 2023

Autor

Kommentare

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.