Skip to content

Conversation

@winterhazel
Copy link
Member

Description

Before commit 03a4b9f, preset variables were injected into the JS interpreter by directly prepending them as a string to the script. 03a4b9f changed the interpreter so that preset variables are injected via bindings instead. However, all preset variables are still injected as a string. Due to this, variables that were previously interpreted as objects are now interpreted as strings. This broke the Quota activation rules and secondary storage selectors features.

This PR changes the code to inject the preset variables using their expected types. Also, the test files of most preset variables have been removed because the tests do not make sense anymore with the new injection mechanism.

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (improves an existing feature and functionality)
  • Cleanup (Code refactoring and cleanup, that may add test cases)
  • build/CI
  • test (unit or integration test code)

Feature/Enhancement Scale or Bug Severity

Bug Severity

  • BLOCKER
  • Critical
  • Major
  • Minor
  • Trivial

How Has This Been Tested?

  • I created a Quota tariff with an activation rule, ran the Usage job, and debugged org.apache.cloudstack.utils.jsinterpreter.JsInterpreter#executeScriptInThread to validate that the preset variables had their expected type in the JS interpreter.
  • I created a secondary storage selector for templates, registered a template, and debugged org.apache.cloudstack.utils.jsinterpreter.JsInterpreter#executeScriptInThread to validate that the preset variables had their expected type in the JS interpreter.
  • I added a flexible host tag to one of my hosts, deployed a VM using an offering that matched the tag, and debugged org.apache.cloudstack.utils.jsinterpreter.JsInterpreter#executeScriptInThread to validate that the offering's host tags were correctly injected into the JS interpreter as a list.

@codecov
Copy link

codecov bot commented Jan 25, 2026

Codecov Report

❌ Patch coverage is 67.21311% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 16.24%. Comparing base (95de88a) to head (f920c7f).
⚠️ Report is 25 commits behind head on 4.20.

Files with missing lines Patch % Lines
...dstack/storage/heuristics/HeuristicRuleHelper.java 40.00% 6 Missing ⚠️
...k/storage/heuristics/presetvariables/Template.java 33.33% 4 Missing ⚠️
...ionrule/presetvariables/GenericPresetVariable.java 0.00% 2 Missing ⚠️
...resetvariables/GenericHeuristicPresetVariable.java 0.00% 2 Missing ⚠️
.../cloudstack/utils/jsinterpreter/JsInterpreter.java 77.77% 1 Missing and 1 partial ⚠️
...loudstack/utils/jsinterpreter/TagAsRuleHelper.java 71.42% 2 Missing ⚠️
...k/storage/heuristics/presetvariables/Snapshot.java 50.00% 1 Missing ⚠️
...ack/storage/heuristics/presetvariables/Volume.java 50.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               4.20   #12515      +/-   ##
============================================
- Coverage     16.26%   16.24%   -0.02%     
+ Complexity    13420    13405      -15     
============================================
  Files          5658     5658              
  Lines        499496   499444      -52     
  Branches      60626    60625       -1     
============================================
- Hits          81233    81147      -86     
- Misses       409214   409246      +32     
- Partials       9049     9051       +2     
Flag Coverage Δ
uitests 4.02% <ø> (ø)
unittests 17.10% <67.21%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@winterhazel
Copy link
Member Author

@blueorangutan package

@blueorangutan
Copy link

@winterhazel a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan
Copy link

Packaging result [SF]: ✖️ el8 ✖️ el9 ✖️ debian ✖️ suse15. SL-JID 16519

@winterhazel
Copy link
Member Author

@blueorangutan package

@blueorangutan
Copy link

@winterhazel a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan
Copy link

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 16520

@DaanHoogland
Copy link
Contributor

@blueorangutan test

@blueorangutan
Copy link

@DaanHoogland a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests

@DaanHoogland DaanHoogland requested a review from shwstppr January 26, 2026 11:35
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@RosiKyu
Copy link
Collaborator

RosiKyu commented Jan 26, 2026

@blueorangutan test

@RosiKyu RosiKyu self-assigned this Jan 26, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 44 out of 44 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@shwstppr shwstppr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code lgtm. Copilot highlighted a logging issue

@blueorangutan
Copy link

[SF] Trillian test result (tid-15283)
Environment: kvm-ol8 (x2), zone: Advanced Networking with Mgmt server ol8
Total time taken: 48361 seconds
Marvin logs: https://github.com/blueorangutan/acs-prs/releases/download/trillian/pr12515-t15283-kvm-ol8.zip
Smoke tests completed. 141 look OK, 0 have errors, 0 did not run
Only failed and skipped tests results shown below:

Test Result Time (s) Test File

@winterhazel
Copy link
Member Author

@blueorangutan package

@blueorangutan
Copy link

@winterhazel a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan
Copy link

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 16566

@github-actions
Copy link

This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.

@winterhazel
Copy link
Member Author

@blueorangutan package

@blueorangutan
Copy link

@winterhazel a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan
Copy link

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 16597

@sonarqubecloud
Copy link

@DaanHoogland
Copy link
Contributor

@blueorangutan test

@blueorangutan
Copy link

@DaanHoogland a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests

@blueorangutan
Copy link

[SF] Trillian test result (tid-15325)
Environment: kvm-ol8 (x2), zone: Advanced Networking with Mgmt server ol8
Total time taken: 50727 seconds
Marvin logs: https://github.com/blueorangutan/acs-prs/releases/download/trillian/pr12515-t15325-kvm-ol8.zip
Smoke tests completed. 141 look OK, 0 have errors, 0 did not run
Only failed and skipped tests results shown below:

Test Result Time (s) Test File

@shwstppr shwstppr self-requested a review February 11, 2026 08:43
Copy link
Contributor

@shwstppr shwstppr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code lgtm

Copy link
Collaborator

@RosiKyu RosiKyu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

TC Area Result
TC1 Quota: account.name object access (positive + negative) PASS
TC2 Secondary storage selector: secondaryStorages.get(n).id (positive + negative) PASS
TC3 Quota: enum→String account.role.type == 'Admin' PASS
TC4 Quota: domain object domain.name == 'ROOT' PASS
TC5 Host tags: tags.contains('gpu') single tag PASS
TC6 Host tags: tags.contains('gpu') && tags.contains('compute') compound PASS
TC7 Negative: invalid JS syntax handling PASS
TC8 Negative: js.interpretation.enabled=false kill switch PASS

Observations (not introduced by this PR):

  1. No save-time JS validation (from PR #7659) - CreateSecondaryStorageSelectorCmd and UpdateSecondaryStorageSelectorCmd store the heuristic rule as a plain string with no syntax validation. Invalid JS is only caught at execution time (e.g. during template registration).

  2. Kill switch does not block execution of existing rules (from commit 03a4b9f) - js.interpretation.enabled=false blocks API entry points only (command registration, quota tariff creation, host/storage tag-as-rule updates). However, the execution path (HeuristicRuleHelperJsInterpreter) has no awareness of this flag, so pre-existing rules in the DB still execute.

  3. Encrypted configuration requires direct DB update (from commit 03a4b9f) - js.interpretation.enabled is defined with "Hidden" category, causing encrypted storage. Setting it via the API causes double-encryption. Must be encrypted via EncryptionCLI and updated directly in the DB, followed by a management server restart (isDynamic=false).

TC1: Basic Preset Variable Object Access (account.name)

Objective:
Verify that the account preset variable is injected into the JavaScript activation rule engine as a proper Java object with accessible properties (e.g., account.name)

Test Steps:

  1. Enable JS interpretation and quota service, restart management server
  2. Set RUNNING_VM tariff value=1.0 with activation rule: account.name == 'admin' ? 2.0 : 1.0
  3. Verify preset variables structure via quota presetvariableslist
  4. Insert manual usage record (23 hrs RUNNING_VM, Feb 10) into cloud_usage
  5. Seed quota_account entry, set usage.stats.job.exec.time, restart cloudstack-usage
  6. Verify positive case: quota_used reflects multiplier 2.0
  7. Update activation rule to: account.name == 'nonexistent' ? 2.0 : 1.0
  8. Reset quota_calculated=0, delete quota_usage, trigger new cycle
  9. Verify negative case: quota_used reflects multiplier 1.0

Expected Result:

  • Positive case: account.name resolves to 'admin', rule returns 2.0, quota_used ≈ 0.06845 (23/672 × 2.0)
  • Negative case: account.name resolves to 'admin''nonexistent', rule returns 1.0, quota_used ≈ 0.03423 (23/672 × 1.0)
  • Ratio between cases = exactly 2:1

Actual Result: PASSED

  • Positive case: quota_used = 0.06845237
  • Negative case: quota_used = 0.03422630
  • Ratio: 0.06845237 / 0.03422630 = 2.0

Test Evidence:

1. Configuration - js.interpretation.enabled:

(localcloud) 🐱 > update configuration name=js.interpretation.enabled value=true
{
  "configuration": {
    "category": "Hidden",
    "component": "ManagementServer",
    "defaultvalue": "false",
    "description": "Enable/Disable all JavaScript interpretation related functionalities to create or update Javascript rules.",
    "displaytext": "Js interpretation enabled",
    "group": "Miscellaneous",
    "isdynamic": false,
    "name": "js.interpretation.enabled",
    "subgroup": "Others",
    "type": "Boolean",
    "value": "Ze+gLNFWo7mLvYcsQj33886SRZMZgC81z8buPAFfuno="
  }
}

2. Configuration - quota.enable.service:

(localcloud) 🐱 > update configuration name=quota.enable.service value=true
{
  "configuration": {
    "category": "Advanced",
    "component": "QUOTA-PLUGIN",
    "defaultvalue": "false",
    "description": "Indicates whether Quota plugin is enabled or not.",
    "displaytext": "Quota enable service",
    "group": "Miscellaneous",
    "isdynamic": true,
    "name": "quota.enable.service",
    "subgroup": "Quota",
    "type": "Boolean",
    "value": "true"
  }
}

3. Tariff update with activation rule (positive case):

(localcloud) 🐱 > quota tariffupdate name=RUNNING_VM activationrule="account.name == 'admin' ? 2.0 : 1.0"
{
  "quotatariff": {
    "activationRule": "account.name == 'admin' ? 2.0 : 1.0",
    "currency": "$",
    "effectiveDate": "2010-05-04T00:00:00+0000",
    "id": "1b68defa-30ec-4cb0-87d2-177c5afed9b8",
    "name": "RUNNING_VM",
    "position": 1,
    "tariffValue": 1,
    "usageDiscriminator": "",
    "usageName": "RUNNING_VM",
    "usageType": 1,
    "usageTypeDescription": "Running Vm Usage",
    "usageUnit": "Compute*Month"
  }
}

4. Preset variables confirm object hierarchy (41 variables):

(localcloud) 🐱 > quota presetvariableslist usagetype=1 account=admin domainid=59139aea-072b-11f1-b31f-1e00c00002b4
{
  "count": 41,
  "presetvariable": [
    { "path": "account",              "type": "object", "value": "" },
    { "path": "account.role",         "type": "object", "value": "" },
    { "path": "account.role.type",    "type": "string", "value": "Admin" },
    { "path": "account.role.id",      "type": "string", "value": "7f9c1515-072b-11f1-b31f-1e00c00002b4" },
    { "path": "account.role.name",    "type": "string", "value": "Root Admin" },
    { "path": "account.id",           "type": "string", "value": "a6873438-072b-11f1-b31f-1e00c00002b4" },
    { "path": "account.name",         "type": "string", "value": "admin" },
    { "path": "domain",               "type": "object", "value": "" },
    { "path": "domain.path",          "type": "string", "value": "/" },
    { "path": "domain.id",            "type": "string", "value": "59139aea-072b-11f1-b31f-1e00c00002b4" },
    { "path": "domain.name",          "type": "string", "value": "ROOT" },
    { "path": "project",              "type": "object", "value": "" },
    { "path": "project.id",           "type": "string", "value": "" },
    { "path": "project.name",         "type": "string", "value": "" },
    { "path": "value",                "type": "object", "value": "" },
    { "path": "value.id",             "type": "string", "value": "" },
    { "path": "value.name",           "type": "string", "value": "" },
    { "path": "zone",                 "type": "object", "value": "" },
    { "path": "zone.id",              "type": "string", "value": "" },
    { "path": "zone.name",            "type": "string", "value": "" }
  ]
}

5. Manual usage record inserted:

mysql> INSERT INTO cloud_usage.cloud_usage
    -> (zone_id, account_id, domain_id, description, usage_display, usage_type, raw_usage,
    ->  vm_instance_id, vm_name, offering_id, template_id, usage_id, type, start_date, end_date,
    ->  cpu_speed, cpu_cores, memory, quota_calculated, is_hidden)
    -> VALUES
    -> (1, 2, 1, 'test-vm-pr12515', '23 Hrs', 1, 23.0, 6, 'i-2-6-VM', 2, 4, 6, 'VirtualMachine',
    ->  '2026-02-10 00:00:00', '2026-02-10 23:59:59', 1000, 1, 1024, 0, 0);
Query OK, 1 row affected (0.00 sec)

mysql> SELECT id, account_id, usage_type, raw_usage, quota_calculated, start_date, end_date FROM cloud_usage.cloud_usage WHERE account_id = 2;
+----+------------+------------+-----------+------------------+---------------------+---------------------+
| id | account_id | usage_type | raw_usage | quota_calculated | start_date          | end_date            |
+----+------------+------------+-----------+------------------+---------------------+---------------------+
|  1 |          2 |          1 |        23 |                0 | 2026-02-10 00:00:00 | 2026-02-10 23:59:59 |
+----+------------+------------+-----------+------------------+---------------------+---------------------+

6. Quota account seeded:

mysql> INSERT INTO cloud_usage.quota_account (account_id, quota_balance, quota_balance_date, quota_enforce, quota_min_balance, quota_alert_date, quota_alert_type, last_statement_date)
    -> VALUES (2, 0.00, '2026-02-09 00:00:00', 0, 0.00, NULL, 0, '2026-02-09 00:00:00');
Query OK, 1 row affected (0.00 sec)

7. Positive case - quota cycle at 11:12 (rule: account.name == 'admin' → 2.0):

2026-02-11 11:12:00,223 INFO  [cloudstack.quota.QuotaManagerImpl] (Usage-Job-1:[]) Starting quota usage calculation for accounts [[{"accountName":"system","domainId":1,"id":1,"uuid":"a6865272-072b-11f1-b31f-1e00c00002b4"},{"accountName":"admin","domainId":1,"id":2,"uuid":"a6873438-072b-11f1-b31f-1e00c00002b4"},{"accountName":"baremetal-system-account","domainId":1,"id":3,"uuid":"f250b39f-1db9-411a-a008-ce90e3feed59"},{"accountName":"ACSUser","domainId":1,"id":4,"uuid":"25c61240-e8f4-4b05-9f24-92dcd984a6fb"},{"accountName":"testuser","domainId":1,"id":5,"uuid":"820a4793-0d1f-47a2-8cf5-3ffe30e02462"},{"accountName":"PrjAcct-testproject-1","domainId":1,"id":6,"uuid":"5d9bee31-eb1d-4701-b15c-ed9b6f753dcf"}]].
2026-02-11 11:12:01,395 INFO  [cloudstack.quota.QuotaManagerImpl] (Usage-Job-1:[]) Finished quota usage calculation for accounts [[...]].
mysql> SELECT quota_calculated FROM cloud_usage.cloud_usage WHERE id = 1;
+------------------+
| quota_calculated |
+------------------+
|                1 |
+------------------+

mysql> SELECT * FROM cloud_usage.quota_usage WHERE account_id = 2;
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+
| id | usage_item_id | zone_id | account_id | domain_id | usage_type | quota_used | start_date          | end_date            |
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+
|  3 |             1 |       1 |          2 |         1 | 1          | 0.06845237 | 2026-02-10 00:00:00 | 2026-02-10 23:59:59 |
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+

**quota_used = 0.06845237 → 23/672 × 2.0 = 0.068452... **

8. Tariff update with activation rule (negative case):

(localcloud) 🐱 > quota tariffupdate name=RUNNING_VM activationrule="account.name == 'nonexistent' ? 2.0 : 1.0"
{
  "quotatariff": {
    "activationRule": "account.name == 'nonexistent' ? 2.0 : 1.0",
    "currency": "$",
    "effectiveDate": "2010-05-04T00:00:00+0000",
    "id": "e8457319-402a-48da-9522-69dc9a173111",
    "name": "RUNNING_VM",
    "position": 1,
    "tariffValue": 1,
    "usageDiscriminator": "",
    "usageName": "RUNNING_VM",
    "usageType": 1,
    "usageTypeDescription": "Running Vm Usage",
    "usageUnit": "Compute*Month"
  }
}

9. Reset and reprocess:

mysql> UPDATE cloud_usage.cloud_usage SET quota_calculated = 0 WHERE id = 1;
Query OK, 1 row affected (0.01 sec)

mysql> DELETE FROM cloud_usage.quota_usage WHERE account_id = 2;
Query OK, 1 row affected (0.00 sec)

10. Negative case - quota cycle at 11:42 (rule: account.name == 'nonexistent' → 1.0):

2026-02-11 11:42:00,194 INFO  [cloudstack.quota.QuotaManagerImpl] (Usage-Job-1:[]) Starting quota usage calculation for accounts [[{"accountName":"system","domainId":1,"id":1,"uuid":"a6865272-072b-11f1-b31f-1e00c00002b4"},{"accountName":"admin","domainId":1,"id":2,"uuid":"a6873438-072b-11f1-b31f-1e00c00002b4"},{"accountName":"baremetal-system-account","domainId":1,"id":3,"uuid":"f250b39f-1db9-411a-a008-ce90e3feed59"},{"accountName":"ACSUser","domainId":1,"id":4,"uuid":"25c61240-e8f4-4b05-9f24-92dcd984a6fb"},{"accountName":"testuser","domainId":1,"id":5,"uuid":"820a4793-0d1f-47a2-8cf5-3ffe30e02462"},{"accountName":"PrjAcct-testproject-1","domainId":1,"id":6,"uuid":"5d9bee31-eb1d-4701-b15c-ed9b6f753dcf"}]].
2026-02-11 11:42:00,937 INFO  [cloudstack.quota.QuotaManagerImpl] (Usage-Job-1:[]) Finished quota usage calculation for accounts [[...]].
mysql> SELECT quota_calculated FROM cloud_usage.cloud_usage WHERE id = 1;
+------------------+
| quota_calculated |
+------------------+
|                1 |
+------------------+

mysql> SELECT * FROM cloud_usage.quota_usage WHERE account_id = 2;
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+
| id | usage_item_id | zone_id | account_id | domain_id | usage_type | quota_used | start_date          | end_date            |
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+
|  4 |             1 |       1 |          2 |         1 | 1          | 0.03422630 | 2026-02-10 00:00:00 | 2026-02-10 23:59:59 |
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+

quota_used = 0.03422630 → 23/672 × 1.0 = 0.034226...

11. Comparison:

Case Activation Rule Multiplier quota_used Match
Positive account.name == 'admin' ? 2.0 : 1.0 2.0 0.06845237 Yes
Negative account.name == 'nonexistent' ? 2.0 : 1.0 1.0 0.03422630 Yes

**Ratio: 0.06845237 / 0.03422630 = 2.0 **

TC2: Secondary Storage Selector - Java List Object Injection

Objective:
Verify that secondaryStorages is injected as a proper Java List of objects (not a stringified representation) in the secondary storage heuristic rule engine. The rule must be able to call .get(index) on the list and access .id on the returned object - both operations that would fail if the variable were a .toString() string

Test Steps:

  1. Create a secondary storage selector with heuristic rule for TEMPLATE type
  2. Set rule to secondaryStorages.get(0).id - selects first store
  3. Register template and verify it lands on sec1
  4. Update rule to secondaryStorages.get(1).id - selects second store
  5. Register second template and verify it lands on sec2
  6. Check management-server.log for JS interpreter execution traces

Expected Result:

  • Template 1 registers on sec1 (7c3a1511-4f6e-4531-90f0-a6f9f5e1652e)
  • Template 2 registers on sec2 (78247ae8-f9bb-4b8d-a708-a0ae1b5c1595)
  • Logs confirm script execution with correct UUID results

Actual Result: PASSED

  • Template 1 landed on sec1
  • Template 2 landed on sec2
  • JS interpreter correctly executed .get() and .id on Java objects

Test Evidence:

1. Selector creation:

(localcloud) 🐱 > create secondarystorageselector zoneid=6cf85da0-c3e7-44ef-9da5-dc7383b23996 name=test-selector-pr12515 description="PR12515 test" type=TEMPLATE heuristicrule="secondaryStorages.get(0)"
{
  "heuristics": {
    "created": "2026-02-11T12:00:58+0000",
    "description": "PR12515 test",
    "heuristicrule": "secondaryStorages.get(0)",
    "id": "193fd72c-b140-4859-b594-6a5ac389c07f",
    "name": "test-selector-pr12515",
    "type": "TEMPLATE",
    "zoneid": "6cf85da0-c3e7-44ef-9da5-dc7383b23996"
  }
}

2. Object structure discovery - confirms Java objects with properties (not strings):

Error: Unable to find a secondary storage with the UUID [{"id":"7c3a1511-4f6e-4531-90f0-a6f9f5e1652e",
"protocol":"nfs","totalDiskSize":2898029182976,"usedDiskSize":1624624857088,
"name":"NFS://10.0.32.4/acs/secondary/ref-trl-10970-k-Mol9-rositsa-kyuchukova/ref-trl-10970-k-Mol9-rositsa-kyuchukova-sec1"}]

This confirms secondaryStorages.get(0) returns a proper Java object with id, protocol, totalDiskSize, usedDiskSize, name properties.

3. Positive case - rule: secondaryStorages.get(0).id → sec1:

(localcloud) 🐱 > update secondarystorageselector id=193fd72c-b140-4859-b594-6a5ac389c07f heuristicrule="secondaryStorages.get(0).id"
{
  "heuristics": {
    "created": "2026-02-11T12:00:58+0000",
    "description": "PR12515 test",
    "heuristicrule": "secondaryStorages.get(0).id",
    "id": "193fd72c-b140-4859-b594-6a5ac389c07f",
    "name": "test-selector-pr12515",
    "type": "TEMPLATE",
    "zoneid": "6cf85da0-c3e7-44ef-9da5-dc7383b23996"
  }
}

(localcloud) 🐱 > register template name=test-template-selector displaytext="PR12515 selector test" url=http://10.0.3.122/vladitemplates/qcow2/linux-debian-12-x86_64-gen2-v1.qcow2 format=QCOW2 hypervisor=KVM ostypeid=7c619234-072b-11f1-b31f-1e00c00002b4 zoneid=6cf85da0-c3e7-44ef-9da5-dc7383b23996
{
  "count": 1,
  "template": [
    {
      "name": "test-template-selector",
      "downloaddetails": [
        {
          "datastore": "NFS://10.0.32.4/acs/secondary/ref-trl-10970-k-Mol9-rositsa-kyuchukova/ref-trl-10970-k-Mol9-rositsa-kyuchukova-sec1",
          "datastoreId": "7c3a1511-4f6e-4531-90f0-a6f9f5e1652e",
          "datastoreRole": "Image"
        }
      ]
    }
  ]
}

**Template landed on sec1 (7c3a1511-4f6e-4531-90f0-a6f9f5e1652e) **

4. Negative case - rule: secondaryStorages.get(1).id → sec2:

(localcloud) 🐱 > update secondarystorageselector id=193fd72c-b140-4859-b594-6a5ac389c07f heuristicrule="secondaryStorages.get(1).id"
{
  "heuristics": {
    "created": "2026-02-11T12:00:58+0000",
    "description": "PR12515 test",
    "heuristicrule": "secondaryStorages.get(1).id",
    "id": "193fd72c-b140-4859-b594-6a5ac389c07f",
    "name": "test-selector-pr12515",
    "type": "TEMPLATE",
    "zoneid": "6cf85da0-c3e7-44ef-9da5-dc7383b23996"
  }
}

(localcloud) 🐱 > register template name=test-template-selector-2 displaytext="PR12515 selector test 2" url=http://10.0.3.122/vladitemplates/qcow2/linux-debian-11-x86_64-gen2-v1.qcow2 format=QCOW2 hypervisor=KVM ostypeid=7c619234-072b-11f1-b31f-1e00c00002b4 zoneid=6cf85da0-c3e7-44ef-9da5-dc7383b23996
{
  "count": 1,
  "template": [
    {
      "name": "test-template-selector-2",
      "downloaddetails": [
        {
          "datastore": "NFS://10.0.32.4/acs/secondary/ref-trl-10970-k-Mol9-rositsa-kyuchukova/ref-trl-10970-k-Mol9-rositsa-kyuchukova-sec2",
          "datastoreId": "78247ae8-f9bb-4b8d-a708-a0ae1b5c1595",
          "datastoreRole": "Image"
        }
      ]
    }
  ]
}

**Template landed on sec2 (78247ae8-f9bb-4b8d-a708-a0ae1b5c1595) **

5. Management server logs - JS interpreter execution traces:

2026-02-11 12:05:36,326 DEBUG [o.a.c.s.h.HeuristicRuleHelper] Found the heuristic rule Heuristic {"heuristicRule":"secondaryStorages.get(0).id","id":2,"name":"test-selector-pr12515","type":"TEMPLATE","uuid":"193fd72c-b140-4859-b594-6a5ac389c07f"} to apply for zone [Zone {"id": "1", "name": "ref-trl-10970-k-Mol9-rositsa-kyuchukova", "uuid": "6cf85da0-c3e7-44ef-9da5-dc7383b23996"}].
2026-02-11 12:05:36,331 DEBUG [o.a.c.u.j.JsInterpreter] Executing script [secondaryStorages.get(0).id].
2026-02-11 12:05:36,346 DEBUG [o.a.c.u.j.JsInterpreter] The script [secondaryStorages.get(0).id] had the following result: [7c3a1511-4f6e-4531-90f0-a6f9f5e1652e].

2026-02-11 12:06:10,175 DEBUG [o.a.c.s.h.HeuristicRuleHelper] Found the heuristic rule Heuristic {"heuristicRule":"secondaryStorages.get(1).id","id":2,"name":"test-selector-pr12515","type":"TEMPLATE","uuid":"193fd72c-b140-4859-b594-6a5ac389c07f"} to apply for zone [Zone {"id": "1", "name": "ref-trl-10970-k-Mol9-rositsa-kyuchukova", "uuid": "6cf85da0-c3e7-44ef-9da5-dc7383b23996"}].
2026-02-11 12:06:10,179 DEBUG [o.a.c.u.j.JsInterpreter] Executing script [secondaryStorages.get(1).id].
2026-02-11 12:06:10,189 DEBUG [o.a.c.u.j.JsInterpreter] The script [secondaryStorages.get(1).id] had the following result: [78247ae8-f9bb-4b8d-a708-a0ae1b5c1595].

6. Comparison:

Case Heuristic Rule Expected Store Actual Store UUID Match
get(0) secondaryStorages.get(0).id sec1 7c3a1511-4f6e-4531-90f0-a6f9f5e1652e Match
get(1) secondaryStorages.get(1).id sec2 78247ae8-f9bb-4b8d-a708-a0ae1b5c1595 Match

Key finding: The String(secondaryStorages.get(0)) debug output revealed the full Java object structure: {"id":"...","protocol":"nfs","totalDiskSize":...,"usedDiskSize":...,"name":"..."}. This confirms secondaryStorages is injected as a proper Java List of objects with accessible properties - not a .toString() string

TC7: Negative - Invalid JS Syntax Handling

Objective:
Verify that a syntactically invalid JavaScript heuristic rule is caught at execution time with a clear, descriptive error message.

Test Steps:

  1. Update existing secondary storage selector with broken JS: secondaryStorages.get(0).id + (trailing operator, no operand)
  2. Attempt to register a template to trigger rule execution
  3. Verify the error message includes the syntax error details

Expected Result:
Rule saves (validation is at execution time), but template registration fails with a JS syntax error.

Actual Result: PASSED

  • Rule saved without error (no compile-time validation)
  • Template registration failed with clear error identifying the exact syntax problem

Test Evidence:

1. Broken rule saved successfully (no save-time validation):

(localcloud) 🐱 > update secondarystorageselector id=193fd72c-b140-4859-b594-6a5ac389c07f heuristicrule="secondaryStorages.get(0).id +"
{
  "heuristics": {
    "heuristicrule": "secondaryStorages.get(0).id +",
    "id": "193fd72c-b140-4859-b594-6a5ac389c07f",
    "name": "test-selector-pr12515",
    "type": "TEMPLATE",
    "zoneid": "6cf85da0-c3e7-44ef-9da5-dc7383b23996"
  }
}

2. Execution-time error with precise syntax details:

(localcloud) 🐱 > register template name=test-bad-syntax displaytext="broken rule test" url=http://10.0.3.122/vladitemplates/qcow2/linux-debian-12-x86_64-gen2-v1.qcow2 format=QCOW2 hypervisor=KVM ostypeid=7c619234-072b-11f1-b31f-1e00c00002b4 zoneid=6cf85da0-c3e7-44ef-9da5-dc7383b23996
🙈 Error: (HTTP 530, error code 4250) Unable to execute script [secondaryStorages.get(0).id +]
due to [Script error: <eval>:1:29 Expected an operand but found eof
secondaryStorages.get(0).id +
                             ^ in <eval> at line number 1 at column number 29]

Note: CloudStack validates JS rules at execution time, not at save time. This means invalid rules can be stored but will fail when triggered.

TC8: Negative - JS Interpretation Disabled (CVE Kill Switch)

Objective:
Verify that when js.interpretation.enabled=false, the CVE-2025-59302 kill switch prevents creation of new JS rules. Also document the behavior for execution of pre-existing rules.

Test Steps:

  1. Encrypt value false using EncryptionCLI with the management server key
  2. Update js.interpretation.enabled directly in DB with encrypted value
  3. Restart management server
  4. Attempt to create a new quota activation rule (Test A)
  5. Attempt to register a template with existing heuristic rule active (Test B)
  6. Check management server logs for JS execution traces
  7. Re-enable JS interpretation and restart

Expected Result:

  • Rule creation blocked with explicit error
  • Rule execution behavior documented (may skip or still execute existing rules)

Actual Result: PASSED (with documented observation)

  • Rule creation: BLOCKED - quota tariffupdate rejected
  • Rule execution: NOT BLOCKED - existing heuristic rule still executed (by design)

Test Evidence:

1. Encrypt and set value=false:

[root@mgmt1 ~]# cat /etc/cloudstack/management/key
password

[root@mgmt1 ~]# java -classpath /usr/share/cloudstack-common/lib/cloudstack-utils.jar com.cloud.utils.crypt.EncryptionCLI -p password -i false
nnWcEv2DhIf/v2Bmu2wc9Uo4wGoS/zvCQPyAVcyXPaYu

mysql> UPDATE cloud.configuration SET value='nnWcEv2DhIf/v2Bmu2wc9Uo4wGoS/zvCQPyAVcyXPaYu' WHERE name='js.interpretation.enabled';
Query OK, 1 row affected (0.00 sec)

2. Test A - Rule creation blocked:

(localcloud) 🐱 > quota tariffupdate name=RUNNING_VM activationrule="account.name == 'admin' ? 2.0 : 1.0"
🙈 Error: (HTTP 531, error code 4365) Quota Tariff Activation Rule cannot be set,
as Javascript interpretation is disabled in the configuration.

3. Test B - Existing rule still executes:

(localcloud) 🐱 > register template name=test-js-disabled-v2 displaytext="JS disabled test v2" ...
{
  "template": [{
    "name": "test-js-disabled-v2",
    "downloaddetails": [{
      "datastoreId": "7c3a1511-4f6e-4531-90f0-a6f9f5e1652e",
      "datastore": "NFS://...sec1"
    }]
  }]
}

4. Logs confirm JS executed despite flag being false:

2026-02-11 12:37:38,940 INFO  [c.c.a.ApiServer] PermissionDenied: Quota Tariff Activation Rule cannot be set,
as Javascript interpretation is disabled in the configuration.

2026-02-11 12:37:57,308 DEBUG [o.a.c.s.h.HeuristicRuleHelper] Found the heuristic rule Heuristic
{"heuristicRule":"secondaryStorages.get(0).id"...} to apply for zone [...]
2026-02-11 12:37:57,315 DEBUG [o.a.c.u.j.JsInterpreter] Executing script [secondaryStorages.get(0).id].
2026-02-11 12:37:57,327 DEBUG [o.a.c.u.j.JsInterpreter] The script [secondaryStorages.get(0).id]
had the following result: [7c3a1511-4f6e-4531-90f0-a6f9f5e1652e].

5. Key observation: The js.interpretation.enabled kill switch blocks creation and update of JS rules (quota tariffs, selectors, host tags) but does NOT block execution of pre-existing rules. This is likely by design - it prevents introduction of new JS code while avoiding disruption to existing production rules.

6. Important note on encrypted configuration: The js.interpretation.enabled value is stored encrypted in the database. Setting it via the CloudStack API (update configuration) causes double-encryption. The correct procedure is:

# Get encryption key
cat /etc/cloudstack/management/key

# Encrypt the desired value
java -classpath /usr/share/cloudstack-common/lib/cloudstack-utils.jar \
  com.cloud.utils.crypt.EncryptionCLI -p <key> -i <true|false>

# Update DB directly with encrypted value
mysql -u root cloud -e "UPDATE configuration SET value='<encrypted>' WHERE name='js.interpretation.enabled';"

# Restart management server (setting is not dynamic)
systemctl restart cloudstack-management

TC5: Flexible Host Tags - ArrayList Injection

Objective:
Verify that host tags are injected into the JS interpreter as a proper Java ArrayList<String> (not a comma-separated string), enabling .contains() method calls in tag-as-rule expressions.

Test Steps:

  1. Set host tags gpu,compute on kvm1 as regular tags
  2. Convert kvm1 tag to a JS rule: tags.contains('gpu') with istagarule=true
  3. Set kvm2 tag to a non-matching rule: tags.contains('storage') with istagarule=true
  4. Create service offering with hosttag=gpu
  5. Deploy VM - should land on kvm1 (positive match)
  6. Deploy second VM - should also land on kvm1 (kvm2 rule returns false)
  7. Verify JS execution in management server logs

Expected Result:

  • kvm1 rule tags.contains('gpu') returns true for gpu-tagged offering
  • kvm2 rule tags.contains('storage') returns false for gpu-tagged offering
  • Both VMs deploy on kvm1

Actual Result: PASSED

  • VM1 deployed on kvm1
  • VM2 deployed on kvm1
  • Logs confirm .contains() executed on both hosts with correct boolean results

Test Evidence:

1. kvm1 tag rule set:

(localcloud) 🐱 > update host id=3c41a063-0dee-4211-a38b-0c5bc135f538 hosttags="tags.contains('gpu')" istagarule=true
{
  "host": {
    "hosttags": "tags.contains('gpu')",
    "istagarule": true,
    "name": "ref-trl-10970-k-Mol9-rositsa-kyuchukova-kvm1",
    "id": "3c41a063-0dee-4211-a38b-0c5bc135f538"
  }
}

2. kvm2 tag rule set (non-matching):

(localcloud) 🐱 > update host id=42852123-7138-40a3-9541-f047b233c0d7 hosttags="tags.contains('storage')" istagarule=true
{
  "host": {
    "hosttags": "tags.contains('storage')",
    "istagarule": true,
    "name": "ref-trl-10970-k-Mol9-rositsa-kyuchukova-kvm2",
    "id": "42852123-7138-40a3-9541-f047b233c0d7"
  }
}

3. Service offering with gpu host tag:

(localcloud) 🐱 > create serviceoffering name=gpu-offering displaytext="GPU tagged offering" cpunumber=1 cpuspeed=500 memory=256 hosttags=gpu
{
  "serviceoffering": {
    "hosttags": "gpu",
    "id": "03fad0a3-0204-4416-b5b0-28eab83296e8",
    "name": "gpu-offering"
  }
}

4. VM1 deployed on kvm1:

(localcloud) 🐱 > deploy virtualmachine serviceofferingid=03fad0a3-0204-4416-b5b0-28eab83296e8 templateid=59265a47-072b-11f1-b31f-1e00c00002b4 zoneid=6cf85da0-c3e7-44ef-9da5-dc7383b23996 name=test-hosttag-vm
{
  "virtualmachine": {
    "hostid": "3c41a063-0dee-4211-a38b-0c5bc135f538",
    "hostname": "ref-trl-10970-k-Mol9-rositsa-kyuchukova-kvm1",
    "name": "test-hosttag-vm",
    "state": "Running"
  }
}

5. VM1 logs - kvm1 rule evaluated, returned true:

2026-02-11 13:13:01,167 DEBUG [o.a.c.u.j.JsInterpreter] Executing script [tags.contains('gpu')].
2026-02-11 13:13:01,179 DEBUG [o.a.c.u.j.JsInterpreter] The script [tags.contains('gpu')] had the following result: [true].

6. VM2 deployed on kvm1:

(localcloud) 🐱 > deploy virtualmachine serviceofferingid=03fad0a3-0204-4416-b5b0-28eab83296e8 templateid=59265a47-072b-11f1-b31f-1e00c00002b4 zoneid=6cf85da0-c3e7-44ef-9da5-dc7383b23996 name=test-hosttag-vm2
{
  "virtualmachine": {
    "hostid": "3c41a063-0dee-4211-a38b-0c5bc135f538",
    "hostname": "ref-trl-10970-k-Mol9-rositsa-kyuchukova-kvm1",
    "name": "test-hosttag-vm2",
    "state": "Running"
  }
}

7. VM2 logs - both hosts evaluated, only kvm1 matches:

2026-02-11 13:14:58,198 DEBUG [o.a.c.u.j.JsInterpreter] Executing script [tags.contains('gpu')].
2026-02-11 13:14:58,205 DEBUG [o.a.c.u.j.JsInterpreter] The script [tags.contains('gpu')] had the following result: [true].
2026-02-11 13:14:58,208 DEBUG [o.a.c.u.j.JsInterpreter] Executing script [tags.contains('storage')].
2026-02-11 13:14:58,215 DEBUG [o.a.c.u.j.JsInterpreter] The script [tags.contains('storage')] had the following result: [false].

8. Comparison:

Host Tag Rule Offering Tag Result VM Placed
kvm1 tags.contains('gpu') gpu true Yes
kvm2 tags.contains('storage') gpu false No

Key observation: The tags variable is injected as a proper Java ArrayList, confirmed by the successful execution of .contains(). If tags were a comma-separated String (the CVE regression), .contains('gpu') would use String.contains() which would also return true but with different semantics - it would match substrings (e.g., tags.contains('gp') would incorrectly return true). The ArrayList .contains() performs exact element matching, which is the correct behavior for tag evaluation.

TC6: Flexible Host Tags - Multiple Tags in ArrayList

Objective:
Verify that host tag rules can use compound boolean expressions with multiple .contains() calls on the same tags ArrayList, confirming the ArrayList supports repeated method invocation within a single JS expression.

Test Steps:

  1. Update kvm1 tag rule to: tags.contains('gpu') && tags.contains('compute') with istagarule=true
  2. kvm2 remains with: tags.contains('storage') with istagarule=true
  3. Create service offering with hosttags=gpu,compute
  4. Deploy VM - should land on kvm1 (both tags match)
  5. Verify JS execution in management server logs

Expected Result:

  • kvm1 rule evaluates both .contains() calls, both return true, && yields true
  • kvm2 rule returns false
  • VM deploys on kvm1

Actual Result: PASSED

  • VM deployed on kvm1
  • Logs confirm compound expression evaluated with correct boolean logic

Test Evidence:

1. kvm1 compound tag rule:

(localcloud) 🐱 > update host id=3c41a063-0dee-4211-a38b-0c5bc135f538 hosttags="tags.contains('gpu') && tags.contains('compute')" istagarule=true
{
  "host": {
    "hosttags": "tags.contains('gpu') && tags.contains('compute')",
    "istagarule": true,
    "name": "ref-trl-10970-k-Mol9-rositsa-kyuchukova-kvm1",
    "id": "3c41a063-0dee-4211-a38b-0c5bc135f538"
  }
}

2. Service offering with two host tags:

(localcloud) 🐱 > create serviceoffering name=gpu-compute-offering displaytext="GPU+Compute offering" cpunumber=1 cpuspeed=500 memory=256 hosttags="gpu,compute"
{
  "serviceoffering": {
    "hosttags": "gpu,compute",
    "id": "9d7513ba-534c-43b5-a5ce-ffe7b8bc8074",
    "name": "gpu-compute-offering"
  }
}

3. VM deployed on kvm1:

(localcloud) 🐱 > deploy virtualmachine serviceofferingid=9d7513ba-534c-43b5-a5ce-ffe7b8bc8074 templateid=59265a47-072b-11f1-b31f-1e00c00002b4 zoneid=6cf85da0-c3e7-44ef-9da5-dc7383b23996 name=test-multitag-vm
{
  "virtualmachine": {
    "hostid": "3c41a063-0dee-4211-a38b-0c5bc135f538",
    "hostname": "ref-trl-10970-k-Mol9-rositsa-kyuchukova-kvm1",
    "name": "test-multitag-vm",
    "serviceofferingname": "gpu-compute-offering",
    "state": "Running"
  }
}

4. Logs - both hosts evaluated, compound expression works:

2026-02-11 13:19:55,105 DEBUG [o.a.c.u.j.JsInterpreter] Executing script [tags.contains('storage')].
2026-02-11 13:19:55,114 DEBUG [o.a.c.u.j.JsInterpreter] The script [tags.contains('storage')] had the following result: [false].
2026-02-11 13:19:55,115 DEBUG [o.a.c.u.j.JsInterpreter] Executing script [tags.contains('gpu') && tags.contains('compute')].
2026-02-11 13:19:55,132 DEBUG [o.a.c.u.j.JsInterpreter] The script [tags.contains('gpu') && tags.contains('compute')] had the following result: [true].

5. Comparison:

Host Tag Rule Offering Tags Result VM Placed
kvm2 tags.contains('storage') gpu,compute false No
kvm1 tags.contains('gpu') && tags.contains('compute') gpu,compute true Yes

TC3: Quota Activation Rule - Enum String Access (account.role.type)

Objective:
Verify that Java enum fields are converted to String values at injection time, allowing direct string comparison in activation rules. account.role.type is a RoleType enum (Admin, DomainAdmin, ResourceAdmin, User) that must resolve to the string "Admin"

Test Steps:

  1. Reset quota_calculated and delete previous quota_usage for account_id=2
  2. Set activation rule: account.role.type == 'Admin' ? 3.0 : 1.0
  3. Trigger usage cycle (set usage.stats.job.exec.time, restart cloudstack-usage)
  4. Verify quota_used reflects multiplier 3.0

Expected Result:

  • account.role.type resolves to 'Admin', rule returns 3.0
  • quota_used = 23/672 × 3.0 = 0.102678...

Actual Result: PASSED

  • quota_used = 0.10267867

Test Evidence:

1. Reset and set activation rule:

mysql> UPDATE cloud_usage.cloud_usage SET quota_calculated = 0 WHERE id = 1;
Query OK, 1 row affected (0.00 sec)

mysql> DELETE FROM cloud_usage.quota_usage WHERE account_id = 2;
Query OK, 1 row affected (0.00 sec)
(localcloud) 🐱 > quota tariffupdate name=RUNNING_VM activationrule="account.role.type == 'Admin' ? 3.0 : 1.0"
{
  "quotatariff": {
    "activationRule": "account.role.type == 'Admin' ? 3.0 : 1.0",
    "id": "7951b87d-4357-4faa-b007-2d4c88006925",
    "name": "RUNNING_VM",
    "tariffValue": 1,
    "usageType": 1
  }
}

2. Quota usage result after cycle at 13:35:

mysql> SELECT * FROM cloud_usage.quota_usage WHERE account_id = 2;
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+
| id | usage_item_id | zone_id | account_id | domain_id | usage_type | quota_used | start_date          | end_date            |
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+
|  5 |             1 |       1 |          2 |         1 | 1          | 0.10267867 | 2026-02-10 00:00:00 | 2026-02-10 23:59:59 |
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+

**quota_used = 0.10267867 → 23/672 × 3.0 = 0.102678... **

3. Calculation verification:

  • Feb 2026 = 28 days = 672 hours
  • 23 hours / 672 hours = 0.034226...
  • 0.034226 × 3.0 = 0.102678...

TC4: Quota Activation Rule - Domain Object Access (domain.name)

Objective:
Verify that the domain preset variable is injected as a proper Java object with accessible properties, allowing domain.name to be used in activation rules for domain-based billing logic.

Test Steps:

  1. Reset quota_calculated and delete previous quota_usage for account_id=2
  2. Set activation rule: domain.name == 'ROOT' ? 4.0 : 1.0
  3. Trigger usage cycle (set usage.stats.job.exec.time, restart cloudstack-usage)
  4. Verify quota_used reflects multiplier 4.0

Expected Result:

  • domain.name resolves to 'ROOT', rule returns 4.0
  • quota_used = 23/672 × 4.0 = 0.136904...

Actual Result: PASSED

  • quota_used = 0.13690474

Test Evidence:

1. Reset and set activation rule:

mysql> UPDATE cloud_usage.cloud_usage SET quota_calculated = 0 WHERE id = 1;
Query OK, 1 row affected (0.00 sec)

mysql> DELETE FROM cloud_usage.quota_usage WHERE account_id = 2;
Query OK, 1 row affected (0.01 sec)
(localcloud) 🐱 > quota tariffupdate name=RUNNING_VM activationrule="domain.name == 'ROOT' ? 4.0 : 1.0"
{
  "quotatariff": {
    "activationRule": "domain.name == 'ROOT' ? 4.0 : 1.0",
    "id": "c9226acf-34ae-4c9a-9332-aceae8e546b9",
    "name": "RUNNING_VM",
    "tariffValue": 1,
    "usageType": 1
  }
}

2. Quota usage result after cycle at 13:45:

mysql> SELECT * FROM cloud_usage.quota_usage WHERE account_id = 2;
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+
| id | usage_item_id | zone_id | account_id | domain_id | usage_type | quota_used | start_date          | end_date            |
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+
|  6 |             1 |       1 |          2 |         1 | 1          | 0.13690474 | 2026-02-10 00:00:00 | 2026-02-10 23:59:59 |
+----+---------------+---------+------------+-----------+------------+------------+---------------------+---------------------+

**quota_used = 0.13690474 → 23/672 × 4.0 = 0.136904... **

3. Calculation verification:

  • Feb 2026 = 28 days = 672 hours
  • 23 hours / 672 hours = 0.034226...
  • 0.034226 × 4.0 = 0.136904...

4. All quota activation rule tests comparison:

TC Activation Rule Multiplier Expected Actual Match
TC1+ account.name == 'admin' ? 2.0 : 1.0 2.0 0.068452 0.06845237 Match
TC1- account.name == 'nonexistent' ? 2.0 : 1.0 1.0 0.034226 0.03422630 Match
TC3 account.role.type == 'Admin' ? 3.0 : 1.0 3.0 0.102678 0.10267867 Match
TC4 domain.name == 'ROOT' ? 4.0 : 1.0 4.0 0.136904 0.13690474 Match

@winterhazel
Copy link
Member Author

@RosiKyu many thanks for your extensive tests! We can open an issue to address your first observation (rule validation before storing it). The second one (disabling the interpretation of existing rules as well) I think requires further discussion, as you pointed out that it may disrupt existing production behavior. The third one (unencrypt the configuration and make it dynamic) is being addressed at #12605.

@DaanHoogland I think we can merge this one already.

@DaanHoogland DaanHoogland merged commit 34f6f41 into 4.20 Feb 11, 2026
48 of 50 checks passed
@DaanHoogland DaanHoogland deleted the fix-preset-variable-injection branch February 11, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants