Fix injection of preset variables into the JS interpreter#12515
Fix injection of preset variables into the JS interpreter#12515DaanHoogland merged 4 commits into4.20from
Conversation
Codecov Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@blueorangutan package |
|
@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. |
|
Packaging result [SF]: ✖️ el8 ✖️ el9 ✖️ debian ✖️ suse15. SL-JID 16519 |
|
@blueorangutan package |
|
@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. |
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 16520 |
|
@blueorangutan test |
|
@DaanHoogland a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests |
|
@blueorangutan test |
There was a problem hiding this comment.
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.
utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java
Show resolved
Hide resolved
shwstppr
left a comment
There was a problem hiding this comment.
code lgtm. Copilot highlighted a logging issue
utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java
Show resolved
Hide resolved
|
[SF] Trillian test result (tid-15283)
|
|
@blueorangutan package |
|
@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. |
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 16566 |
|
This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch. |
|
@blueorangutan package |
|
@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. |
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 16597 |
|
|
@blueorangutan test |
|
@DaanHoogland a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests |
|
[SF] Trillian test result (tid-15325)
|
There was a problem hiding this comment.
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):
-
No save-time JS validation (from PR #7659) -
CreateSecondaryStorageSelectorCmdandUpdateSecondaryStorageSelectorCmdstore the heuristic rule as a plain string with no syntax validation. Invalid JS is only caught at execution time (e.g. during template registration). -
Kill switch does not block execution of existing rules (from commit 03a4b9f) -
js.interpretation.enabled=falseblocks API entry points only (command registration, quota tariff creation, host/storage tag-as-rule updates). However, the execution path (HeuristicRuleHelper→JsInterpreter) has no awareness of this flag, so pre-existing rules in the DB still execute. -
Encrypted configuration requires direct DB update (from commit 03a4b9f) -
js.interpretation.enabledis defined with"Hidden"category, causing encrypted storage. Setting it via the API causes double-encryption. Must be encrypted viaEncryptionCLIand 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:
- Enable JS interpretation and quota service, restart management server
- Set RUNNING_VM tariff value=1.0 with activation rule:
account.name == 'admin' ? 2.0 : 1.0 - Verify preset variables structure via
quota presetvariableslist - Insert manual usage record (23 hrs RUNNING_VM, Feb 10) into cloud_usage
- Seed quota_account entry, set usage.stats.job.exec.time, restart cloudstack-usage
- Verify positive case: quota_used reflects multiplier 2.0
- Update activation rule to:
account.name == 'nonexistent' ? 2.0 : 1.0 - Reset quota_calculated=0, delete quota_usage, trigger new cycle
- Verify negative case: quota_used reflects multiplier 1.0
Expected Result:
- Positive case:
account.nameresolves to'admin', rule returns 2.0, quota_used ≈ 0.06845 (23/672 × 2.0) - Negative case:
account.nameresolves 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:
- Create a secondary storage selector with heuristic rule for TEMPLATE type
- Set rule to
secondaryStorages.get(0).id- selects first store - Register template and verify it lands on sec1
- Update rule to
secondaryStorages.get(1).id- selects second store - Register second template and verify it lands on sec2
- 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.idon 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:
- Update existing secondary storage selector with broken JS:
secondaryStorages.get(0).id +(trailing operator, no operand) - Attempt to register a template to trigger rule execution
- 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:
- Encrypt value
falseusing EncryptionCLI with the management server key - Update
js.interpretation.enableddirectly in DB with encrypted value - Restart management server
- Attempt to create a new quota activation rule (Test A)
- Attempt to register a template with existing heuristic rule active (Test B)
- Check management server logs for JS execution traces
- 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-managementTC5: 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:
- Set host tags
gpu,computeon kvm1 as regular tags - Convert kvm1 tag to a JS rule:
tags.contains('gpu')withistagarule=true - Set kvm2 tag to a non-matching rule:
tags.contains('storage')withistagarule=true - Create service offering with hosttag=gpu
- Deploy VM - should land on kvm1 (positive match)
- Deploy second VM - should also land on kvm1 (kvm2 rule returns false)
- Verify JS execution in management server logs
Expected Result:
- kvm1 rule
tags.contains('gpu')returnstruefor gpu-tagged offering - kvm2 rule
tags.contains('storage')returnsfalsefor 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:
- Update kvm1 tag rule to:
tags.contains('gpu') && tags.contains('compute')withistagarule=true - kvm2 remains with:
tags.contains('storage')withistagarule=true - Create service offering with hosttags=
gpu,compute - Deploy VM - should land on kvm1 (both tags match)
- Verify JS execution in management server logs
Expected Result:
- kvm1 rule evaluates both
.contains()calls, both returntrue,&&yieldstrue - 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:
- Reset quota_calculated and delete previous quota_usage for account_id=2
- Set activation rule:
account.role.type == 'Admin' ? 3.0 : 1.0 - Trigger usage cycle (set usage.stats.job.exec.time, restart cloudstack-usage)
- Verify quota_used reflects multiplier 3.0
Expected Result:
account.role.typeresolves 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:
- Reset quota_calculated and delete previous quota_usage for account_id=2
- Set activation rule:
domain.name == 'ROOT' ? 4.0 : 1.0 - Trigger usage cycle (set usage.stats.job.exec.time, restart cloudstack-usage)
- Verify quota_used reflects multiplier 4.0
Expected Result:
domain.nameresolves 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 |
|
@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. |



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
Feature/Enhancement Scale or Bug Severity
Bug Severity
How Has This Been Tested?
org.apache.cloudstack.utils.jsinterpreter.JsInterpreter#executeScriptInThreadto validate that the preset variables had their expected type in the JS interpreter.org.apache.cloudstack.utils.jsinterpreter.JsInterpreter#executeScriptInThreadto validate that the preset variables had their expected type in the JS interpreter.org.apache.cloudstack.utils.jsinterpreter.JsInterpreter#executeScriptInThreadto validate that the offering's host tags were correctly injected into the JS interpreter as a list.