F5Networks / f5-appsvcs-templates

F5 BIG-IP Application Service Templates (FAST)
Apache License 2.0
32 stars 13 forks source link

FAST reported installedTemplates hash does not match actual zip file hash #118

Closed simonkowallik closed 1 year ago

simonkowallik commented 1 year ago

Environment

Summary

The sha256 hash reported in the FAST API does not match the actual zip file sha256 hash.

Steps To Reproduce

List the steps to reproduce the behavior:

[root@bigip:Active:Standalone] config $ curl -sk -u $creds https://localhost/mgmt/shared/fast/info | \
jq '.installedTemplates | map(select(.name=="simplest"))'
[
  {
    "name": "simplest",
    "hash": "1cf4fb6d0549451c1a32bacff04fa3d3e2e646d3d3f5e5ad2be550215fa8051c",  <----- reported hash
    "supported": false,
    "templates": [
      {
        "name": "simplest/simplest",
        "hash": "b110031073ff5aeda9fdc412dbacde56ed7e9e842851951f61817ced4545a269",  <----- yaml file hash
        "description": "The simplest FAST template",
        "title": "simplest template",
        "appsList": []
      }
    ],
    "schemas": [],
    "dataFiles": [],
    "enabled": true
  }
]
[root@bigip:Active:Standalone] config $ cat /var/config/rest/iapps/f5-appsvcs-templates/scratch/simplest.zip | \
openssl sha256 -hex
(stdin)= 80a5602b95b460eb1386e50e7255f86160e239af586936712fc9a1e2ceeccb2d    <----- actual hash

The reported hash does not match the actual file hash:

1cf4fb6d0549451c1a32bacff04fa3d3e2e646d3d3f5e5ad2be550215fa8051c != 80a5602b95b460eb1386e50e7255f86160e239af586936712fc9a1e2ceeccb2d

However extracting the zip file and comparing the reported hash for the main yaml file in the FAST template:

[root@bigip:Active:Standalone] tmp $ cp /var/config/rest/iapps/f5-appsvcs-templates/scratch/simplest.zip /tmp/
[root@bigip:Active:Standalone] tmp $ unzip simplest.zip
Archive:  simplest.zip
  inflating: simplest/simplest.yaml
[root@bigip:Active:Standalone] tmp $ cat simplest/simplest.yaml | openssl sha256 -hex
(stdin)= b110031073ff5aeda9fdc412dbacde56ed7e9e842851951f61817ced4545a269    <----- actual hash matches

Expected Behavior

It is expected that the hash reported in the API actually matches the file hash calculated by openssl or sha256sum.

"hash": "80a5602b95b460eb1386e50e7255f86160e239af586936712fc9a1e2ceeccb2d"

Motivation

When managing templates with stateless automation systems (eg. ansible automation platform) it is very helpful to understand whether the FAST template changed. As hashes are created by FAST anyway it would be a great if we could calculate a local sha256 hash of the zip archive and just compare it to the hash in the API and then decide whether the template needs updating.

It does work for the main yaml file within the FAST template, however this becomes error prone as more complex templates typically consists of multiple files and the main yaml file does not necessarily change.

simonkowallik commented 1 year ago

https://github.com/F5Networks/f5-appsvcs-templates/blob/12c09698e2430fc6dbed1b5c7ba8fd5d1058e06a/nodejs/fastWorker.js#L2449-L2464

The above looks good, reproducing it with the below produces the expected result, so not sure where the hash changes.

[root@bigip:Active:Standalone] tmp # NODE_PATH="/usr/share/rest/node/node_modules" node hash.js
80a5602b95b460eb1386e50e7255f86160e239af586936712fc9a1e2ceeccb2d

hash.js:

const crypto = require('crypto');
const fs = require('fs-extra');

const zipFileHash = crypto.createHash('sha256');
const input = fs.createReadStream('simplest.zip');

input.on('data', (chunk) => {
  zipFileHash.update(chunk);
});

input.on('close', () => {
  zipFileDigestHash = zipFileHash.digest('hex');
  console.log(zipFileDigestHash);
});
joelkeener commented 1 year ago

The first hash is the template set, and each template within the set will have its own hash.

What you point out as the "<----- reported hash" is the template set, and the "<----- yaml file hash" is unique to each template -- of which there are only one in this case.

joelkeener commented 1 year ago

But I do also see what you are saying: that the hash calculated by fastWorker.js, with crypto.createHash(), of the zipped file doesn't match the hash that is reported for the template set itself in the json response from /mgmt/shared/fast/info.

joelkeener commented 1 year ago

The code you are referring to is for our OffBox templates, and they do create a hash of the zipped file, but the hash of your templateset, and of any templateset uploaded to the device, is a hash of each template's hash, and the schema's hash, and the data's hash.

You don't have schema or data, so to get 1cf4fb6d0549451c1a32bacff04fa3d3e2e646d3d3f5e5ad2be550215fa8051c from your templateset, you just need to plug the one template's hash into the following code:

const crypto = require('crypto');

const tsHash = crypto.createHash('sha256');
// update with template hash
["b110031073ff5aeda9fdc412dbacde56ed7e9e842851951f61817ced4545a269", ""].forEach((hash) => tsHash.update(hash));
// update with schema hash
[""].forEach((hash) => tsHash.update(hash));
tsHash.update("");
// update with data hash
[""].forEach((hash) => tsHash.update(hash));
tsHash.update("");

const tsHashDigest = tsHash.digest('hex');
console.log(tsHashDigest);

Please let me know if you have any questions about how we calculate the template set hash.

joelkeener commented 1 year ago

Once you have the hashes for every file in your template set, and you have grouped them into the three types, templates(.y*ml, .mst), schemas(.json) and dataFiles(.data), then you must sort the hashes in each group.

Here is a revised javascript that will do this correctly:

const crypto = require('crypto');

function updateHash(hashes) {
    return hashes.sort()
        .forEach((hash) => tsHash.update(hash));
}

let tsHash = crypto.createHash('sha256');

// template files, including all subtemplates: .yaml or .mst
updateHash(
    [
        '97fa20951a63084fa3399baa3692a2df21aff4b9c17c321d0dcf0c2fa032c40b', // simplest/_t_template.yaml
        '1fab74f9f675c1d83aaeae3f2b4c4de8591b52611efee6df7350e353a868434d', // simplest/_template.yaml
        '018de2590116c118018e80a9b45fd7f78515f2a3ab0882e99fa03860ff8586e8', // simplest/_x_template.yaml
        '11bca68c2abd4a5cada35659d0b01753db06cc303bf9de5cc649ed2d21408aa8', // simplest/alternate.yaml
        'bc4c1e8c544d6fb3b62bc248bac2974327cfd38e22074e24d65dafa163f16b74', // simplest/simplest.yaml
    ]
);
// schemas: .json files
updateHash(
    [
        '41bcb2be1ad8341a685f7150c99f864977e3b8b4332d60f8d82ac0236b786632', // simplest/f5.json
    ]
);
// dataFiles: .data files or files from ATG-Storage
updateHash(
    [
        '', // any .data file
    ]
);

const tsHashDigest = tsHash.digest('hex');
console.log(tsHashDigest);
simonkowallik commented 1 year ago

@joelkeener thank you for the detailed information. Knowing how the templateSet hash is calculated is very helpful in a stateless automation system (like ansible) to achieve idempotency.

joelkeener commented 1 year ago

There is already a tool that will output the hash of a local template in FAST Core, you just need the name of and path to the local template set, and with that you can use the FsSingleTemplateProvider to get the hash of the template set like this:

const fast = require('@f5devcentral/f5-fast-core');

const templateSetPath = '/path/to/templateSet';
const templateProvider = new fast.FsSingleTemplateProvider(templateSetPath);

templateProvider.getSetData('templateSetName')
    .then((tsData) => {
        console.log(tsData.hash)
    });

This will be documented in the FAST Core README.md file.

shyawnkarim commented 1 year ago

Closing.

Our documentation was updated with Release 1.24.0.