From a7f9756d6267869b96a9209af58c0335835a773d Mon Sep 17 00:00:00 2001 From: Vishesh <8760112+vishesh92@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:39:39 +0530 Subject: [PATCH 1/4] Remove realhostip references from the code (#12856) * Remove realhostip references from the code * remove unused code --- .pre-commit-config.yaml | 2 +- .../META-INF/db/schema-42210to42300.sql | 6 ++++++ scripts/util/keystore-cert-import | 19 +++++++++--------- .../cloud/server/ConfigurationServerImpl.java | 3 --- .../storage/download/DownloadMonitorImpl.java | 5 ----- .../storage/upload/UploadMonitorImpl.java | 13 +++++------- .../java/com/cloud/keystore/KeystoreTest.java | 12 +++++------ services/console-proxy/server/pom.xml | 2 +- services/secondary-storage/server/pom.xml | 2 +- .../certs/{realhostip.crt => systemvm.crt} | 0 .../certs/{realhostip.csr => systemvm.csr} | 0 .../certs/{realhostip.key => systemvm.key} | 0 ...{realhostip.keystore => systemvm.keystore} | Bin systemvm/agent/scripts/_run.sh | 2 +- systemvm/agent/scripts/config_ssl.sh | 6 +++--- .../debian/opt/cloud/bin/setup/bootstrap.sh | 2 +- systemvm/patch-sysvms.sh | 18 ++++++++--------- systemvm/pom.xml | 2 +- .../utils/imagestore/ImageStoreUtilTest.java | 4 ++-- 19 files changed, 47 insertions(+), 51 deletions(-) rename systemvm/agent/certs/{realhostip.crt => systemvm.crt} (100%) rename systemvm/agent/certs/{realhostip.csr => systemvm.csr} (100%) rename systemvm/agent/certs/{realhostip.key => systemvm.key} (100%) rename systemvm/agent/certs/{realhostip.keystore => systemvm.keystore} (100%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 755ae125edf..91537e25267 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -151,7 +151,7 @@ repos: ^server/src/test/resources/certs/rsa_self_signed\.key$| ^services/console-proxy/rdpconsole/src/test/doc/rdp-key\.pem$| ^systemvm/agent/certs/localhost\.key$| - ^systemvm/agent/certs/realhostip\.key$| + ^systemvm/agent/certs/systemvm\.key$| ^test/integration/smoke/test_ssl_offloading\.py$ - id: end-of-file-fixer exclude: \.vhd$|\.svg$ diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 478125335e7..d999cbcd509 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -150,6 +150,12 @@ FROM `cloud`.`configuration` `cfg` WHERE NOT EXISTS (SELECT 1 FROM `cloud`.`configuration` WHERE `name` = 'kvm.cpu.dynamic.scaling.capacity') AND `cfg`.`name` = 'vm.serviceoffering.cpu.cores.max'; +-- Remove stale realhostip.com default values; domain has been dead since ~2015. +UPDATE `cloud`.`configuration` + SET value = NULL + WHERE name IN ('consoleproxy.url.domain', 'secstorage.ssl.cert.domain') + AND value IN ('realhostip.com', '*.realhostip.com'); + -- Add management_server_details table to allow ManagementServer scope configs CREATE TABLE IF NOT EXISTS `management_server_details` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', diff --git a/scripts/util/keystore-cert-import b/scripts/util/keystore-cert-import index cf355e09845..447dcd71745 100755 --- a/scripts/util/keystore-cert-import +++ b/scripts/util/keystore-cert-import @@ -137,18 +137,19 @@ if [ -f "$SYSTEM_FILE" ]; then chmod 644 /usr/local/share/ca-certificates/cloudstack/ca.crt update-ca-certificates > /dev/null 2>&1 || true - # Import CA cert(s) into realhostip.keystore so the SSVM JVM - # (which overrides the truststore via -Djavax.net.ssl.trustStore in _run.sh) - # can trust servers signed by the CloudStack CA - REALHOSTIP_KS_FILE="$(dirname "$(dirname "$PROPS_FILE")")/certs/realhostip.keystore" - REALHOSTIP_PASS="vmops.com" - if [ -f "$REALHOSTIP_KS_FILE" ]; then + # Also import CA cert(s) into systemvm.keystore. KS_FILE (cloud.jks) above + # is the agent's mTLS keystore; the SSVM JVM, however, reads its truststore + # from systemvm.keystore (see -Djavax.net.ssl.trustStore in _run.sh), so the + # CA must be added here too for the SSVM to trust CloudStack-CA-signed servers. + SYSTEMVM_KS_FILE="$(dirname "$(dirname "$PROPS_FILE")")/certs/systemvm.keystore" + SYSTEMVM_PASS="vmops.com" + if [ -f "$SYSTEMVM_KS_FILE" ]; then awk 'BEGIN{n=0} /-----BEGIN CERTIFICATE-----/{n++} n>0{print > "cloudca." n }' "$CACERT_FILE" for caChain in $(ls cloudca.* 2>/dev/null); do - keytool -delete -noprompt -alias "$caChain" -keystore "$REALHOSTIP_KS_FILE" \ - -storepass "$REALHOSTIP_PASS" > /dev/null 2>&1 || true + keytool -delete -noprompt -alias "$caChain" -keystore "$SYSTEMVM_KS_FILE" \ + -storepass "$SYSTEMVM_PASS" > /dev/null 2>&1 || true keytool -import -noprompt -trustcacerts -alias "$caChain" -file "$caChain" \ - -keystore "$REALHOSTIP_KS_FILE" -storepass "$REALHOSTIP_PASS" > /dev/null 2>&1 + -keystore "$SYSTEMVM_KS_FILE" -storepass "$SYSTEMVM_PASS" > /dev/null 2>&1 done rm -f cloudca.* fi diff --git a/server/src/main/java/com/cloud/server/ConfigurationServerImpl.java b/server/src/main/java/com/cloud/server/ConfigurationServerImpl.java index 8f10dd84b54..def564dfdc6 100644 --- a/server/src/main/java/com/cloud/server/ConfigurationServerImpl.java +++ b/server/src/main/java/com/cloud/server/ConfigurationServerImpl.java @@ -222,9 +222,6 @@ public class ConfigurationServerImpl extends ManagerBase implements Configuratio _configDao.update(Config.SecStorageEncryptCopy.key(), Config.SecStorageEncryptCopy.getCategory(), "false"); logger.debug("ConfigurationServer made secondary storage copy encrypt set to false."); - _configDao.update("secstorage.secure.copy.cert", "realhostip"); - logger.debug("ConfigurationServer made secondary storage copy use realhostip."); - _configDao.update("user.password.encoders.exclude", "MD5,LDAP,PLAINTEXT"); logger.debug("Configuration server excluded insecure encoders"); diff --git a/server/src/main/java/com/cloud/storage/download/DownloadMonitorImpl.java b/server/src/main/java/com/cloud/storage/download/DownloadMonitorImpl.java index 67d5b091a03..5258f433755 100644 --- a/server/src/main/java/com/cloud/storage/download/DownloadMonitorImpl.java +++ b/server/src/main/java/com/cloud/storage/download/DownloadMonitorImpl.java @@ -87,11 +87,6 @@ public class DownloadMonitorImpl extends ManagerBase implements DownloadMonitor final Map configs = _configDao.getConfiguration("management-server", params); _proxy = configs.get(Config.SecStorageProxy.key()); - String cert = configs.get("secstorage.ssl.cert.domain"); - if (!"realhostip.com".equalsIgnoreCase(cert)) { - logger.warn("Only realhostip.com ssl cert is supported, ignoring self-signed and other certs"); - } - _copyAuthPasswd = configs.get("secstorage.copy.password"); DownloadListener dl = new DownloadListener(this); diff --git a/server/src/main/java/com/cloud/storage/upload/UploadMonitorImpl.java b/server/src/main/java/com/cloud/storage/upload/UploadMonitorImpl.java index 7962d9dced9..a32790c135d 100644 --- a/server/src/main/java/com/cloud/storage/upload/UploadMonitorImpl.java +++ b/server/src/main/java/com/cloud/storage/upload/UploadMonitorImpl.java @@ -231,8 +231,8 @@ public class UploadMonitorImpl extends ManagerBase implements UploadMonitor { UploadVO upload = extractURLList.get(0); String uploadUrl = extractURLList.get(0).getUploadUrl(); String[] token = uploadUrl.split("/"); - // example: uploadUrl = https://10-11-101-112.realhostip.com/userdata/2fdd9a70-9c4a-4a04-b1d5-1e41c221a1f9.iso - // then token[2] = 10-11-101-112.realhostip.com, token[4] = 2fdd9a70-9c4a-4a04-b1d5-1e41c221a1f9.iso + // example: uploadUrl = https://10-11-101-112.example.com/userdata/2fdd9a70-9c4a-4a04-b1d5-1e41c221a1f9.iso + // then token[2] = 10-11-101-112.example.com, token[4] = 2fdd9a70-9c4a-4a04-b1d5-1e41c221a1f9.iso String hostname = ep.getPublicAddr().replace(".", "-") + "."; if ((token != null) && (token.length == 5) && (token[2].equals(hostname + _ssvmUrlDomain))) // ssvm publicip and domain suffix not changed return extractURLList.get(0); @@ -365,7 +365,9 @@ public class UploadMonitorImpl extends ManagerBase implements UploadMonitor { if (_ssvmUrlDomain != null && _ssvmUrlDomain.length() > 0) { hostname = hostname + "." + _ssvmUrlDomain; } else { - hostname = hostname + ".realhostip.com"; + logger.warn("SSL copy is enabled but secstorage.ssl.cert.domain is not configured; " + + "using IP address directly. Configure a wildcard SSL certificate domain for proper HTTPS support."); + hostname = ipAddress; } } return scheme + "://" + hostname + "/userdata/" + uuid; @@ -376,11 +378,6 @@ public class UploadMonitorImpl extends ManagerBase implements UploadMonitor { final Map configs = _configDao.getConfiguration("management-server", params); _sslCopy = Boolean.parseBoolean(configs.get("secstorage.encrypt.copy")); - String cert = configs.get("secstorage.secure.copy.cert"); - if ("realhostip.com".equalsIgnoreCase(cert)) { - logger.warn("Only realhostip.com ssl cert is supported, ignoring self-signed and other certs"); - } - _ssvmUrlDomain = configs.get("secstorage.ssl.cert.domain"); _agentMgr.registerForHostEvents(new UploadListener(this), true, false, false); diff --git a/server/src/test/java/com/cloud/keystore/KeystoreTest.java b/server/src/test/java/com/cloud/keystore/KeystoreTest.java index 970892dc325..2b991d4a4f7 100644 --- a/server/src/test/java/com/cloud/keystore/KeystoreTest.java +++ b/server/src/test/java/com/cloud/keystore/KeystoreTest.java @@ -75,20 +75,20 @@ public class KeystoreTest extends TestCase { ComponentLocator locator = ComponentLocator.getCurrentLocator(); KeystoreDao ksDao = locator.getDao(KeystoreDao.class); - ksDao.save("CPVMCertificate", "CPVMCertificate", "KeyForCertificate", "realhostip.com"); + ksDao.save("CPVMCertificate", "CPVMCertificate", "KeyForCertificate", "example.com"); ksVo = ksDao.findByName("CPVMCertificate"); assertTrue(ksVo != null); assertTrue(ksVo.getCertificate().equals("CPVMCertificate")); assertTrue(ksVo.getKey().equals("KeyForCertificate")); - assertTrue(ksVo.getDomainSuffix().equals("realhostip.com")); + assertTrue(ksVo.getDomainSuffix().equals("example.com")); - ksDao.save("CPVMCertificate", "CPVMCertificate Again", "KeyForCertificate Again", "again.realhostip.com"); + ksDao.save("CPVMCertificate", "CPVMCertificate Again", "KeyForCertificate Again", "again.example.com"); ksVo = ksDao.findByName("CPVMCertificate"); assertTrue(ksVo != null); assertTrue(ksVo.getCertificate().equals("CPVMCertificate Again")); assertTrue(ksVo.getKey().equals("KeyForCertificate Again")); - assertTrue(ksVo.getDomainSuffix().equals("again.realhostip.com")); + assertTrue(ksVo.getDomainSuffix().equals("again.example.com")); ksDao.expunge(ksVo.getId()); } @@ -112,9 +112,9 @@ public class KeystoreTest extends TestCase { assertTrue(ksMgr.configure("TaskManager", new HashMap())); assertTrue(ksMgr.start()); - ksMgr.saveCertificate("CPVMCertificate", certContent, keyContent, "realhostip.com"); + ksMgr.saveCertificate("CPVMCertificate", certContent, keyContent, "example.com"); - byte[] ksBits = ksMgr.getKeystoreBits("CPVMCertificate", "realhostip", "vmops.com"); + byte[] ksBits = ksMgr.getKeystoreBits("CPVMCertificate", "example", "vmops.com"); assertTrue(ksBits != null); try { diff --git a/services/console-proxy/server/pom.xml b/services/console-proxy/server/pom.xml index 3f5b9db68c2..6e18a13bc57 100644 --- a/services/console-proxy/server/pom.xml +++ b/services/console-proxy/server/pom.xml @@ -80,7 +80,7 @@ certs - realhostip.csr + systemvm.csr diff --git a/services/secondary-storage/server/pom.xml b/services/secondary-storage/server/pom.xml index e6aec8a42f7..ca26bf2bbbe 100644 --- a/services/secondary-storage/server/pom.xml +++ b/services/secondary-storage/server/pom.xml @@ -107,7 +107,7 @@ javax.net.ssl.trustStore - certs/realhostip.keystore + certs/systemvm.keystore log.home ${PWD}/ diff --git a/systemvm/agent/certs/realhostip.crt b/systemvm/agent/certs/systemvm.crt similarity index 100% rename from systemvm/agent/certs/realhostip.crt rename to systemvm/agent/certs/systemvm.crt diff --git a/systemvm/agent/certs/realhostip.csr b/systemvm/agent/certs/systemvm.csr similarity index 100% rename from systemvm/agent/certs/realhostip.csr rename to systemvm/agent/certs/systemvm.csr diff --git a/systemvm/agent/certs/realhostip.key b/systemvm/agent/certs/systemvm.key similarity index 100% rename from systemvm/agent/certs/realhostip.key rename to systemvm/agent/certs/systemvm.key diff --git a/systemvm/agent/certs/realhostip.keystore b/systemvm/agent/certs/systemvm.keystore similarity index 100% rename from systemvm/agent/certs/realhostip.keystore rename to systemvm/agent/certs/systemvm.keystore diff --git a/systemvm/agent/scripts/_run.sh b/systemvm/agent/scripts/_run.sh index 11158ecf5bd..bb024f71c08 100755 --- a/systemvm/agent/scripts/_run.sh +++ b/systemvm/agent/scripts/_run.sh @@ -60,4 +60,4 @@ if [ "$(uname -m | grep '64')" == "" ]; then fi fi -java -Djavax.net.ssl.trustStore=./certs/realhostip.keystore -Djdk.tls.ephemeralDHKeySize=2048 -Dlog.home=$LOGHOME -mx${maxmem}m -cp $CP com.cloud.agent.AgentShell $keyvalues $@ +java -Djavax.net.ssl.trustStore=./certs/systemvm.keystore -Djdk.tls.ephemeralDHKeySize=2048 -Dlog.home=$LOGHOME -mx${maxmem}m -cp $CP com.cloud.agent.AgentShell $keyvalues $@ diff --git a/systemvm/agent/scripts/config_ssl.sh b/systemvm/agent/scripts/config_ssl.sh index e9340b099f6..3968b2617f2 100755 --- a/systemvm/agent/scripts/config_ssl.sh +++ b/systemvm/agent/scripts/config_ssl.sh @@ -52,13 +52,13 @@ cflag= cpkflag= cpcflag= cccflag= -customPrivKey=$(dirname $0)/certs/realhostip.key -customPrivCert=$(dirname $0)/certs/realhostip.crt +customPrivKey=$(dirname $0)/certs/systemvm.key +customPrivCert=$(dirname $0)/certs/systemvm.crt customCertChain= customCACert= publicIp= hostName= -keyStore=$(dirname $0)/certs/realhostip.keystore +keyStore=$(dirname $0)/certs/systemvm.keystore defaultJavaKeyStoreFile=/etc/ssl/certs/java/cacerts defaultJavaKeyStorePass="changeit" aliasName="CPVMCertificate" diff --git a/systemvm/debian/opt/cloud/bin/setup/bootstrap.sh b/systemvm/debian/opt/cloud/bin/setup/bootstrap.sh index f7c071c8cc0..c601f6ad221 100755 --- a/systemvm/debian/opt/cloud/bin/setup/bootstrap.sh +++ b/systemvm/debian/opt/cloud/bin/setup/bootstrap.sh @@ -65,7 +65,7 @@ patch_systemvm() { fi rm -fr $backupfolder # Import global cacerts into 'cloud' service's keystore - keytool -importkeystore -srckeystore /etc/ssl/certs/java/cacerts -destkeystore /usr/local/cloud/systemvm/certs/realhostip.keystore -srcstorepass changeit -deststorepass vmops.com -noprompt || true + keytool -importkeystore -srckeystore /etc/ssl/certs/java/cacerts -destkeystore /usr/local/cloud/systemvm/certs/systemvm.keystore -srcstorepass changeit -deststorepass vmops.com -noprompt || true return 0 } diff --git a/systemvm/patch-sysvms.sh b/systemvm/patch-sysvms.sh index 8d96de9ba3b..e8fa06018ed 100755 --- a/systemvm/patch-sysvms.sh +++ b/systemvm/patch-sysvms.sh @@ -126,25 +126,25 @@ patch_systemvm() { if [ "$TYPE" = "consoleproxy" ] || [ "$TYPE" = "secstorage" ]; then # Import global cacerts into 'cloud' service's keystore - REALHOSTIP_KS_FILE="/usr/local/cloud/systemvm/certs/realhostip.keystore" - REALHOSTIP_PASS="vmops.com" + SYSTEMVM_KS_FILE="/usr/local/cloud/systemvm/certs/systemvm.keystore" + SYSTEMVM_PASS="vmops.com" keytool -importkeystore -srckeystore /etc/ssl/certs/java/cacerts \ - -destkeystore "$REALHOSTIP_KS_FILE" -srcstorepass changeit -deststorepass \ - "$REALHOSTIP_PASS" -noprompt 2>/dev/null || true + -destkeystore "$SYSTEMVM_KS_FILE" -srcstorepass changeit -deststorepass \ + "$SYSTEMVM_PASS" -noprompt 2>/dev/null || true - # Import CA cert(s) into realhostip.keystore so the SSVM JVM + # Import CA cert(s) into systemvm.keystore so the SSVM JVM # (which overrides the truststore via -Djavax.net.ssl.trustStore in _run.sh) # can trust servers signed by the CloudStack CA CACERT_FILE="/usr/local/share/ca-certificates/cloudstack/ca.crt" - if [ -f "$CACERT_FILE" ] && [ -f "$REALHOSTIP_KS_FILE" ]; then + if [ -f "$CACERT_FILE" ] && [ -f "$SYSTEMVM_KS_FILE" ]; then awk 'BEGIN{n=0} /-----BEGIN CERTIFICATE-----/{n++} n>0{print > "cloudca." n }' "$CACERT_FILE" for caChain in $(ls cloudca.* 2>/dev/null); do - keytool -delete -noprompt -alias "$caChain" -keystore "$REALHOSTIP_KS_FILE" \ - -storepass "$REALHOSTIP_PASS" > /dev/null 2>&1 || true + keytool -delete -noprompt -alias "$caChain" -keystore "$SYSTEMVM_KS_FILE" \ + -storepass "$SYSTEMVM_PASS" > /dev/null 2>&1 || true keytool -import -noprompt -trustcacerts -alias "$caChain" -file "$caChain" \ - -keystore "$REALHOSTIP_KS_FILE" -storepass "$REALHOSTIP_PASS" > /dev/null 2>&1 + -keystore "$SYSTEMVM_KS_FILE" -storepass "$SYSTEMVM_PASS" > /dev/null 2>&1 done rm -f cloudca.* fi diff --git a/systemvm/pom.xml b/systemvm/pom.xml index 9bffc45cf4e..ca9176ee85f 100644 --- a/systemvm/pom.xml +++ b/systemvm/pom.xml @@ -205,7 +205,7 @@ javax.net.ssl.trustStore - certs/realhostip.keystore + certs/systemvm.keystore log.home ${PWD}/ diff --git a/utils/src/test/java/org/apache/cloudstack/utils/imagestore/ImageStoreUtilTest.java b/utils/src/test/java/org/apache/cloudstack/utils/imagestore/ImageStoreUtilTest.java index b7df14dc85d..e50eefd466b 100644 --- a/utils/src/test/java/org/apache/cloudstack/utils/imagestore/ImageStoreUtilTest.java +++ b/utils/src/test/java/org/apache/cloudstack/utils/imagestore/ImageStoreUtilTest.java @@ -27,7 +27,7 @@ public class ImageStoreUtilTest { @Test public void testgenerateHttpsPostUploadUrl() throws MalformedURLException { - String ssvmdomain = "*.realhostip.com"; + String ssvmdomain = "*.example.com"; String ipAddress = "10.147.28.14"; String uuid = UUID.randomUUID().toString(); String protocol = "https"; @@ -47,7 +47,7 @@ public class ImageStoreUtilTest { @Test public void testgenerateHttpPostUploadUrl() throws MalformedURLException { - String ssvmdomain = "*.realhostip.com"; + String ssvmdomain = "*.example.com"; String ipAddress = "10.147.28.14"; String uuid = UUID.randomUUID().toString(); String protocol = "http"; From b0601e5478624f8d6c42c70125449d9b1a9401ba Mon Sep 17 00:00:00 2001 From: Vishesh <8760112+vishesh92@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:59:31 +0530 Subject: [PATCH 2/4] Fix issue triage github action (#13312) --- .../.github_workflows_shared_noop-reminder.md | 5 ++ .../.github_workflows_shared_reporting.md | 73 ------------------- .github/workflows/issue-triage-agent.lock.yml | 29 ++++---- .github/workflows/issue-triage-agent.md | 3 +- 4 files changed, 21 insertions(+), 89 deletions(-) create mode 100644 .github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_noop-reminder.md delete mode 100644 .github/aw/imports/github/gh-aw/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github_workflows_shared_reporting.md diff --git a/.github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_noop-reminder.md b/.github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_noop-reminder.md new file mode 100644 index 00000000000..77cca08c1be --- /dev/null +++ b/.github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_noop-reminder.md @@ -0,0 +1,5 @@ +**Important**: If no action is needed after completing your analysis, you **MUST** call the `noop` safe-output tool with a brief explanation. Failing to call any safe-output tool is the most common cause of safe-output workflow failures. + +```json +{"noop": {"message": "No action needed: [brief explanation of what was analyzed and why]"}} +``` diff --git a/.github/aw/imports/github/gh-aw/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github_workflows_shared_reporting.md b/.github/aw/imports/github/gh-aw/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github_workflows_shared_reporting.md deleted file mode 100644 index bc08afb42be..00000000000 --- a/.github/aw/imports/github/gh-aw/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github_workflows_shared_reporting.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -# Report formatting guidelines ---- - -## Report Structure Guidelines - -### 1. Header Levels -**Use h3 (###) or lower for all headers in your issue report to maintain proper document hierarchy.** - -When creating GitHub issues or discussions: -- Use `###` (h3) for main sections (e.g., "### Test Summary") -- Use `####` (h4) for subsections (e.g., "#### Device-Specific Results") -- Never use `##` (h2) or `#` (h1) in reports - these are reserved for titles - -### 2. Progressive Disclosure -**Wrap detailed test results in `
Section Name` tags to improve readability and reduce scrolling.** - -Use collapsible sections for: -- Verbose details (full test logs, raw data) -- Secondary information (minor warnings, extra context) -- Per-item breakdowns when there are many items - -Always keep critical information visible (summary, critical issues, key metrics). - -### 3. Report Structure Pattern - -1. **Overview**: 1-2 paragraphs summarizing key findings -2. **Critical Information**: Show immediately (summary stats, critical issues) -3. **Details**: Use `
Section Name` for expanded content -4. **Context**: Add helpful metadata (workflow run, date, trigger) - -### Design Principles (Airbnb-Inspired) - -Reports should: -- **Build trust through clarity**: Most important info immediately visible -- **Exceed expectations**: Add helpful context like trends, comparisons -- **Create delight**: Use progressive disclosure to reduce overwhelm -- **Maintain consistency**: Follow patterns across all reports - -### Example Report Structure - -```markdown -### Summary -- Key metric 1: value -- Key metric 2: value -- Status: ✅/⚠️/❌ - -### Critical Issues -[Always visible - these are important] - -
-View Detailed Results - -[Comprehensive details, logs, traces] - -
- -
-View All Warnings - -[Minor issues and potential problems] - -
- -### Recommendations -[Actionable next steps - keep visible] -``` - -## Workflow Run References - -- Format run IDs as links: `[§12345](https://github.com/owner/repo/actions/runs/12345)` -- Include up to 3 most relevant run URLs at end under `**References:**` -- Do NOT add footer attribution (system adds automatically) diff --git a/.github/workflows/issue-triage-agent.lock.yml b/.github/workflows/issue-triage-agent.lock.yml index 3acf91dcc1e..94abbb0908c 100644 --- a/.github/workflows/issue-triage-agent.lock.yml +++ b/.github/workflows/issue-triage-agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"3cfe810a6f1f402ae902098f1dc14b99a4281a5986d58b76e96eec1a2fde7646","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"d4b8988df7c60cd416200769cc6bc7f1aab5bb3128df0b9a83a35e061b4da111","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"46d564922b082d0db93244972e8005ea6904ee5f","version":"v0.76.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55","digest":"sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55","digest":"sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55@sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55","digest":"sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55@sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.19","digest":"sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.19@sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -27,6 +27,7 @@ # # Resolved workflow manifest: # Imports: +# - github/gh-aw/.github/workflows/shared/noop-reminder.md@359795d49ada21681ab616bd4cbcb144a7387115 # - github/gh-aw/.github/workflows/shared/reporting.md@359795d49ada21681ab616bd4cbcb144a7387115 # # Secrets used: @@ -197,20 +198,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_9a8f42497f411c95_EOF' + cat << 'GH_AW_PROMPT_e560c36b9148ef78_EOF' - GH_AW_PROMPT_9a8f42497f411c95_EOF + GH_AW_PROMPT_e560c36b9148ef78_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_9a8f42497f411c95_EOF' + cat << 'GH_AW_PROMPT_e560c36b9148ef78_EOF' Tools: add_comment, add_labels, missing_tool, missing_data, noop - GH_AW_PROMPT_9a8f42497f411c95_EOF + GH_AW_PROMPT_e560c36b9148ef78_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_9a8f42497f411c95_EOF' + cat << 'GH_AW_PROMPT_e560c36b9148ef78_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -239,14 +240,14 @@ jobs: {{/if}} - GH_AW_PROMPT_9a8f42497f411c95_EOF + GH_AW_PROMPT_e560c36b9148ef78_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_9a8f42497f411c95_EOF' + cat << 'GH_AW_PROMPT_e560c36b9148ef78_EOF' {{#runtime-import .github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_reporting.md}} - {{#runtime-import .github/workflows/shared/noop-reminder.md}} + {{#runtime-import .github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_noop-reminder.md}} {{#runtime-import .github/workflows/issue-triage-agent.md}} - GH_AW_PROMPT_9a8f42497f411c95_EOF + GH_AW_PROMPT_e560c36b9148ef78_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -435,9 +436,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_40bb017992a44aa9_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_c8eaf0ada4607ff7_EOF' {"add_comment":{"max":1},"add_labels":{"allowed":["bug","feature","enhancement","documentation","question","help-wanted","good-first-issue"]},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_40bb017992a44aa9_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_c8eaf0ada4607ff7_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -653,7 +654,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_8874a73d1c6a94b7_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_88b8311e90d25032_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -694,7 +695,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_8874a73d1c6a94b7_EOF + GH_AW_MCP_CONFIG_88b8311e90d25032_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true diff --git a/.github/workflows/issue-triage-agent.md b/.github/workflows/issue-triage-agent.md index 26c43644618..2d382f9117c 100644 --- a/.github/workflows/issue-triage-agent.md +++ b/.github/workflows/issue-triage-agent.md @@ -6,6 +6,7 @@ permissions: issues: read imports: - github/gh-aw/.github/workflows/shared/reporting.md@359795d49ada21681ab616bd4cbcb144a7387115 +- github/gh-aw/.github/workflows/shared/noop-reminder.md@359795d49ada21681ab616bd4cbcb144a7387115 safe-outputs: add-comment: {} add-labels: @@ -88,5 +89,3 @@ This provides both per-issue context and batch visibility. - `question`: Used for issues that are asking for clarification or have questions about the project. - `help-wanted`: Indicates that the issue is a good candidate for external contributions and help - `good-first-issue`: Marks issues that are suitable for newcomers to the project, often with simpler scope. - -{{#runtime-import shared/noop-reminder.md}} From 2fd83e13b1879ba5df18204d595c563fc2b10c33 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Fri, 12 Jun 2026 08:16:10 -0300 Subject: [PATCH 3/4] Introduce Quota resource statement API (#13236) --- .../apache/cloudstack/api/ApiConstants.java | 4 + .../META-INF/db/schema-42210to42300.sql | 6 + .../cloudstack/quota/QuotaManagerImpl.java | 91 +++++-- .../cloudstack/quota/constant/QuotaTypes.java | 90 ++++--- .../quota/dao/QuotaUsageJoinDao.java | 2 +- .../quota/dao/QuotaUsageJoinDaoImpl.java | 8 +- .../quota/QuotaManagerImplTest.java | 73 +++++- .../command/QuotaResourceStatementCmd.java | 118 +++++++++ .../api/command/QuotaStatementCmd.java | 15 +- .../QuotaResourceStatementItemResponse.java | 82 ++++++ .../QuotaResourceStatementResponse.java | 66 +++++ .../api/response/QuotaResponseBuilder.java | 3 + .../response/QuotaResponseBuilderImpl.java | 241 ++++++++++++++---- .../api/response/QuotaStatementResponse.java | 4 +- .../apache/cloudstack/quota/QuotaService.java | 2 +- .../cloudstack/quota/QuotaServiceImpl.java | 10 +- .../QuotaResponseBuilderImplTest.java | 198 +++++++++----- .../quota/QuotaServiceImplTest.java | 6 +- 18 files changed, 813 insertions(+), 206 deletions(-) create mode 100644 plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java create mode 100644 plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java create mode 100644 plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index ee10db031ba..c5671d22d07 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -602,6 +602,8 @@ public class ApiConstants { public static final String SUITABLE_FOR_VM = "suitableforvirtualmachine"; public static final String SUPPORTS_STORAGE_SNAPSHOT = "supportsstoragesnapshot"; public static final String TARGET_IQN = "targetiqn"; + public static final String TARIFF_ID = "tariffid"; + public static final String TARIFF_NAME = "tariffname"; public static final String TASKS_FILTER = "tasksfilter"; public static final String TEMPLATE_FILTER = "templatefilter"; public static final String TEMPLATE_ID = "templateid"; @@ -663,6 +665,7 @@ public class ApiConstants { public static final String VIRTUAL_MACHINE_STATE = "vmstate"; public static final String VIRTUAL_MACHINES = "virtualmachines"; public static final String USAGE_ID = "usageid"; + public static final String USAGE_NAME = "usagename"; public static final String USAGE_TYPE = "usagetype"; public static final String INCLUDE_TAGS = "includetags"; @@ -879,6 +882,7 @@ public class ApiConstants { public static final String IS_SOURCE_NAT = "issourcenat"; public static final String IS_STATIC_NAT = "isstaticnat"; public static final String ITERATIONS = "iterations"; + public static final String ITEMS = "items"; public static final String SORT_BY = "sortby"; public static final String CHANGE_CIDR = "changecidr"; public static final String PURPOSE = "purpose"; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index d999cbcd509..2d25b3355d8 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -202,3 +202,9 @@ CREATE TABLE IF NOT EXISTS `cloud`.`image_transfer`( CONSTRAINT `fk_image_transfer__host_id` FOREIGN KEY (`host_id`) REFERENCES `host`(`id`) ON DELETE CASCADE, INDEX `i_image_transfer__backup_id`(`backup_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +--- Quota resource statement +INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'quotaResourceStatement', permission, sort_order +FROM cloud.role_permissions rp +WHERE rule = 'quotaStatement' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'quotaResourceStatement'); diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java index 58f51eae9fc..20bc0b015bb 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -32,6 +33,9 @@ import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.quota.activationrule.presetvariables.Configuration; import org.apache.cloudstack.quota.activationrule.presetvariables.GenericPresetVariable; @@ -43,9 +47,11 @@ import org.apache.cloudstack.quota.constant.QuotaTypes; import org.apache.cloudstack.quota.dao.QuotaAccountDao; import org.apache.cloudstack.quota.dao.QuotaBalanceDao; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.apache.cloudstack.usage.UsageUnitTypes; @@ -86,7 +92,8 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { private QuotaBalanceDao _quotaBalanceDao; @Inject private ConfigurationDao _configDao; - + @Inject + private QuotaTariffUsageDao quotaTariffUsageDao; @Inject protected PresetVariableHelper presetVariableHelper; @@ -311,14 +318,14 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { String accountToString = account.reflectionToString(); logger.info("Calculating quota usage of [{}] usage records for account [{}].", usageRecords.size(), accountToString); - List> pairsUsageAndQuotaUsage = new ArrayList<>(); + Map>> mapUsageAndQuotaUsage = new LinkedHashMap<>(); try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) { for (UsageVO usageRecord : usageRecords) { int usageType = usageRecord.getUsageType(); if (!shouldCalculateUsageRecord(account, usageRecord)) { - pairsUsageAndQuotaUsage.add(new Pair<>(usageRecord, null)); + mapUsageAndQuotaUsage.put(usageRecord, null); continue; } @@ -326,18 +333,31 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { List quotaTariffs = pairQuotaTariffsPerUsageTypeAndHasActivationRule.first(); boolean hasAnyQuotaTariffWithActivationRule = pairQuotaTariffsPerUsageTypeAndHasActivationRule.second(); - BigDecimal aggregatedQuotaTariffsValue = aggregateQuotaTariffsValues(usageRecord, quotaTariffs, hasAnyQuotaTariffWithActivationRule, jsInterpreter, accountToString); + Map aggregatedQuotaTariffsAndValues = aggregateQuotaTariffsValues(usageRecord, + quotaTariffs, hasAnyQuotaTariffWithActivationRule, jsInterpreter, accountToString); + BigDecimal aggregatedQuotaTariffsValue = aggregatedQuotaTariffsAndValues.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add); + logger.debug("The aggregation of the quota tariffs of account [{}] resulted in [{}] for the usage record [{}].", + account, aggregatedQuotaTariffsValue, usageRecord); QuotaUsageVO quotaUsage = createQuotaUsageAccordingToUsageUnit(usageRecord, aggregatedQuotaTariffsValue, accountToString); + if (quotaUsage == null) { + mapUsageAndQuotaUsage.put(usageRecord, null); + continue; + } - pairsUsageAndQuotaUsage.add(new Pair<>(usageRecord, quotaUsage)); + List quotaTariffUsages = new ArrayList<>(); + for (Map.Entry entry : aggregatedQuotaTariffsAndValues.entrySet()) { + QuotaTariffUsageVO quotaTariffUsage = createQuotaTariffUsage(usageRecord, entry.getKey(), entry.getValue()); + quotaTariffUsages.add(quotaTariffUsage); + } + mapUsageAndQuotaUsage.put(usageRecord, new Pair<>(quotaUsage, quotaTariffUsages)); } } catch (Exception e) { logger.error(String.format("Failed to calculate the quota usage for account [%s] due to [%s].", accountToString, e.getMessage()), e); return new ArrayList<>(); } - return persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(pairsUsageAndQuotaUsage); + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback>) status -> persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(mapUsageAndQuotaUsage)); } protected boolean shouldCalculateUsageRecord(AccountVO accountVO, UsageVO usageRecord) { @@ -349,31 +369,41 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { return true; } - protected List persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(List> pairsUsageAndQuotaUsage) { + protected List persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(Map>> mapUsageAndQuotaTariffUsage) { List quotaUsages = new ArrayList<>(); - for (Pair pairUsageAndQuotaUsage : pairsUsageAndQuotaUsage) { - UsageVO usageVo = pairUsageAndQuotaUsage.first(); + for (Map.Entry>> usageAndTariffUsage : mapUsageAndQuotaTariffUsage.entrySet()) { + UsageVO usageVo = usageAndTariffUsage.getKey(); usageVo.setQuotaCalculated(1); _usageDao.persistUsage(usageVo); - QuotaUsageVO quotaUsageVo = pairUsageAndQuotaUsage.second(); - if (quotaUsageVo != null) { - _quotaUsageDao.persistQuotaUsage(quotaUsageVo); - quotaUsages.add(quotaUsageVo); + Pair> pairUsageAndTariffUsages = usageAndTariffUsage.getValue(); + if (pairUsageAndTariffUsages != null) { + QuotaUsageVO quotaUsage = pairUsageAndTariffUsages.first(); + _quotaUsageDao.persistQuotaUsage(quotaUsage); + quotaUsages.add(quotaUsage); + + persistQuotaTariffUsages(pairUsageAndTariffUsages.second(), quotaUsage.getId()); } } return quotaUsages; } - protected BigDecimal aggregateQuotaTariffsValues(UsageVO usageRecord, List quotaTariffs, boolean hasAnyQuotaTariffWithActivationRule, - JsInterpreter jsInterpreter, String accountToString) { + protected void persistQuotaTariffUsages(List quotaTariffUsages, Long quotaUsageId) { + for (QuotaTariffUsageVO quotaTariffUsage : quotaTariffUsages) { + quotaTariffUsage.setQuotaUsageId(quotaUsageId); + quotaTariffUsageDao.persistQuotaTariffUsage(quotaTariffUsage); + } + } + + protected Map aggregateQuotaTariffsValues(UsageVO usageRecord, List quotaTariffs, boolean hasAnyQuotaTariffWithActivationRule, + JsInterpreter jsInterpreter, String accountToString) { String usageRecordToString = usageRecord.toString(usageAggregationTimeZone); logger.debug("Validating usage record [{}] for account [{}] against [{}] quota tariffs.", usageRecordToString, accountToString, quotaTariffs.size()); PresetVariables presetVariables = getPresetVariables(hasAnyQuotaTariffWithActivationRule, usageRecord); - BigDecimal aggregatedQuotaTariffsValue = BigDecimal.ZERO; + Map aggregatedQuotaTariffsAndValues = new HashMap<>(); quotaTariffs.sort(Comparator.comparing(QuotaTariffVO::getPosition)); @@ -382,10 +412,9 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { for (QuotaTariffVO quotaTariff : quotaTariffs) { if (isQuotaTariffInPeriodToBeApplied(usageRecord, quotaTariff, accountToString)) { - BigDecimal tariffValue = getQuotaTariffValueToBeApplied(quotaTariff, jsInterpreter, presetVariables, lastTariffs); - aggregatedQuotaTariffsValue = aggregatedQuotaTariffsValue.add(tariffValue); + aggregatedQuotaTariffsAndValues.put(quotaTariff, tariffValue); Tariff tariffPresetVariable = new Tariff(); tariffPresetVariable.setId(quotaTariff.getUuid()); @@ -394,10 +423,10 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { } } - logger.debug(String.format("The aggregation of the quota tariffs resulted in the value [%s] for the usage record [%s]. We will use this value to calculate the final" - + " usage value.", aggregatedQuotaTariffsValue, usageRecordToString)); + logger.debug("The aggregation of the quota tariffs resulted in [{}] quota tariffs for the usage record [{}]. The values of the quota tariffs will be used" + + " to calculate the final usage value.", aggregatedQuotaTariffsAndValues.size(), usageRecordToString); - return aggregatedQuotaTariffsValue; + return aggregatedQuotaTariffsAndValues; } protected PresetVariables getPresetVariables(boolean hasAnyQuotaTariffWithActivationRule, UsageVO usageRecord) { @@ -408,6 +437,26 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { return null; } + protected QuotaTariffUsageVO createQuotaTariffUsage(UsageVO usageRecord, QuotaTariffVO quotaTariff, BigDecimal quotaTariffValue) { + BigDecimal quotaUsageValue = BigDecimal.ZERO; + + if (!quotaTariffValue.equals(BigDecimal.ZERO)) { + String quotaUnit = QuotaTypes.getQuotaType(usageRecord.getUsageType()).getQuotaUnit(); + logger.trace("Calculating the value of the quota tariff [{}] according to its value [{}] and its quota unit [{}].", quotaTariff, quotaTariffValue, quotaUnit); + quotaUsageValue = getUsageValueAccordingToUsageUnitType(usageRecord, quotaTariffValue, quotaUnit); + logger.debug("The calculation of the value of the quota tariff [{}] according to its value [{}] and its usage unit [{}] resulted in the value [{}].", + quotaTariff, quotaTariffValue, quotaUnit, quotaUsageValue); + } else { + logger.debug("Quota tariff [{}] has no value to be calculated; therefore, it will be marked as value zero.", quotaTariff); + } + + QuotaTariffUsageVO quotaTariffUsage = new QuotaTariffUsageVO(); + quotaTariffUsage.setTariffId(quotaTariff.getId()); + quotaTariffUsage.setQuotaUsed(quotaUsageValue); + + return quotaTariffUsage; + } + /** * Returns the quota tariff value according to the result of the activation rule.
*
    diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java index 0da0d6e53f7..7b725d57c6e 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java @@ -20,11 +20,23 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.network.IpAddress; +import com.cloud.network.Network; +import com.cloud.network.VpnUser; +import com.cloud.network.rules.LoadBalancer; +import com.cloud.network.rules.PortForwardingRule; +import com.cloud.network.security.SecurityGroup; +import com.cloud.network.vpc.Vpc; +import com.cloud.offering.NetworkOffering; +import com.cloud.storage.Snapshot; +import com.cloud.storage.Volume; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.storage.object.Bucket; import org.apache.cloudstack.usage.UsageTypes; import org.apache.cloudstack.usage.UsageUnitTypes; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.apache.commons.lang3.StringUtils; public class QuotaTypes extends UsageTypes { private final Integer quotaType; @@ -32,44 +44,46 @@ public class QuotaTypes extends UsageTypes { private final String quotaUnit; private final String description; private final String discriminator; + private final Class clazz; private final static Map quotaTypeMap; static { final HashMap quotaTypeList = new HashMap(); - quotaTypeList.put(RUNNING_VM, new QuotaTypes(RUNNING_VM, "RUNNING_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Running Vm Usage")); - quotaTypeList.put(ALLOCATED_VM, new QuotaTypes(ALLOCATED_VM, "ALLOCATED_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Allocated Vm Usage")); - quotaTypeList.put(IP_ADDRESS, new QuotaTypes(IP_ADDRESS, "IP_ADDRESS", UsageUnitTypes.IP_MONTH.toString(), "IP Address Usage")); - quotaTypeList.put(NETWORK_BYTES_SENT, new QuotaTypes(NETWORK_BYTES_SENT, "NETWORK_BYTES_SENT", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Sent)")); - quotaTypeList.put(NETWORK_BYTES_RECEIVED, new QuotaTypes(NETWORK_BYTES_RECEIVED, "NETWORK_BYTES_RECEIVED", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Received)")); - quotaTypeList.put(VOLUME, new QuotaTypes(VOLUME, "VOLUME", UsageUnitTypes.GB_MONTH.toString(), "Volume Usage")); - quotaTypeList.put(TEMPLATE, new QuotaTypes(TEMPLATE, "TEMPLATE", UsageUnitTypes.GB_MONTH.toString(), "Template Usage")); - quotaTypeList.put(ISO, new QuotaTypes(ISO, "ISO", UsageUnitTypes.GB_MONTH.toString(), "ISO Usage")); - quotaTypeList.put(SNAPSHOT, new QuotaTypes(SNAPSHOT, "SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "Snapshot Usage")); - quotaTypeList.put(SECURITY_GROUP, new QuotaTypes(SECURITY_GROUP, "SECURITY_GROUP", UsageUnitTypes.POLICY_MONTH.toString(), "Security Group Usage")); - quotaTypeList.put(LOAD_BALANCER_POLICY, new QuotaTypes(LOAD_BALANCER_POLICY, "LOAD_BALANCER_POLICY", UsageUnitTypes.POLICY_MONTH.toString(), "Load Balancer Usage")); - quotaTypeList.put(PORT_FORWARDING_RULE, new QuotaTypes(PORT_FORWARDING_RULE, "PORT_FORWARDING_RULE", UsageUnitTypes.POLICY_MONTH.toString(), "Port Forwarding Usage")); - quotaTypeList.put(NETWORK_OFFERING, new QuotaTypes(NETWORK_OFFERING, "NETWORK_OFFERING", UsageUnitTypes.POLICY_MONTH.toString(), "Network Offering Usage")); - quotaTypeList.put(VPN_USERS, new QuotaTypes(VPN_USERS, "VPN_USERS", UsageUnitTypes.POLICY_MONTH.toString(), "VPN users usage")); - quotaTypeList.put(VM_DISK_IO_READ, new QuotaTypes(VM_DISK_IO_READ, "VM_DISK_IO_READ", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Read)")); - quotaTypeList.put(VM_DISK_IO_WRITE, new QuotaTypes(VM_DISK_IO_WRITE, "VM_DISK_IO_WRITE", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Write)")); - quotaTypeList.put(VM_DISK_BYTES_READ, new QuotaTypes(VM_DISK_BYTES_READ, "VM_DISK_BYTES_READ", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Read)")); - quotaTypeList.put(VM_DISK_BYTES_WRITE, new QuotaTypes(VM_DISK_BYTES_WRITE, "VM_DISK_BYTES_WRITE", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Write)")); - quotaTypeList.put(VM_SNAPSHOT, new QuotaTypes(VM_SNAPSHOT, "VM_SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot storage usage")); - quotaTypeList.put(VOLUME_SECONDARY, new QuotaTypes(VOLUME_SECONDARY, "VOLUME_SECONDARY", UsageUnitTypes.GB_MONTH.toString(), "Volume secondary storage usage")); - quotaTypeList.put(VM_SNAPSHOT_ON_PRIMARY, new QuotaTypes(VM_SNAPSHOT_ON_PRIMARY, "VM_SNAPSHOT_ON_PRIMARY", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot primary storage usage")); - quotaTypeList.put(BACKUP, new QuotaTypes(BACKUP, "BACKUP", UsageUnitTypes.GB_MONTH.toString(), "Backup storage usage")); - quotaTypeList.put(BUCKET, new QuotaTypes(BUCKET, "BUCKET", UsageUnitTypes.GB_MONTH.toString(), "Object Store bucket usage")); - quotaTypeList.put(NETWORK, new QuotaTypes(NETWORK, "NETWORK", UsageUnitTypes.COMPUTE_MONTH.toString(), "Network usage")); - quotaTypeList.put(VPC, new QuotaTypes(VPC, "VPC", UsageUnitTypes.COMPUTE_MONTH.toString(), "VPC usage")); + quotaTypeList.put(RUNNING_VM, new QuotaTypes(RUNNING_VM, "RUNNING_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Running Vm Usage", VirtualMachine.class)); + quotaTypeList.put(ALLOCATED_VM, new QuotaTypes(ALLOCATED_VM, "ALLOCATED_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Allocated Vm Usage", VirtualMachine.class)); + quotaTypeList.put(IP_ADDRESS, new QuotaTypes(IP_ADDRESS, "IP_ADDRESS", UsageUnitTypes.IP_MONTH.toString(), "IP Address Usage", IpAddress.class)); + quotaTypeList.put(NETWORK_BYTES_SENT, new QuotaTypes(NETWORK_BYTES_SENT, "NETWORK_BYTES_SENT", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Sent)", Network.class)); + quotaTypeList.put(NETWORK_BYTES_RECEIVED, new QuotaTypes(NETWORK_BYTES_RECEIVED, "NETWORK_BYTES_RECEIVED", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Received)", Network.class)); + quotaTypeList.put(VOLUME, new QuotaTypes(VOLUME, "VOLUME", UsageUnitTypes.GB_MONTH.toString(), "Volume Usage", Volume.class)); + quotaTypeList.put(TEMPLATE, new QuotaTypes(TEMPLATE, "TEMPLATE", UsageUnitTypes.GB_MONTH.toString(), "Template Usage", VirtualMachineTemplate.class)); + quotaTypeList.put(ISO, new QuotaTypes(ISO, "ISO", UsageUnitTypes.GB_MONTH.toString(), "ISO Usage", VirtualMachineTemplate.class)); + quotaTypeList.put(SNAPSHOT, new QuotaTypes(SNAPSHOT, "SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "Snapshot Usage", Snapshot.class)); + quotaTypeList.put(SECURITY_GROUP, new QuotaTypes(SECURITY_GROUP, "SECURITY_GROUP", UsageUnitTypes.POLICY_MONTH.toString(), "Security Group Usage", SecurityGroup.class)); + quotaTypeList.put(LOAD_BALANCER_POLICY, new QuotaTypes(LOAD_BALANCER_POLICY, "LOAD_BALANCER_POLICY", UsageUnitTypes.POLICY_MONTH.toString(), "Load Balancer Usage", LoadBalancer.class)); + quotaTypeList.put(PORT_FORWARDING_RULE, new QuotaTypes(PORT_FORWARDING_RULE, "PORT_FORWARDING_RULE", UsageUnitTypes.POLICY_MONTH.toString(), "Port Forwarding Usage", PortForwardingRule.class)); + quotaTypeList.put(NETWORK_OFFERING, new QuotaTypes(NETWORK_OFFERING, "NETWORK_OFFERING", UsageUnitTypes.POLICY_MONTH.toString(), "Network Offering Usage", NetworkOffering.class)); + quotaTypeList.put(VPN_USERS, new QuotaTypes(VPN_USERS, "VPN_USERS", UsageUnitTypes.POLICY_MONTH.toString(), "VPN users usage", VpnUser.class)); + quotaTypeList.put(VM_DISK_IO_READ, new QuotaTypes(VM_DISK_IO_READ, "VM_DISK_IO_READ", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Read)", Volume.class)); + quotaTypeList.put(VM_DISK_IO_WRITE, new QuotaTypes(VM_DISK_IO_WRITE, "VM_DISK_IO_WRITE", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Write)", Volume.class)); + quotaTypeList.put(VM_DISK_BYTES_READ, new QuotaTypes(VM_DISK_BYTES_READ, "VM_DISK_BYTES_READ", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Read)", Volume.class)); + quotaTypeList.put(VM_DISK_BYTES_WRITE, new QuotaTypes(VM_DISK_BYTES_WRITE, "VM_DISK_BYTES_WRITE", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Write)", Volume.class)); + quotaTypeList.put(VM_SNAPSHOT, new QuotaTypes(VM_SNAPSHOT, "VM_SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot storage usage", Snapshot.class)); + quotaTypeList.put(VOLUME_SECONDARY, new QuotaTypes(VOLUME_SECONDARY, "VOLUME_SECONDARY", UsageUnitTypes.GB_MONTH.toString(), "Volume secondary storage usage", Volume.class)); + quotaTypeList.put(VM_SNAPSHOT_ON_PRIMARY, new QuotaTypes(VM_SNAPSHOT_ON_PRIMARY, "VM_SNAPSHOT_ON_PRIMARY", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot primary storage usage", Snapshot.class)); + quotaTypeList.put(BACKUP, new QuotaTypes(BACKUP, "BACKUP", UsageUnitTypes.GB_MONTH.toString(), "Backup storage usage", BackupOffering.class)); + quotaTypeList.put(BUCKET, new QuotaTypes(BUCKET, "BUCKET", UsageUnitTypes.GB_MONTH.toString(), "Object Store bucket usage", Bucket.class)); + quotaTypeList.put(NETWORK, new QuotaTypes(NETWORK, "NETWORK", UsageUnitTypes.COMPUTE_MONTH.toString(), "Network usage", Network.class)); + quotaTypeList.put(VPC, new QuotaTypes(VPC, "VPC", UsageUnitTypes.COMPUTE_MONTH.toString(), "VPC usage", Vpc.class)); quotaTypeMap = Collections.unmodifiableMap(quotaTypeList); } - private QuotaTypes(Integer quotaType, String name, String unit, String description) { + private QuotaTypes(Integer quotaType, String name, String unit, String description, Class clazz) { this.quotaType = quotaType; this.description = description; this.quotaName = name; this.quotaUnit = unit; this.discriminator = "None"; + this.clazz = clazz; } public static Map listQuotaTypes() { @@ -104,22 +118,20 @@ public class QuotaTypes extends UsageTypes { return null; } + public Class getClazz() { + return clazz; + } + static public QuotaTypes getQuotaType(int quotaType) { return quotaTypeMap.get(quotaType); } - static public QuotaTypes getQuotaTypeByName(String name) { - if (StringUtils.isBlank(name)) { - throw new CloudRuntimeException("Could not retrieve Quota type by name because the value passed as parameter is null, empty, or blank."); + static public Class getClazz(int quotaType) { + QuotaTypes t = quotaTypeMap.get(quotaType); + if (t != null) { + return t.getClazz(); } - - for (QuotaTypes type : quotaTypeMap.values()) { - if (type.getQuotaName().equals(name)) { - return type; - } - } - - throw new CloudRuntimeException(String.format("Could not find Quota type with name [%s].", name)); + return null; } @Override diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java index 126fa11413f..ead70ae3501 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java @@ -26,6 +26,6 @@ import java.util.List; public interface QuotaUsageJoinDao extends GenericDao { - List findQuotaUsage(Long accountId, Long domainId, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId); + List findQuotaUsage(Long accountId, List domainIds, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId); } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java index b98ea2b3a5d..e69b1a05ba4 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java @@ -61,7 +61,7 @@ public class QuotaUsageJoinDaoImpl extends GenericDaoBase searchBuilder) { searchBuilder.and("accountId", searchBuilder.entity().getAccountId(), SearchCriteria.Op.EQ); - searchBuilder.and("domainId", searchBuilder.entity().getDomainId(), SearchCriteria.Op.EQ); + searchBuilder.and("domainIds", searchBuilder.entity().getDomainId(), SearchCriteria.Op.IN); searchBuilder.and("usageType", searchBuilder.entity().getUsageType(), SearchCriteria.Op.EQ); searchBuilder.and("resourceId", searchBuilder.entity().getResourceId(), SearchCriteria.Op.EQ); searchBuilder.and("networkId", searchBuilder.entity().getNetworkId(), SearchCriteria.Op.EQ); @@ -71,11 +71,13 @@ public class QuotaUsageJoinDaoImpl extends GenericDaoBase findQuotaUsage(Long accountId, Long domainId, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId) { + public List findQuotaUsage(Long accountId, List domainIds, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId) { SearchCriteria sc = tariffId == null ? searchQuotaUsages.create() : searchQuotaUsagesJoinTariffUsages.create(); sc.setParametersIfNotNull("accountId", accountId); - sc.setParametersIfNotNull("domainId", domainId); + if (domainIds != null) { + sc.setParameters("domainIds", domainIds.toArray()); + } sc.setParametersIfNotNull("usageType", usageType); sc.setParametersIfNotNull("resourceId", resourceId); sc.setParametersIfNotNull("networkId", networkId); diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java index a33faa054de..1e08e7d7fc0 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java @@ -22,6 +22,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -33,7 +34,9 @@ import org.apache.cloudstack.quota.activationrule.presetvariables.Tariff; import org.apache.cloudstack.quota.activationrule.presetvariables.Value; import org.apache.cloudstack.quota.constant.QuotaTypes; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.apache.cloudstack.usage.UsageUnitTypes; @@ -89,6 +92,9 @@ public class QuotaManagerImplTest { @Mock PresetVariables presetVariablesMock; + @Mock + QuotaTariffUsageDao quotaTariffUsageDaoMock; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Test @@ -484,47 +490,49 @@ public class QuotaManagerImplTest { } @Test - public void aggregateQuotaTariffsValuesTestTariffsWereNotInPeriodToBeAppliedReturnZero() { + public void aggregateQuotaTariffsValuesTestTariffsWereNotInPeriodToBeAppliedReturnEmptyMap() { List tariffs = createTariffList(); Mockito.doReturn(false).when(quotaManagerImplSpy).isQuotaTariffInPeriodToBeApplied(Mockito.any(), Mockito.any(), Mockito.anyString()); - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.ZERO, result); + Assert.assertTrue(result.isEmpty()); } @Test - public void aggregateQuotaTariffsValuesTestTariffsIsEmptyReturnZero() { - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, new ArrayList<>(), false, jsInterpreterMock, ""); + public void aggregateQuotaTariffsValuesTestTariffsIsEmptyReturnEmptyMap() { + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, new ArrayList<>(), false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.ZERO, result); + Assert.assertTrue(result.isEmpty()); } @Test - public void aggregateQuotaTariffsValuesTestTariffsAreInPeriodToBeAppliedReturnAggregation() { + public void aggregateQuotaTariffsValuesTestTariffsAreInPeriodToBeAppliedReturnTariffsWithTheirValues() { List tariffs = createTariffList(); Mockito.doReturn(true, false, true).when(quotaManagerImplSpy).isQuotaTariffInPeriodToBeApplied(Mockito.any(), Mockito.any(), Mockito.anyString()); Mockito.doReturn(BigDecimal.TEN).when(quotaManagerImplSpy).getQuotaTariffValueToBeApplied(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.TEN.multiply(new BigDecimal(2)), result); + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.values().stream().allMatch(value -> value.equals(BigDecimal.TEN))); } @Test public void persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsagesTestReturnOnlyPersistedQuotaUsageVo() { - List> listPair = new ArrayList<>(); + Map>> mapUsageQuotaUsage = new LinkedHashMap<>(); QuotaUsageVO quotaUsageVoMock1 = Mockito.mock(QuotaUsageVO.class); QuotaUsageVO quotaUsageVoMock2 = Mockito.mock(QuotaUsageVO.class); - listPair.add(new Pair<>(new UsageVO(), quotaUsageVoMock1)); - listPair.add(new Pair<>(new UsageVO(), null)); - listPair.add(new Pair<>(new UsageVO(), quotaUsageVoMock2)); + mapUsageQuotaUsage.put(new UsageVO(), new Pair<>(quotaUsageVoMock1, new ArrayList<>())); + mapUsageQuotaUsage.put(new UsageVO(), null); + mapUsageQuotaUsage.put(new UsageVO(), new Pair<>(quotaUsageVoMock2, new ArrayList<>())); Mockito.doReturn(null).when(usageDaoMock).persistUsage(Mockito.any()); Mockito.doReturn(null).when(quotaUsageDaoMock).persistQuotaUsage(Mockito.any()); + Mockito.doNothing().when(quotaManagerImplSpy).persistQuotaTariffUsages(Mockito.any(), Mockito.any()); - List result = quotaManagerImplSpy.persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(listPair); + List result = quotaManagerImplSpy.persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(mapUsageQuotaUsage); Assert.assertEquals(2, result.size()); Assert.assertEquals(quotaUsageVoMock1, result.get(0)); @@ -551,4 +559,41 @@ public class QuotaManagerImplTest { return lastTariffs; } + @Test + public void createQuotaTariffUsageTestTariffValueIsNotZeroReturnDetailedVo() { + Mockito.doReturn(1).when(usageVoMock).getUsageType(); + Mockito.doReturn(BigDecimal.TEN).when(quotaManagerImplSpy).getUsageValueAccordingToUsageUnitType(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doReturn(1L).when(quotaTariffVoMock).getId(); + + QuotaTariffUsageVO result = quotaManagerImplSpy.createQuotaTariffUsage(usageVoMock, quotaTariffVoMock, BigDecimal.TEN); + + Assert.assertEquals(1L, result.getTariffId().longValue()); + Assert.assertEquals(BigDecimal.TEN, result.getQuotaUsed()); + } + + @Test + public void persistQuotaTariffUsagesTestZeroQuotaTariffUsagesZeroPersisted() { + List quotaTariffUsages = new ArrayList<>(); + + quotaManagerImplSpy.persistQuotaTariffUsages(quotaTariffUsages, 1L); + + Mockito.verify(quotaTariffUsageDaoMock, Mockito.never()).persistQuotaTariffUsage(Mockito.any()); + } + + @Test + public void persistQuotaTariffUsagesTestTwoQuotaUsageDetailsTwoPersisted() { + List quotaUsageDetails = new ArrayList<>(); + QuotaTariffUsageVO quotaUsageDetailVoMock1 = Mockito.mock(QuotaTariffUsageVO.class); + QuotaTariffUsageVO quotaUsageDetailVoMock2 = Mockito.mock(QuotaTariffUsageVO.class); + + quotaUsageDetails.add(quotaUsageDetailVoMock1); + quotaUsageDetails.add(quotaUsageDetailVoMock2); + + Mockito.doNothing().when(quotaTariffUsageDaoMock).persistQuotaTariffUsage(Mockito.any()); + + quotaManagerImplSpy.persistQuotaTariffUsages(quotaUsageDetails, 1L); + + Mockito.verify(quotaTariffUsageDaoMock, Mockito.times(2)).persistQuotaTariffUsage(Mockito.any()); + } + } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java new file mode 100644 index 00000000000..00dcb8fa575 --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java @@ -0,0 +1,118 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//with the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. +package org.apache.cloudstack.api.command; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.AccountResponse; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.QuotaResponseBuilder; +import org.apache.cloudstack.api.response.QuotaResourceStatementResponse; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; + +import javax.inject.Inject; + +import java.util.Date; + +@APICommand(name = "quotaResourceStatement", responseObject = QuotaResourceStatementResponse.class, since = "4.23.0.0", + description = "Generates a detailed Quota statement for a specific resource.", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class QuotaResourceStatementCmd extends BaseCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.STRING, required = true, description = "ID of the resource.") + private String id; + + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, + description = "Generate the statement for this Account. A resource may have belonged to different owners at distinct points in time, " + + "so this parameter can be used to only consider the period for which it belonged to a specific Account.") + private Long accountId; + + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, + description = "Generate the statement for this Project. A resource may have belonged to different owners at distinct points in time, " + + "so this parameter can be used to only consider the period for which it belonged to a specific Project.") + private Long projectId; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "ID of the Domain for which the Quota statement will be generated.") + private Long domainId; + + @Parameter(name = ApiConstants.USAGE_TYPE, type = CommandType.INTEGER, required = true, description = "Usage type of the Quota usage.") + private Integer usageType; + + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, required = true, description = "Start date for the query. " + + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) + private Date startDate; + + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = true, description = "End date for the query. " + + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) + private Date endDate; + + @Parameter(name = ApiConstants.IS_RECURSIVE, type = CommandType.BOOLEAN, description = "Whether to include usage records from children of the filtered domain. " + + " Defaults to false.") + private Boolean recursive; + + @Inject + QuotaResponseBuilder responseBuilder; + + @Override + public void execute() { + QuotaResourceStatementResponse response = responseBuilder.createQuotaResourceStatement(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + if (ObjectUtils.allNull(accountId, projectId)) { + return -1; + } + return _accountService.finalizeAccountId(accountId, null, null, projectId); + } + + public String getId() { + return id; + } + + public Integer getUsageType() { + return usageType; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public Long getAccountId() { + return accountId; + } + + public Long getDomainId() { + return domainId; + } + + public boolean isRecursive() { + return BooleanUtils.isTrue(recursive); + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java index bfe26a9f425..f3a27ea5871 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java @@ -20,7 +20,6 @@ import java.util.Date; import javax.inject.Inject; -import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; @@ -32,18 +31,17 @@ import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.api.response.QuotaStatementItemResponse; import org.apache.cloudstack.api.response.QuotaStatementResponse; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @APICommand(name = "quotaStatement", responseObject = QuotaStatementItemResponse.class, description = "Create a Quota statement for the provided Account, Project, or Domain.", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, httpMethod = "GET") public class QuotaStatementCmd extends BaseCmd { - @ACL @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "Name of the Account for which the Quota statement will be generated. Deprecated, please use accountid instead.") private String accountName; - @ACL @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the Domain for which the Quota statement will be generated. May be used individually or with account.") private Long domainId; @@ -60,16 +58,18 @@ public class QuotaStatementCmd extends BaseCmd { description = "Consider only Quota usage records for the specified usage type in the statement.") private Integer usageType; - @ACL @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the Account for which the Quota statement will be generated. Can not be specified with projectid.") private Long accountId; - @ACL @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "ID of the Project for which the Quota statement will be generated. Can not be specified with accountid.", since = "4.23.0") private Long projectId; + @Parameter(name = ApiConstants.IS_RECURSIVE, type = CommandType.BOOLEAN, description = "Whether to include usage records from children of the filtered domain. " + + " Defaults to false.") + private Boolean recursive; + @Parameter(name = ApiConstants.SHOW_RESOURCES, type = CommandType.BOOLEAN, description = "List the resources of each Quota type in the period.", since = "4.23.0") private boolean showResources; @@ -152,4 +152,9 @@ public class QuotaStatementCmd extends BaseCmd { response.setResponseName(getCommandName()); setResponseObject(response); } + + public boolean isRecursive() { + return BooleanUtils.isTrue(recursive); + } + } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java new file mode 100644 index 00000000000..9f0a28389a9 --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java @@ -0,0 +1,82 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//with the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. +package org.apache.cloudstack.api.response; + +import java.math.BigDecimal; +import java.util.Date; + +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; + +public class QuotaResourceStatementItemResponse extends BaseResponse { + + @SerializedName(ApiConstants.TARIFF_ID) + @Param(description = "ID of the tariff.") + private String tariffId; + + @SerializedName(ApiConstants.TARIFF_NAME) + @Param(description = "Name of the tariff.") + private String tariffName; + + @SerializedName(ApiConstants.QUOTA_CONSUMED) + @Param(description = "Amount of quota used.") + private BigDecimal quotaUsed; + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Item's start date.") + private Date startDate; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "Item's end date.") + private Date endDate; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "UUID of the resource's owner.") + private String accountId; + + public QuotaResourceStatementItemResponse() { + super("quotaresourcestatementitem"); + } + + public void setTariffId(String tariffId) { + this.tariffId = tariffId; + } + + public void setTariffName(String tariffName) { + this.tariffName = tariffName; + } + + public void setQuotaUsed(BigDecimal quotaUsed) { + this.quotaUsed = quotaUsed; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java new file mode 100644 index 00000000000..ce3ab1f177b --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java @@ -0,0 +1,66 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//with the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import java.math.BigDecimal; +import java.util.List; + +public class QuotaResourceStatementResponse extends BaseResponse { + + @SerializedName(ApiConstants.USAGE_NAME) + @Param(description = "Name of the usage type.") + private String usageName; + + @SerializedName(ApiConstants.UNIT) + @Param(description = "Unit of the usage type.") + private String unit; + + @SerializedName(ApiConstants.ITEMS) + @Param(description = "List of Quota tariff usages.", responseObject = QuotaResourceStatementItemResponse.class) + private List items; + + @SerializedName(ApiConstants.TOTAL_QUOTA) + @Param(description = "Total amount of quota used.") + private BigDecimal totalQuotaUsed; + + public QuotaResourceStatementResponse() { + super("quotaresourcestatement"); + } + + public void setUsageName(String usageName) { + this.usageName = usageName; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public void setQuotaUsageDetails(List items) { + this.items = items; + } + + public void setTotalQuotaUsed(BigDecimal totalQuotaUsed) { + this.totalQuotaUsed = totalQuotaUsed; + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java index f0390bf626d..63bf043f4fa 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java @@ -23,6 +23,7 @@ import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; @@ -83,4 +84,6 @@ public interface QuotaResponseBuilder { Pair, Integer> createQuotaCreditsListResponse(QuotaCreditsListCmd cmd); QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd); + + QuotaResourceStatementResponse createQuotaResourceStatement(QuotaResourceStatementCmd cmd); } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 231ba777e60..77099bcdc88 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -66,11 +66,14 @@ import com.cloud.user.User; import com.cloud.user.UserVO; import com.cloud.utils.DateUtil; import com.cloud.utils.Pair; +import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; @@ -78,6 +81,7 @@ import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; @@ -107,16 +111,20 @@ import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao; import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao; import org.apache.cloudstack.quota.dao.QuotaSummaryDao; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.dao.QuotaUsageJoinDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.apache.cloudstack.quota.vo.QuotaCreditsVO; import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; import org.apache.cloudstack.quota.vo.QuotaSummaryVO; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; import org.apache.cloudstack.quota.vo.QuotaUsageResourceVO; +import org.apache.cloudstack.usage.UsageTypes; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections4.CollectionUtils; @@ -181,7 +189,12 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { private VMTemplateDao vmTemplateDao; @Inject private VolumeDao volumeDao; - + @Inject + private QuotaUsageJoinDao quotaUsageJoinDao; + @Inject + private QuotaTariffUsageDao quotaTariffUsageDao; + @Inject + private EntityManager entityMgr; private final Class[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class, ResourceCounting.class}; @@ -342,9 +355,9 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { @Override public QuotaStatementResponse createQuotaStatementResponse(QuotaStatementCmd cmd) { - Long accountId = getAccountIdForQuotaStatement(cmd); - Long domainId = getDomainIdForQuotaStatement(cmd, accountId); - List quotaUsages = _quotaService.getQuotaUsage(accountId, null, domainId, cmd.getUsageType(), cmd.getStartDate(), cmd.getEndDate()); + Long accountId = getAccountIdForQuotaStatement(cmd.getEntityOwnerId(), null); + Pair> baseDomainAndFilteredDomains = getDomainIdsForQuotaStatement(accountId, cmd.getDomainId(), cmd.isRecursive()); + List quotaUsages = _quotaService.getQuotaUsage(accountId, null, baseDomainAndFilteredDomains.second(), cmd.getUsageType(), cmd.getStartDate(), cmd.getEndDate()); logger.debug("Creating quota statement from [{}] usage records for parameters [{}].", quotaUsages.size(), ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "accountName", "accountId", "projectId", "domainId", "startDate", "endDate", "usageType", "showResources")); @@ -367,52 +380,16 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { Account account = _accountDao.findByIdIncludingRemoved(accountId); statement.setAccountId(account.getUuid()); statement.setAccountName(account.getAccountName()); - domainId = account.getDomainId(); } - if (domainId != null) { - DomainVO domain = domainDao.findByIdIncludingRemoved(domainId); + Long baseDomainId = baseDomainAndFilteredDomains.first(); + if (baseDomainId != null) { + DomainVO domain = domainDao.findByIdIncludingRemoved(baseDomainId); statement.setDomainId(domain.getUuid()); } return statement; } - protected Long getAccountIdForQuotaStatement(QuotaStatementCmd cmd) { - if (Account.Type.NORMAL.equals(CallContext.current().getCallingAccount().getType())) { - logger.debug("Limiting the Quota statement for the calling Account, as they are a User Account."); - return CallContext.current().getCallingAccountId(); - } - - long accountId = cmd.getEntityOwnerId(); - if (accountId != -1) { - return accountId; - } - - if (cmd.getDomainId() == null) { - logger.debug("Limiting the Quota statement for the calling Account, as 'domainid' was not informed."); - return CallContext.current().getCallingAccountId(); - } - - logger.debug("Allowing admin/domain admin to generate the Quota statement for the provided Domain."); - return null; - } - - protected Long getDomainIdForQuotaStatement(QuotaStatementCmd cmd, Long accountId) { - if (accountId != null) { - logger.debug("Quota statement is already limited to Account [{}].", accountId); - Account account = _accountDao.findByIdIncludingRemoved(accountId); - return account.getDomainId(); - } - - Long domainId = cmd.getDomainId(); - if (domainId != null) { - return domainId; - } - - logger.debug("Limiting the Quota statement for the calling Account's Domain."); - return CallContext.current().getCallingAccount().getDomainId(); - } - protected void createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformed(List quotaUsages, Integer usageType) { if (usageType != null) { logger.debug("As the usage type [{}] was informed as parameter of the API quotaStatement, we will not create dummy records.", usageType); @@ -1158,6 +1135,184 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { return createValidateActivationRuleResponse(activationRule, quotaName, false, message); } + @Override + public QuotaResourceStatementResponse createQuotaResourceStatement(QuotaResourceStatementCmd cmd) { + String resourceUuid = cmd.getId(); + Integer usageType = cmd.getUsageType(); + Date startDate = cmd.getStartDate(); + Date endDate = cmd.getEndDate(); + + if (startDate.after(endDate)) { + throw new InvalidParameterValueException(String.format("The start date [%s] must be before the end date [%s].", startDate, endDate)); + } + + InternalIdentity resource = retrieveResource(resourceUuid, usageType); + if (resource == null) { + logger.error("Could not find resource [{}] of type [{}]. Returning an empty list.", resourceUuid, usageType); + return createQuotaResourceStatementResponse(resourceUuid, usageType, new ArrayList<>(), new BigDecimal(0)); + } + + long id = resource.getId(); + + Long currentOwnerId = null; + if (resource instanceof ControlledEntity) { + currentOwnerId = ((ControlledEntity) resource).getAccountId(); + } + Long accountId = getAccountIdForQuotaStatement(cmd.getEntityOwnerId(), currentOwnerId); + Pair> baseDomainAndFilteredDomains = getDomainIdsForQuotaStatement(accountId, cmd.getDomainId(), cmd.isRecursive()); + + Long resourceId = null; + Long networkId = null; + Long offeringId = null; + + switch (usageType) { + case UsageTypes.NETWORK_OFFERING: + case UsageTypes.BACKUP: + offeringId = id; + break; + case UsageTypes.NETWORK_BYTES_RECEIVED: + case UsageTypes.NETWORK_BYTES_SENT: + networkId = id; + break; + default: + resourceId = id; + } + + logger.debug("Attempting to find quota usages with parameters usage type [{}], usage id [{}], network id [{}], offering id [{}], and between [{}] and [{}].", + usageType, resourceId, networkId, offeringId, startDate, endDate); + + List quotaUsageJoinList = quotaUsageJoinDao.findQuotaUsage(accountId, baseDomainAndFilteredDomains.second(), usageType, resourceId, networkId, offeringId, startDate, endDate, null); + + logger.debug("Found [{}] quota usages using as parameter usage type [{}], usage id [{}], network id [{}], offering id [{}], and between [{}] and [{}].", + quotaUsageJoinList.size(), usageType, resourceId, networkId, offeringId, startDate, endDate); + + List quotaResourceStatementItemResponseList = new ArrayList<>(); + BigDecimal totalQuotaUsed = new BigDecimal(0); + List quotaTariffs = _quotaTariffDao.listQuotaTariffs(null, null, usageType, null, null, true, null, null).first(); + + for (QuotaUsageJoinVO quotaUsageJoin : quotaUsageJoinList) { + Account account = _accountMgr.getAccount(quotaUsageJoin.getAccountId()); + String accountUuid = account.getUuid(); + + List quotaTariffUsageList = quotaTariffUsageDao.listQuotaTariffUsages(quotaUsageJoin.getId()); + logger.debug("Found [{}] quota tariff usages associated to the quota usage [{}] of resource [{}] and type [{}] between [{}] and [{}].", + quotaTariffUsageList.size(), quotaUsageJoin, resourceUuid, usageType, startDate, endDate); + for (QuotaTariffUsageVO quotaTariffUsage: quotaTariffUsageList) { + quotaResourceStatementItemResponseList.add(createQuotaResourceStatementItemResponse(quotaTariffUsage, quotaTariffs, quotaUsageJoin.getStartDate(), + quotaUsageJoin.getEndDate(), accountUuid)); + totalQuotaUsed = totalQuotaUsed.add(quotaTariffUsage.getQuotaUsed()); + } + } + logger.debug("The total quota used of type [{}] between [{}] and [{}] for the resource [{}] was [{}].", usageType, startDate, endDate, resourceUuid, totalQuotaUsed); + + return createQuotaResourceStatementResponse(resourceUuid, usageType, quotaResourceStatementItemResponseList, totalQuotaUsed); + } + + protected Long getAccountIdForQuotaStatement(long providedAccountId, Long fallbackAccountId) { + Account caller = CallContext.current().getCallingAccount(); + + if (providedAccountId != -1L) { + Account account = _accountDao.findByIdIncludingRemoved(providedAccountId); + _accountMgr.checkAccess(caller, null, false, account); + logger.debug("Limiting the Quota resource statement for the provided Account [{}].", providedAccountId); + return providedAccountId; + } + + Account.Type callerType = caller.getType(); + if (Account.Type.ADMIN.equals(callerType) || Account.Type.DOMAIN_ADMIN.equals(callerType)) { + logger.debug("Not limiting the Quota resource statement for a specific Account, as no specific Account was provided and the caller is either an admin or domain admin."); + return null; + } + + if (fallbackAccountId != null) { + Account fallbackAccount = _accountDao.findByIdIncludingRemoved(fallbackAccountId); + _accountMgr.checkAccess(caller, null, false, fallbackAccount); + logger.debug("Limiting the Quota statement for the fallback Account [{}], as no specific Account was provided.", fallbackAccountId); + return fallbackAccountId; + } + + logger.debug("Limiting the Quota resource statement for the calling account, as no specific Account was provided and no fallback account was provided."); + return caller.getAccountId(); + } + + protected Pair> getDomainIdsForQuotaStatement(Long finalAccountId, Long providedDomainId, boolean isRecursive) { + if (finalAccountId != null) { + // Access to the provided account has already been validated + logger.debug("Not limiting the Quota statement for a specific Domain, as we are already limiting by Account."); + return new Pair<>(null, null); + } + + // User accounts will have already been limited to themselves + Account caller = CallContext.current().getCallingAccount(); + Long domainId = providedDomainId; + + if (domainId != null) { + Domain domain = domainDao.findByIdIncludingRemoved(domainId); + _accountMgr.checkAccess(caller, domain); + logger.debug("Limiting the Quota statement for the provided Domain [{}].", domainId); + } else { + domainId = caller.getDomainId(); + logger.debug("Limiting the Quota statement for the caller's Domain [{}], as no 'domainid' was provided.", domainId); + } + + if (isRecursive) { + logger.debug("Allowing the Quota statement for the Domain's children, as the 'isrecursive' parameter was provided."); + return new Pair<>(domainId, domainDao.getDomainAndChildrenIds(domainId)); + } + + return new Pair<>(domainId, List.of(domainId)); + } + + protected InternalIdentity retrieveResource(String resourceUuid, Integer usageType) { + Class clazz = QuotaTypes.getClazz(usageType); + if (clazz == null) { + throw new InvalidParameterValueException(String.format("Invalid usage type [%s] provided.", usageType)); + } + + logger.debug("Attempting to find a resource with ID [{}] and of type [{}].", resourceUuid, usageType); + Object object = entityMgr.findByUuidIncludingRemoved(clazz, resourceUuid); + if (object == null) { + return null; + } + return (InternalIdentity) object; + } + + protected QuotaResourceStatementItemResponse createQuotaResourceStatementItemResponse(QuotaTariffUsageVO quotaTariffUsage, List quotaTariffs, + Date startDate, Date endDate, String accountUuid) { + logger.trace("Creating quota resource statement item associated to quota tariff usage [{}].", quotaTariffUsage); + QuotaResourceStatementItemResponse quotaResourceStatementItemResponse = new QuotaResourceStatementItemResponse(); + + QuotaTariffVO quotaTariff = quotaTariffs.stream().filter(quotaTariffVO -> quotaTariffUsage.getTariffId().equals(quotaTariffVO.getId())).findAny().orElse(null); + + quotaResourceStatementItemResponse.setQuotaUsed(quotaTariffUsage.getQuotaUsed()); + quotaResourceStatementItemResponse.setStartDate(startDate); + quotaResourceStatementItemResponse.setEndDate(endDate); + quotaResourceStatementItemResponse.setAccountId(accountUuid); + if (quotaTariff != null) { + logger.trace("Quota usage details item will be associated to the quota tariff [{}].", quotaTariff); + quotaResourceStatementItemResponse.setTariffId(quotaTariff.getUuid()); + quotaResourceStatementItemResponse.setTariffName(quotaTariff.getName()); + } + + return quotaResourceStatementItemResponse; + } + + protected QuotaResourceStatementResponse createQuotaResourceStatementResponse(String resourceUuid, Integer usageType, + List quotaUsageDetailsItems, BigDecimal totalQuotaUsed) { + logger.trace("Creating quota usage details list response associated to the resource of UUID [{}], with an usage type of [{}], [{}] quota" + + " usage details items, and a total quota used of [{}].", resourceUuid, usageType, quotaUsageDetailsItems.size(), totalQuotaUsed); + QuotaResourceStatementResponse quotaResourceStatementResponse = new QuotaResourceStatementResponse(); + + QuotaTypes quotaType = QuotaTypes.getQuotaType(usageType); + quotaResourceStatementResponse.setUsageName(quotaType.getQuotaName()); + quotaResourceStatementResponse.setUnit(quotaType.getQuotaUnit()); + + quotaResourceStatementResponse.setQuotaUsageDetails(quotaUsageDetailsItems); + quotaResourceStatementResponse.setTotalQuotaUsed(totalQuotaUsed); + + return quotaResourceStatementResponse; + } + /** * Checks whether script variables are compatible with the usage type. First, we remove all script variables that correspond to the script's usage type variables. * Then, returns true if none of the remaining script variables match any usage types variables, and false otherwise. diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java index 81cb1011182..c30780582d3 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java @@ -31,11 +31,11 @@ public class QuotaStatementResponse extends BaseResponse { @Param(description = "ID of the Account.") private String accountId; - @SerializedName(ApiConstants.ACCOUNT) + @SerializedName(ApiConstants.ACCOUNT_NAME) @Param(description = "Name of the Account.") private String accountName; - @SerializedName(ApiConstants.DOMAIN) + @SerializedName(ApiConstants.DOMAIN_ID) @Param(description = "ID of the Domain.") private String domainId; diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java index 478e43d2e20..cc257c233de 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java @@ -28,7 +28,7 @@ import com.cloud.utils.component.PluggableService; public interface QuotaService extends PluggableService { - List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate); + List getQuotaUsage(Long accountId, String accountName, List domainIds, Integer usageType, Date startDate, Date endDate); List listQuotaBalancesForAccount(Long accountId, Date startDate, Date endDate); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java index e1ec2ebb2a6..a788f0c8b01 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java @@ -38,6 +38,7 @@ import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; import org.apache.cloudstack.api.command.QuotaEnabledCmd; import org.apache.cloudstack.api.command.QuotaListEmailConfigurationCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; @@ -131,6 +132,7 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi cmdList.add(QuotaListEmailConfigurationCmd.class); cmdList.add(QuotaPresetVariablesListCmd.class); cmdList.add(QuotaValidateActivationRuleCmd.class); + cmdList.add(QuotaResourceStatementCmd.class); return cmdList; } @@ -203,15 +205,15 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi } @Override - public List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate) { + public List getQuotaUsage(Long accountId, String accountName, List domainIds, Integer usageType, Date startDate, Date endDate) { if (startDate.after(endDate)) { throw new InvalidParameterValueException("Incorrect Date Range. Start date: " + startDate + " is after end date:" + endDate); } - logger.debug("Getting quota records of type [{}] for account [{}] in domain [{}], between [{}] and [{}].", - usageType, accountId, domainId, startDate, endDate); + logger.debug("Getting quota records of type [{}] for account [{}] in domains [{}], between [{}] and [{}].", + usageType, accountId, domainIds, startDate, endDate); - return quotaUsageJoinDao.findQuotaUsage(accountId, domainId, usageType, null, null, null, startDate, endDate, null); + return quotaUsageJoinDao.findQuotaUsage(accountId, domainIds, usageType, null, null, null, startDate, endDate, null); } @Override diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 0c7610461c1..37307db4562 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -38,14 +38,15 @@ import com.cloud.exception.PermissionDeniedException; import com.cloud.user.AccountManager; import com.cloud.user.UserVO; import com.cloud.utils.Pair; +import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; -import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd; import org.apache.cloudstack.context.CallContext; @@ -156,7 +157,7 @@ public class QuotaResponseBuilderImplTest extends TestCase { Date date = new Date(); @Mock - Account accountMock; + AccountVO accountMock; @Mock DomainVO domainVoMock; @@ -188,6 +189,9 @@ public class QuotaResponseBuilderImplTest extends TestCase { @Mock User callerUserMock; + @Mock + EntityManager entityManagerMock; + @Before public void setup() { CallContext.register(callerUserMock, callerAccountMock); @@ -1024,107 +1028,161 @@ public class QuotaResponseBuilderImplTest extends TestCase { } @Test - public void getAccountIdForQuotaStatementTestLimitsToCallingAccountForNormalUser() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getAccountIdForQuotaStatementTestReturnsProvidedAccount() { + long providedAccountId = 200L; - Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); - Mockito.doReturn(Account.Type.NORMAL).when(accountMock).getType(); + Mockito.when(accountDaoMock.findByIdIncludingRemoved(providedAccountId)).thenReturn(accountMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); + long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(providedAccountId, null); - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); - - Assert.assertEquals(Long.valueOf(callerAccountMock.getAccountId()), result); - } + Assert.assertEquals(200L, result); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); } @Test - public void getAccountIdForQuotaStatementTestReturnsEntityOwnerIdWhenProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsAdminWithoutProvidedAccount() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.ADMIN); - Mockito.doReturn(42L).when(cmd).getEntityOwnerId(); + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, null); - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); - - Assert.assertEquals(Long.valueOf(42L), result); + assertNull(result); + Mockito.verify(accountManagerMock, Mockito.never()).getAccount(Mockito.anyLong()); } @Test - public void getAccountIdForQuotaStatementTestLimitsToCallingAccountWhenCallerIsAdminAndDomainIsNotProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsDomainAdminWithoutProvidedAccount() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.DOMAIN_ADMIN); - Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); - Mockito.doReturn(Account.Type.ADMIN).when(accountMock).getType(); - Mockito.doReturn(-1L).when(cmd).getEntityOwnerId(); - Mockito.doReturn(null).when(cmd).getDomainId(); + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, null); - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); - - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); - - Assert.assertEquals(Long.valueOf(callerAccountMock.getAccountId()), result); - } + assertNull(result); + Mockito.verify(accountManagerMock, Mockito.never()).getAccount(Mockito.anyLong()); } @Test - public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsAdminAndDomainIsProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getAccountIdForQuotaStatementTestReturnsFallbackAccountWhenNoAccountProvidedAndCallerIsNotAdmin() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.NORMAL); - Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); - Mockito.doReturn(Account.Type.ADMIN).when(accountMock).getType(); - Mockito.doReturn(-1L).when(cmd).getEntityOwnerId(); - Mockito.doReturn(10L).when(cmd).getDomainId(); + Mockito.when(accountDaoMock.findByIdIncludingRemoved(300L)).thenReturn(accountMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); + long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, 300L); - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); - - Assert.assertNull(result); - } + assertEquals(300L, result); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); } @Test - public void getDomainIdForQuotaStatementTestReturnsAccountDomainIdWhenAccountIdIsProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); - AccountVO account = Mockito.mock(AccountVO.class); + public void getAccountIdForQuotaStatementTestAccessDeniedForProvidedAccount() { + Mockito.when(accountDaoMock.findByIdIncludingRemoved(200L)).thenReturn(accountMock); + Mockito.doThrow(new PermissionDeniedException("Access denied")) + .when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); - Mockito.doReturn(account).when(accountDaoMock).findByIdIncludingRemoved(55L); - Mockito.doReturn(77L).when(account).getDomainId(); - - Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, 55L); - - Assert.assertEquals(Long.valueOf(77L), result); + Assert.assertThrows(PermissionDeniedException.class, + () -> quotaResponseBuilderSpy.getAccountIdForQuotaStatement(200L, null)); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); } @Test - public void getDomainIdForQuotaStatementTestReturnsProvidedDomainIdWhenAccountIdIsNull() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getDomainIdsForQuotaStatementTestReturnsNullPairWhenAccountIsProvided() { + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(100L, null, false); - Mockito.doReturn(99L).when(cmd).getDomainId(); - - Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, null); - - Assert.assertEquals(Long.valueOf(99L), result); + assertNull(result.first()); + assertNull(result.second()); + Mockito.verify(domainDaoMock, Mockito.never()).findByIdIncludingRemoved(Mockito.anyLong()); } @Test - public void getDomainIdForQuotaStatementTestFallsBackToCallingAccountDomainIdWhenNeitherAccountNorDomainIsProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); - Account account = Mockito.mock(Account.class); + public void getDomainIdsForQuotaStatementTestReturnsProvidedDomainIdNonRecursively() { + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); - Mockito.doReturn(null).when(cmd).getDomainId(); - Mockito.doReturn(123L).when(account).getDomainId(); - Mockito.doReturn(account).when(callContextMock).getCallingAccount(); + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, false); - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); + assertEquals(5L, (long) result.first()); + assertEquals(List.of(5L), result.second()); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); + } - Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, null); + @Test + public void getDomainIdsForQuotaStatementTestReturnsCallerDomainNonRecursively() { + Mockito.when(callerAccountMock.getDomainId()).thenReturn(7L); - Assert.assertEquals(123L, result.longValue()); - } + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, null, false); + + assertEquals(7L, (long) result.first()); + assertEquals(List.of(7L), result.second()); + Mockito.verify(domainDaoMock, Mockito.never()).findByIdIncludingRemoved(Mockito.anyLong()); + } + + @Test + public void getDomainIdsForQuotaStatementTestReturnsProvidedDomainRecursively() { + List domainAndChildren = List.of(5L, 10L, 15L); + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.when(domainDaoMock.getDomainAndChildrenIds(5L)).thenReturn(domainAndChildren); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); + + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, true); + + assertEquals(5L, (long) result.first()); + assertEquals(domainAndChildren, result.second()); + Mockito.verify(domainDaoMock).getDomainAndChildrenIds(5L); + } + + @Test + public void getDomainIdsForQuotaStatementReturnsCallerDomainRecursively() { + List domainAndChildren = List.of(1L, 2L, 3L); + Mockito.when(callerAccountMock.getDomainId()).thenReturn(1L); + + Mockito.when(domainDaoMock.getDomainAndChildrenIds(1L)).thenReturn(domainAndChildren); + + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, null, true); + + assertEquals(1L, (long) result.first()); + assertEquals(domainAndChildren, result.second()); + Mockito.verify(domainDaoMock).getDomainAndChildrenIds(1L); + } + + @Test + public void getDomainIdsForQuotaStatementTestThrowsAccessDeniedForProvidedDomain() { + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.doThrow(new PermissionDeniedException("Access denied")) + .when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); + + Assert.assertThrows(PermissionDeniedException.class, + () -> quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, false)); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); + } + + @Test(expected = InvalidParameterValueException.class) + public void retrieveResourceTestThrowsExceptionForInvalidUsageType() { + Integer invalidUsageType = 999; + quotaResponseBuilderSpy.retrieveResource("validUuid", invalidUsageType); + } + + @Test + public void retrieveResourceTestReturnsNullForNonexistentResource() { + String invalidUuid = "nonexistentUuid"; + Integer validUsageType = QuotaTypes.ALLOCATED_VM; + + Mockito.doReturn(null).when(entityManagerMock).findByUuidIncludingRemoved(Mockito.any(), Mockito.eq(invalidUuid)); + InternalIdentity result = quotaResponseBuilderSpy.retrieveResource(invalidUuid, validUsageType); + + Assert.assertNull(result); + } + + @Test + public void retrieveResourceTestReturnsCorrectResource() { + String validUuid = "validUuid"; + Integer validUsageType = QuotaTypes.ALLOCATED_VM; + InternalIdentity mockResource = Mockito.mock(InternalIdentity.class); + + Mockito.doReturn(mockResource).when(entityManagerMock).findByUuidIncludingRemoved(Mockito.any(), Mockito.eq(validUuid)); + + InternalIdentity result = quotaResponseBuilderSpy.retrieveResource(validUuid, validUsageType); + + Assert.assertNotNull(result); + Assert.assertEquals(mockResource, result); } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java index 89c48b1cc66..c0ee6c5fc3f 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java @@ -117,12 +117,12 @@ public class QuotaServiceImplTest extends TestCase { public void testGetQuotaUsage() { final long accountId = 2L; final String accountName = "admin123"; - final long domainId = 1L; + final List domainIds = List.of(1L); final Date startDate = new DateTime().minusDays(2).toDate(); final Date endDate = new Date(); - quotaServiceImplSpy.getQuotaUsage(accountId, accountName, domainId, QuotaTypes.IP_ADDRESS, startDate, endDate); - Mockito.verify(quotaUsageJoinDaoMock, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(), + quotaServiceImplSpy.getQuotaUsage(accountId, accountName, domainIds, QuotaTypes.IP_ADDRESS, startDate, endDate); + Mockito.verify(quotaUsageJoinDaoMock, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainIds), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(Date.class), Mockito.any(Date.class), Mockito.any()); } From 2081ac4666e2763755f4c34806a783d0f13f03fd Mon Sep 17 00:00:00 2001 From: Bryan Lima <42067040+BryanMLima@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:25:55 +0200 Subject: [PATCH 4/4] Guest OS rules (#10098) Co-authored-by: Fabricio Duarte --- api/src/main/java/com/cloud/host/Host.java | 2 + .../apache/cloudstack/api/ApiConstants.java | 1 + .../api/command/admin/host/UpdateHostCmd.java | 11 ++- .../cloudstack/api/response/HostResponse.java | 12 +++ .../src/main/java/com/cloud/host/HostVO.java | 5 +- .../main/java/com/cloud/host/dao/HostDao.java | 7 ++ .../java/com/cloud/host/dao/HostDaoImpl.java | 37 ++++++- .../META-INF/db/views/cloud.host_view.sql | 1 + .../cloudstack/quota/QuotaManagerImpl.java | 2 +- .../response/QuotaResponseBuilderImpl.java | 2 +- .../manager/allocator/impl/BaseAllocator.java | 25 +++++ .../allocator/impl/FirstFitAllocator.java | 7 +- .../allocator/impl/RandomAllocator.java | 3 +- .../main/java/com/cloud/api/ApiDBUtils.java | 12 +++ .../cloud/api/query/dao/HostJoinDaoImpl.java | 1 + .../api/query/dao/StoragePoolJoinDaoImpl.java | 5 +- .../com/cloud/api/query/vo/HostJoinVO.java | 7 ++ .../ConfigurationManagerImpl.java | 8 +- .../deploy/DeploymentPlanningManagerImpl.java | 52 ++++++++-- .../cloud/resource/ResourceManagerImpl.java | 98 +++++++++++++------ .../cloud/storage/VolumeApiServiceImpl.java | 5 +- .../heuristics/HeuristicRuleHelper.java | 2 +- .../allocator/impl/FirstFitAllocatorTest.java | 18 ++-- .../allocator/impl/RandomAllocatorTest.java | 8 ++ ui/public/locales/en.json | 2 + ui/public/locales/pt_BR.json | 2 + ui/src/views/infra/HostUpdate.vue | 32 +++++- ...RuleHelper.java => GenericRuleHelper.java} | 57 +++++++---- .../utils/jsinterpreter/JsInterpreter.java | 6 +- .../jsinterpreter/JsInterpreterTest.java | 4 +- 30 files changed, 341 insertions(+), 93 deletions(-) rename utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/{TagAsRuleHelper.java => GenericRuleHelper.java} (61%) diff --git a/api/src/main/java/com/cloud/host/Host.java b/api/src/main/java/com/cloud/host/Host.java index b5234820151..8b14cfd3a39 100644 --- a/api/src/main/java/com/cloud/host/Host.java +++ b/api/src/main/java/com/cloud/host/Host.java @@ -63,6 +63,8 @@ public interface Host extends StateObject, Identity, Partition, HAResour String HOST_OVFTOOL_VERSION = "host.ovftool.version"; String HOST_VIRTV2V_VERSION = "host.virtv2v.version"; String HOST_SSH_PORT = "host.ssh.port"; + String GUEST_OS_CATEGORY_ID = "guest.os.category.id"; + String GUEST_OS_RULE = "guest.os.rule"; int DEFAULT_SSH_PORT = 22; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index c5671d22d07..17416f08690 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -449,6 +449,7 @@ public class ApiConstants { public static final String MAX_VGPU_PER_PHYSICAL_GPU = "maxvgpuperphysicalgpu"; public static final String GUEST_OS_LIST = "guestoslist"; public static final String GUEST_OS_COUNT = "guestoscount"; + public static final String GUEST_OS_RULE = "guestosrule"; public static final String OS_MAPPING_CHECK_ENABLED = "osmappingcheckenabled"; public static final String OUTOFBANDMANAGEMENT_POWERSTATE = "outofbandmanagementpowerstate"; public static final String OUTOFBANDMANAGEMENT_ENABLED = "outofbandmanagementenabled"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java index c085abd42c7..69bca7c79e2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java @@ -49,9 +49,14 @@ public class UpdateHostCmd extends BaseCmd { @Parameter(name = ApiConstants.OS_CATEGORY_ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class, - description = "The ID of OS category to update the host with") + description = "the ID of OS category used to prioritize VMs with matching OS category during the allocation process. " + + "It cannot be used alongside the 'guestosrule' parameter.") private Long osCategoryId; + @Parameter(name = ApiConstants.GUEST_OS_RULE, type = CommandType.STRING, description = "the guest OS rule written in JavaScript to match with the OS of the VM." + + "It cannot be used alongside the 'oscategoryid' parameter.") + private String guestOsRule; + @Parameter(name = ApiConstants.ALLOCATION_STATE, type = CommandType.STRING, description = "Change resource state of host, valid values are [Enable, Disable]. Operation may failed if host in states not allowing Enable/Disable") @@ -96,6 +101,10 @@ public class UpdateHostCmd extends BaseCmd { return osCategoryId; } + public String getGuestOsRule() { + return guestOsRule; + } + public String getAllocationState() { return allocationState; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java index 5d085d1bee0..f8e1af8d5b7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java @@ -63,6 +63,10 @@ public class HostResponse extends BaseResponseWithAnnotations { @Param(description = "The OS category name of the host") private String osCategoryName; + @SerializedName(ApiConstants.GUEST_OS_RULE) + @Param(description = "the guest OS rule") + private String guestOsRule; + @SerializedName(ApiConstants.IP_ADDRESS) @Param(description = "The IP address of the host") private String ipAddress; @@ -999,4 +1003,12 @@ public class HostResponse extends BaseResponseWithAnnotations { public String getExtensionName() { return extensionName; } + + public String getGuestOsRule() { + return guestOsRule; + } + + public void setGuestOsRule(String guestOsRule) { + this.guestOsRule = guestOsRule; + } } diff --git a/engine/schema/src/main/java/com/cloud/host/HostVO.java b/engine/schema/src/main/java/com/cloud/host/HostVO.java index d51b4eca057..468caf1e227 100644 --- a/engine/schema/src/main/java/com/cloud/host/HostVO.java +++ b/engine/schema/src/main/java/com/cloud/host/HostVO.java @@ -45,7 +45,7 @@ import javax.persistence.Transient; import com.cloud.cpu.CPU; import org.apache.cloudstack.util.CPUArchConverter; import org.apache.cloudstack.util.HypervisorTypeConverter; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.BooleanUtils; @@ -856,7 +856,8 @@ public class HostVO implements Host { } if (BooleanUtils.isTrue(this.getIsTagARule())) { - return TagAsRuleHelper.interpretTagAsRule(this.getHostTags().get(0), serviceOffering.getHostTag(), HostTagsDao.hostTagRuleExecutionTimeout.value()); + return GenericRuleHelper.interpretTagAsRule(this.getHostTags().get(0), serviceOffering.getHostTag(), HostTagsDao.hostTagRuleExecutionTimeout.value(), + HostTagsDao.hostTagRuleExecutionTimeout.key()); } if (StringUtils.isEmpty(serviceOffering.getHostTag())) { diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java index 5f5b2affee0..5c0d04fb2be 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java @@ -32,12 +32,17 @@ import com.cloud.utils.Pair; import com.cloud.utils.db.GenericDao; import com.cloud.utils.fsm.StateDao; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.framework.config.ConfigKey; /** * Data Access Object for server * */ public interface HostDao extends GenericDao, StateDao { + + ConfigKey guestOsRuleExecutionTimeout = new ConfigKey<>("Advanced", Long.class, "guest.os.rule.execution.timeout", "3000", "The maximum runtime, in milliseconds, " + + "to execute a guest OS rule; if it is reached, a timeout will happen.", true); + long countBy(long clusterId, ResourceState... states); Integer countAllByType(final Host.Type type); @@ -220,6 +225,8 @@ public interface HostDao extends GenericDao, StateDao findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags); + List findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(String templateGuestOSName); + List findClustersThatMatchHostTagRule(String computeOfferingTags); List listSsvmHostsWithPendingMigrateJobsOrderedByJobCount(); diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java index 99c9a979c3b..cd4423dfa26 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java @@ -37,7 +37,9 @@ import javax.persistence.TableGenerator; import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.commons.collections.CollectionUtils; import com.cloud.agent.api.VgpuTypesInfo; @@ -84,7 +86,7 @@ import org.apache.commons.lang3.ObjectUtils; @DB @TableGenerator(name = "host_req_sq", table = "op_host", pkColumnName = "id", valueColumnName = "sequence", allocationSize = 1) -public class HostDaoImpl extends GenericDaoBase implements HostDao { //FIXME: , ExternalIdDao { +public class HostDaoImpl extends GenericDaoBase implements HostDao, Configurable { private static final String LIST_HOST_IDS_BY_HOST_TAGS = "SELECT filtered.host_id, COUNT(filtered.tag) AS tag_count " + "FROM (SELECT host_id, tag, is_tag_a_rule FROM host_tags GROUP BY host_id,tag,is_tag_a_rule) AS filtered " @@ -1520,11 +1522,13 @@ public class HostDaoImpl extends GenericDaoBase implements HostDao } } + @Override public List findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags) { List hostTagVOList = _hostTagsDao.findHostRuleTags(); List result = new ArrayList<>(); for (HostTagVO rule: hostTagVOList) { - if (TagAsRuleHelper.interpretTagAsRule(rule.getTag(), computeOfferingTags, HostTagsDao.hostTagRuleExecutionTimeout.value())) { + if (GenericRuleHelper.interpretTagAsRule(rule.getTag(), computeOfferingTags, HostTagsDao.hostTagRuleExecutionTimeout.value(), + HostTagsDao.hostTagRuleExecutionTimeout.key())) { result.add(findById(rule.getHostId())); } } @@ -1532,6 +1536,23 @@ public class HostDaoImpl extends GenericDaoBase implements HostDao return result; } + @Override + public List findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(String templateGuestOSName) { + List hostIdsWithGuestOsRule = _detailsDao.findByName(Host.GUEST_OS_RULE); + List hostsWithIncompatibleRules = new ArrayList<>(); + for (DetailVO guestOsRule : hostIdsWithGuestOsRule) { + if (!GenericRuleHelper.interpretGuestOsRule(guestOsRule.getValue(), templateGuestOSName, HostDao.guestOsRuleExecutionTimeout.value(), + HostDao.guestOsRuleExecutionTimeout.key())) { + logger.trace("The guest OS rule [{}] of the host with ID [{}] is incompatible with the OS of the VM.", + guestOsRule.getHostId(), guestOsRule.getValue()); + hostsWithIncompatibleRules.add(findById(guestOsRule.getHostId())); + } + } + logger.trace("The hosts with the following IDs [{}] are incompatible with the VM considering their guest OS rule.", + hostsWithIncompatibleRules); + return hostsWithIncompatibleRules; + } + public List findClustersThatMatchHostTagRule(String computeOfferingTags) { Set result = new HashSet<>(); List hosts = findHostsWithTagRuleThatMatchComputeOfferingTags(computeOfferingTags); @@ -1984,4 +2005,14 @@ public class HostDaoImpl extends GenericDaoBase implements HostDao return customSearch(sc, null); } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] {guestOsRuleExecutionTimeout}; + } + + @Override + public String getConfigComponentName() { + return HostDaoImpl.class.getSimpleName(); + } } diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql index d9f4e267159..255ce866c42 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql @@ -64,6 +64,7 @@ SELECT guest_os_category.id guest_os_category_id, guest_os_category.uuid guest_os_category_uuid, guest_os_category.name guest_os_category_name, + (SELECT `value` FROM `cloud`.`host_details` `hd` WHERE `hd`.`host_id` = `cloud`.`host`.`id` AND `hd`.`name` = 'guest.os.rule') AS `guest_os_rule`, mem_caps.used_capacity memory_used_capacity, mem_caps.reserved_capacity memory_reserved_capacity, cpu_caps.used_capacity cpu_used_capacity, diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java index 20bc0b015bb..5afef8bc95b 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java @@ -320,7 +320,7 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { Map>> mapUsageAndQuotaUsage = new LinkedHashMap<>(); - try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) { + try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value(), QuotaConfig.QuotaActivationRuleTimeout.key())) { for (UsageVO usageRecord : usageRecords) { int usageType = usageRecord.getUsageType(); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 77099bcdc88..b71057e6423 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -1115,7 +1115,7 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { addAllPresetVariables(PresetVariables.class, quotaType, usageTypeVariablesAndDescriptions, null); List usageTypeVariables = usageTypeVariablesAndDescriptions.stream().map(Pair::first).collect(Collectors.toList()); - try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) { + try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value(), QuotaConfig.QuotaActivationRuleTimeout.key())) { Map newVariables = injectUsageTypeVariables(jsInterpreter, usageTypeVariables); String scriptToExecute = jsInterpreterHelper.replaceScriptVariables(activationRule, newVariables); jsInterpreter.executeScript(String.format("new Function(\"%s\")", scriptToExecute.replaceAll("\n", ""))); diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java index 58fcc62cdc3..ad5726e0a01 100644 --- a/server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java @@ -17,11 +17,13 @@ package com.cloud.agent.manager.allocator.impl; import com.cloud.agent.manager.allocator.HostAllocator; +import com.cloud.api.ApiDBUtils; import com.cloud.capacity.CapacityManager; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.offering.ServiceOffering; +import com.cloud.storage.VMTemplateVO; import com.cloud.utils.Pair; import com.cloud.utils.component.AdapterBase; import org.apache.commons.collections.CollectionUtils; @@ -69,6 +71,29 @@ public abstract class BaseAllocator extends AdapterBase implements HostAllocator clusterHosts.addAll(hostsWithTagRules); } + protected void filterHostsBasedOnGuestOsRules(VMTemplateVO vmTemplate, List clusterHosts) { + if (clusterHosts.isEmpty()) { + logger.info("Will not filter hosts based on guest OS as there is no available hosts left to verify."); + return; + } + + String templateGuestOSName = ApiDBUtils.getTemplateGuestOSName(vmTemplate); + List incompatibleHosts = hostDao.findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(templateGuestOSName); + + if (incompatibleHosts.isEmpty()) { + logger.info("No incompatible hosts found with guest OS rules matching the VM guest OS [{}].", templateGuestOSName); + return; + } + + logger.info("Found incompatible hosts {} with guest OS rules that did not match the VM guest OS [{}]. They will be removed from the suitable hosts list.", + incompatibleHosts, templateGuestOSName); + clusterHosts.removeAll(incompatibleHosts); + + if (clusterHosts.isEmpty()) { + logger.info("After filtering by guest OS rules, no compatible hosts were found for VM with OS [{}].", templateGuestOSName); + } + } + /** * Adds hosts with enough CPU capability and enough CPU capacity to the suitable hosts list. */ diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java index 8b4ca318bf7..4bc34d8a5c6 100644 --- a/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java @@ -123,7 +123,7 @@ public class FirstFitAllocator extends BaseAllocator { String hostTagOnTemplate = template.getTemplateTag(); String paramAsStringToLog = String.format("zone [%s], pod [%s], cluster [%s]", dcId, podId, clusterId); - List suitableHosts = retrieveHosts(vmProfile, type, (List) hosts, clusterId, podId, dcId, hostTagOnOffering, hostTagOnTemplate); + List suitableHosts = retrieveHosts(vmProfile, type, (List) hosts, template, clusterId, podId, dcId, hostTagOnOffering, hostTagOnTemplate); if (suitableHosts.isEmpty()) { logger.info("No suitable host found for VM [{}] in {}.", vmProfile, paramAsStringToLog); @@ -137,8 +137,8 @@ public class FirstFitAllocator extends BaseAllocator { return allocateTo(vmProfile, plan, offering, template, avoid, suitableHosts, returnUpTo, considerReservedCapacity, account); } - protected List retrieveHosts(VirtualMachineProfile vmProfile, Type type, List hostsToFilter, Long clusterId, Long podId, long dcId, String hostTagOnOffering, - String hostTagOnTemplate) { + protected List retrieveHosts(VirtualMachineProfile vmProfile, Type type, List hostsToFilter, VMTemplateVO template, + Long clusterId, Long podId, long dcId, String hostTagOnOffering, String hostTagOnTemplate) { String haVmTag = (String) vmProfile.getParameter(VirtualMachineProfile.Param.HaTag); List clusterHosts; @@ -159,6 +159,7 @@ public class FirstFitAllocator extends BaseAllocator { filterHostsWithUefiEnabled(type, vmProfile, clusterId, podId, dcId, clusterHosts); addHostsBasedOnTagRules(hostTagOnOffering, clusterHosts); + filterHostsBasedOnGuestOsRules(template, clusterHosts); return clusterHosts; diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java index 96b4255b876..92fdaf7faf0 100644 --- a/server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java @@ -42,7 +42,7 @@ public class RandomAllocator extends BaseAllocator { private ResourceManager _resourceMgr; protected List findSuitableHosts(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, List hosts, int returnUpTo, - boolean considerReservedCapacity) { + boolean considerReservedCapacity) { if (type == Host.Type.Storage) { return null; } @@ -115,6 +115,7 @@ public class RandomAllocator extends BaseAllocator { } addHostsBasedOnTagRules(offeringHostTag, availableHosts); + filterHostsBasedOnGuestOsRules(template, availableHosts); return availableHosts; } diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 1d00e9ec16b..d8b3c155ae8 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -31,6 +31,7 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; import com.cloud.cpu.CPU; +import com.cloud.storage.GuestOSVO; import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.affinity.AffinityGroup; @@ -2345,4 +2346,15 @@ public class ApiDBUtils { public static List listZoneClustersArchs(long zoneId) { return s_clusterDao.getClustersArchsByZone(zoneId); } + + public static String getTemplateGuestOSName(VMTemplateVO template) { + long guestOSId = template.getGuestOSId(); + GuestOSVO guestOS = s_guestOSDao.findById(guestOSId); + + if (guestOS == null) { + return null; + } + + return guestOS.getDisplayName(); + } } diff --git a/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java index 0200dec2cd3..b7464ba987c 100644 --- a/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java @@ -225,6 +225,7 @@ public class HostJoinDaoImpl extends GenericDaoBase implements hostResponse.setHaHost(containsHostHATag(hostTags)); hostResponse.setExplicitHostTags(host.getExplicitTag()); hostResponse.setImplicitHostTags(host.getImplicitTag()); + hostResponse.setGuestOsRule(host.getGuestOsRule()); hostResponse.setHypervisorVersion(host.getHypervisorVersion()); if (host.getArch() != null) { diff --git a/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java index a9d5b726b86..6c8e683d7b3 100644 --- a/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java @@ -35,7 +35,7 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.springframework.stereotype.Component; @@ -401,7 +401,8 @@ public class StoragePoolJoinDaoImpl extends GenericDaoBase, Configurable { return false; } } + + DetailVO guestOsRule = _hostDetailsDao.findDetail(host.getId(), HostVO.GUEST_OS_RULE); + if (guestOsRule != null) { + return isHostGuestOsCompatible(vmProfile, host, guestOsRule); + } + + DetailVO guestOsCategoryId = _hostDetailsDao.findDetail(host.getId(), HostVO.GUEST_OS_CATEGORY_ID); + if (guestOsCategoryId != null) { + return isHostGuestOsCategoryCompatible(vmProfile, host, guestOsCategoryId); + } + + return true; + } + + private boolean isHostGuestOsCategoryCompatible(VirtualMachineProfile vmProfile, HostVO host, DetailVO guestOsCategoryId) { long guestOSId = vmProfile.getTemplate().getGuestOSId(); GuestOSVO guestOS = _guestOSDao.findById(guestOSId); - if (guestOS != null) { - long guestOSCategoryId = guestOS.getCategoryId(); - DetailVO hostDetail = _hostDetailsDao.findDetail(host.getId(), "guest.os.category.id"); - if (hostDetail != null) { - String guestOSCategoryIdString = hostDetail.getValue(); - if (String.valueOf(guestOSCategoryId) != guestOSCategoryIdString) { - logger.debug("The last host has different guest.os.category.id than guest os category of VM, skipping"); - return false; - } - } + + if (guestOS == null) { + return true; + } + + long guestOSCategoryId = guestOS.getCategoryId(); + String guestOSCategoryIdString = guestOsCategoryId.getValue(); + if (!String.valueOf(guestOSCategoryId).equals(guestOSCategoryIdString)) { + logger.debug("The last host has different `{}` than guest os category of VM, skipping", Host.GUEST_OS_CATEGORY_ID); + return false; + } + + return true; + } + + private boolean isHostGuestOsCompatible(VirtualMachineProfile vmProfile, HostVO host, DetailVO guestOsRule) { + VMTemplateVO template = (VMTemplateVO) vmProfile.getTemplate(); + String templateGuestOSName = ApiDBUtils.getTemplateGuestOSName(template); + + List incompatibleHosts = _hostDao.findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(templateGuestOSName); + + boolean isHostCompatible = incompatibleHosts.stream().noneMatch(incompatibleHost -> incompatibleHost.getId() == host.getId()); + + if (!isHostCompatible) { + logger.debug("The template [{}] is incompatible with the host [{}] due to the guest OS rule [{}].", template, host, guestOsRule); + return false; } return true; } diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index 0f6933664f9..8db55790fd8 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java @@ -84,8 +84,8 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.ObjectUtils; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; @@ -2046,31 +2046,74 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, _hostDao.update(host.getId(), host); } - private void updateHostGuestOSCategory(Long hostId, Long guestOSCategoryId) { - // Verify that the guest OS Category exists - if (!(guestOSCategoryId > 0) || _guestOSCategoryDao.findById(guestOSCategoryId) == null) { + /** + * Updates the guest OS details related to the host ({@code guest.os.rule} and {@code guest.os.category.id}); only one can be set at a time for a given host. + */ + private void updateGuestOsRelatedFields(Long hostId, Long cmdOsCategoryId, String cmdGuestOsRule) { + if (ObjectUtils.allNotNull(cmdOsCategoryId, cmdGuestOsRule)) { + throw new InvalidParameterValueException("Informing both guest OS category and guest OS rule is invalid; please, inform only one of them."); + } + + if (cmdOsCategoryId != null) { + updateHostCategoryOs(hostId, cmdOsCategoryId); + } else if (cmdGuestOsRule != null) { + updateHostGuestOsRule(hostId, cmdGuestOsRule); + } + } + + /** + * Associates/removes a guest OS rule to a host and removes any guest OS category associated with it. + */ + private void updateHostGuestOsRule(Long hostId, String cmdGuestOsRule) { + final DetailVO hostGuestOsRule = _hostDetailsDao.findDetail(hostId, Host.GUEST_OS_RULE); + + if (StringUtils.isNotBlank(cmdGuestOsRule)) { + createOrUpdateHostDetails(hostGuestOsRule, cmdGuestOsRule, Host.GUEST_OS_RULE, hostId); + } else if (hostGuestOsRule != null) { + _hostDetailsDao.remove(hostGuestOsRule.getId()); + } + removeHostDetails(hostId, Host.GUEST_OS_CATEGORY_ID); + } + + /** + * Removes a host details if it exists. + */ + private void removeHostDetails(Long hostId, String detailString) { + DetailVO detailVO = _hostDetailsDao.findDetail(hostId, detailString); + + if (detailVO != null) { + _hostDetailsDao.remove(detailVO.getId()); + } + } + + private void createOrUpdateHostDetails(DetailVO hostDetail, String detailValue, String detailName, Long hostId) { + if (hostDetail != null) { + hostDetail.setValue(detailValue); + _hostDetailsDao.update(hostDetail.getId(), hostDetail); + } else { + Map detail = new HashMap<>(); + detail.put(detailName, detailValue); + _hostDetailsDao.persist(hostId, detail); + } + } + + /** + * Associates/removes a valid guest OS category to a host and removes any guest OS rules associated with it. + */ + private void updateHostCategoryOs(Long hostId, Long cmdOsCategoryId) { + if (cmdOsCategoryId <= 0 || _guestOSCategoryDao.findById(cmdOsCategoryId) == null) { throw new InvalidParameterValueException("Please specify a valid guest OS category."); } - final GuestOSCategoryVO guestOSCategory = _guestOSCategoryDao.findById(guestOSCategoryId); - final DetailVO guestOSDetail = _hostDetailsDao.findDetail(hostId, "guest.os.category.id"); + GuestOSCategoryVO guestOSCategory = _guestOSCategoryDao.findById(cmdOsCategoryId); + DetailVO guestOSDetail = _hostDetailsDao.findDetail(hostId, Host.GUEST_OS_CATEGORY_ID); if (guestOSCategory != null && !GuestOSCategoryVO.CATEGORY_NONE.equalsIgnoreCase(guestOSCategory.getName())) { - // Create/Update an entry for guest.os.category.id - if (guestOSDetail != null) { - guestOSDetail.setValue(String.valueOf(guestOSCategory.getId())); - _hostDetailsDao.update(guestOSDetail.getId(), guestOSDetail); - } else { - final Map detail = new HashMap<>(); - detail.put("guest.os.category.id", String.valueOf(guestOSCategory.getId())); - _hostDetailsDao.persist(hostId, detail); - } - } else { - // Delete any existing entry for guest.os.category.id - if (guestOSDetail != null) { - _hostDetailsDao.remove(guestOSDetail.getId()); - } + createOrUpdateHostDetails(guestOSDetail, String.valueOf(guestOSCategory.getId()), Host.GUEST_OS_CATEGORY_ID, hostId); + } else if (guestOSDetail != null) { + _hostDetailsDao.remove(guestOSDetail.getId()); } + removeHostDetails(hostId, Host.GUEST_OS_RULE); } private void removeStorageAccessGroupsOnPodsInZone(long zoneId, List newStoragePoolTags, List tagsToDeleteOnZone) { @@ -2821,16 +2864,17 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, @Override public Host updateHost(final UpdateHostCmd cmd) throws NoTransitionException { - return updateHost(cmd.getId(), cmd.getName(), cmd.getOsCategoryId(), + return updateHost(cmd.getId(), cmd.getName(), cmd.getOsCategoryId(), cmd.getGuestOsRule(), cmd.getAllocationState(), cmd.getUrl(), cmd.getHostTags(), cmd.getIsTagARule(), cmd.getAnnotation(), false, cmd.getExternalDetails(), cmd.isCleanupExternalDetails()); } - private Host updateHost(Long hostId, String name, Long guestOSCategoryId, String allocationState, + private Host updateHost(Long hostId, String name, Long guestOSCategoryId, String guestOsRule, String allocationState, String url, List hostTags, Boolean isTagARule, String annotation, boolean isUpdateFromHostHealthCheck, Map externalDetails, boolean cleanupExternalDetails) throws NoTransitionException { jsInterpreterHelper.ensureInterpreterEnabledIfParameterProvided(ApiConstants.IS_TAG_A_RULE, Boolean.TRUE.equals(isTagARule)); + jsInterpreterHelper.ensureInterpreterEnabledIfParameterProvided(ApiConstants.GUEST_OS_RULE, guestOsRule != null); // Verify that the host exists final HostVO host = _hostDao.findById(hostId); @@ -2847,9 +2891,7 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, updateHostName(host, name); } - if (guestOSCategoryId != null) { - updateHostGuestOSCategory(hostId, guestOSCategoryId); - } + updateGuestOsRelatedFields(hostId, guestOSCategoryId, guestOsRule); if (hostTags != null) { updateHostTags(host, hostId, hostTags, isTagARule); @@ -2919,7 +2961,7 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, @Override public Host autoUpdateHostAllocationState(Long hostId, ResourceState.Event resourceEvent) throws NoTransitionException { - return updateHost(hostId, null, null, resourceEvent.toString(), null, null, null, null, true, null, false); + return updateHost(hostId, null, null, null, resourceEvent.toString(), null, null, null, null, true, null, false); } @Override @@ -3639,7 +3681,7 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, // If the server's private IP is the same as is public IP, this host has // a host-only private network. Don't check for conflicts with the // private IP address table. - if (!ObjectUtils.equals(serverPrivateIP, serverPublicIP)) { + if (!StringUtils.equals(serverPrivateIP, serverPublicIP)) { if (!_privateIPAddressDao.mark(dc.getId(), pod.getId(), serverPrivateIP)) { // If the server's private IP address is already in the // database, return false @@ -4284,7 +4326,7 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, return null; } else { _hostDao.loadDetails(host); - final DetailVO detail = _hostDetailsDao.findDetail(hostId, "guest.os.category.id"); + final DetailVO detail = _hostDetailsDao.findDetail(hostId, Host.GUEST_OS_CATEGORY_ID); if (detail == null) { return null; } else { diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index afe16c2b574..ee27e171a73 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -112,7 +112,7 @@ import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo; import org.apache.commons.collections.CollectionUtils; @@ -3904,7 +3904,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic boolean result; if (storagePoolTags.second()) { - result = TagAsRuleHelper.interpretTagAsRule(storageTagsList.get(0), diskOfferingTags, storageTagRuleExecutionTimeout.value()); + result = GenericRuleHelper.interpretTagAsRule(storageTagsList.get(0), diskOfferingTags, storageTagRuleExecutionTimeout.value(), + storageTagRuleExecutionTimeout.key()); } else { result = CollectionUtils.isSubCollection(Arrays.asList(newDiskOfferingTagsAsStringArray), storageTagsList); } diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java index beecf90d2b8..97c13f71447 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java @@ -257,7 +257,7 @@ public class HeuristicRuleHelper { * @return the {@link DataStore} returned by the script. */ public DataStore interpretHeuristicRule(String rule, HeuristicType heuristicType, Object obj, long zoneId) { - try (JsInterpreter jsInterpreter = new JsInterpreter(HEURISTICS_SCRIPT_TIMEOUT)) { + try (JsInterpreter jsInterpreter = new JsInterpreter(HEURISTICS_SCRIPT_TIMEOUT, StorageManager.HEURISTICS_SCRIPT_TIMEOUT.key())) { buildPresetVariables(jsInterpreter, heuristicType, zoneId, obj); Object scriptReturn = jsInterpreter.executeScript(rule); diff --git a/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java b/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java index 00cac5bbd52..f39ed06a656 100644 --- a/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java +++ b/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java @@ -193,7 +193,7 @@ public class FirstFitAllocatorTest { Mockito.doReturn(account).when(virtualMachineProfile).getOwner(); Mockito.doReturn(hostTag).when(serviceOffering).getHostTag(); Mockito.doReturn(templateTag).when(vmTemplateVO).getTemplateTag(); - Mockito.doReturn(emptyList).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + Mockito.doReturn(emptyList).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.any(VMTemplateVO.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); List suitableHosts = firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, type, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); Assert.assertNull(suitableHosts); @@ -208,7 +208,7 @@ public class FirstFitAllocatorTest { Mockito.doReturn(account).when(virtualMachineProfile).getOwner(); Mockito.doReturn(hostTag).when(serviceOffering).getHostTag(); Mockito.doReturn(templateTag).when(vmTemplateVO).getTemplateTag(); - Mockito.doReturn(hosts).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + Mockito.doReturn(hosts).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.any(VMTemplateVO.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); Mockito.doReturn(hosts).when(firstFitAllocatorSpy).allocateTo(Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class), Mockito.any(ServiceOffering.class), Mockito.any(VMTemplateVO.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyList(), Mockito.anyInt(), Mockito.anyBoolean(), Mockito.any(Account.class)); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsToAvoidSet(Mockito.any(Host.Type.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); List suitableHosts = firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, type, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); @@ -226,7 +226,7 @@ public class FirstFitAllocatorTest { Mockito.doReturn(account).when(virtualMachineProfile).getOwner(); Mockito.doReturn(hostTag).when(serviceOffering).getHostTag(); Mockito.doReturn(templateTag).when(vmTemplateVO).getTemplateTag(); - Mockito.doReturn(hosts).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + Mockito.doReturn(hosts).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.any(VMTemplateVO.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); Mockito.doReturn(hosts).when(firstFitAllocatorSpy).allocateTo(Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class), Mockito.any(ServiceOffering.class), Mockito.any(VMTemplateVO.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyList(), Mockito.anyInt(), Mockito.anyBoolean(), Mockito.any(Account.class)); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsToAvoidSet(Mockito.any(Host.Type.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, type, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); @@ -245,7 +245,8 @@ public class FirstFitAllocatorTest { Mockito.doReturn(hostsWithHaTag).when(hostDaoMock).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); - List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, clusterId, podId, dcId, hostTag, templateTag); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); + List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, vmTemplateVO, clusterId, podId, dcId, hostTag, templateTag); Assert.assertEquals(2, resultHosts.size()); Assert.assertEquals(host1, resultHosts.get(0)); @@ -262,7 +263,8 @@ public class FirstFitAllocatorTest { Mockito.doReturn(hostsWithHaTag).when(hostDaoMock).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); - List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, hostsToFilter, clusterId, podId, dcId, hostTag, templateTag); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); + List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, hostsToFilter, vmTemplateVO, clusterId, podId, dcId, hostTag, templateTag); Assert.assertEquals(2, resultHosts.size()); Assert.assertEquals(host1, resultHosts.get(0)); @@ -278,7 +280,8 @@ public class FirstFitAllocatorTest { Mockito.doReturn(upAndEnabledHostsWithNoHa).when(resourceManagerMock).listAllUpAndEnabledNonHAHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.nullable(String.class), Mockito.anyList()); - List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, clusterId, podId, dcId, null, null); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); + List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, vmTemplateVO, clusterId, podId, dcId, null, null); Assert.assertEquals(2, resultHosts.size()); Assert.assertEquals(host1, resultHosts.get(0)); @@ -293,7 +296,8 @@ public class FirstFitAllocatorTest { Mockito.doNothing().when(firstFitAllocatorSpy).retainHostsMatchingServiceOfferingAndTemplateTags(Mockito.anyList(), Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); - firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, clusterId, podId, dcId, hostTag, templateTag); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); + firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, vmTemplateVO, clusterId, podId, dcId, hostTag, templateTag); Mockito.verify(firstFitAllocatorSpy, Mockito.times(1)).retainHostsMatchingServiceOfferingAndTemplateTags(Mockito.anyList(), Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); } diff --git a/server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java b/server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java index ab9b322eb84..938ed7a152e 100644 --- a/server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java +++ b/server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java @@ -222,6 +222,7 @@ public class RandomAllocatorTest { Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, null, clusterId, podId, zoneId); Assert.assertEquals(1, availableHosts.size()); @@ -237,6 +238,7 @@ public class RandomAllocatorTest { Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, null, clusterId, podId, zoneId); Assert.assertEquals(2, availableHosts.size()); @@ -252,6 +254,7 @@ public class RandomAllocatorTest { Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, hostTag, clusterId, podId, zoneId); Assert.assertEquals(1, availableHosts.size()); @@ -267,6 +270,7 @@ public class RandomAllocatorTest { Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, hostTag, clusterId, podId, zoneId); Assert.assertEquals(2, availableHosts.size()); @@ -281,6 +285,7 @@ public class RandomAllocatorTest { Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, null, clusterId, podId, zoneId); Assert.assertEquals(1, availableHosts.size()); @@ -295,6 +300,7 @@ public class RandomAllocatorTest { Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, null, clusterId, podId, zoneId); Assert.assertEquals(2, availableHosts.size()); @@ -309,6 +315,7 @@ public class RandomAllocatorTest { Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, hostTag, clusterId, podId, zoneId); Assert.assertEquals(1, availableHosts.size()); @@ -323,6 +330,7 @@ public class RandomAllocatorTest { Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, hostTag, clusterId, podId, zoneId); Assert.assertEquals(2, availableHosts.size()); diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 3a16d613f37..7d848e82113 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1219,6 +1219,8 @@ "label.guestnetwork": "Guest Network", "label.guestnetworkid": "Network ID", "label.guestnetworkname": "Network name", +"label.guestosasrule": "Guest OS as rule", +"label.guestosrule": "Guest OS rule", "label.gueststartip": "Guest start IP", "label.guest.vlan": "Guest VLAN", "label.guestvlanrange": "VLAN range(s)", diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json index 91f0fd895e5..ad27813f1ce 100644 --- a/ui/public/locales/pt_BR.json +++ b/ui/public/locales/pt_BR.json @@ -1090,6 +1090,8 @@ "label.guestnetwork": "Rede guest", "label.guestnetworkid": "ID de rede", "label.guestnetworkname": "Nome da rede", +"label.guestosasrule": "SO guest como regra JS", +"label.guestosrule": "Regra SO guest", "label.gueststartip": "IP de in\u00edcio do guest", "label.guestvlanrange": "Intervalo(s) de VLAN", "label.guestvmcidr": "CIDR", diff --git a/ui/src/views/infra/HostUpdate.vue b/ui/src/views/infra/HostUpdate.vue index 1fee7a6846c..5ef762443e9 100644 --- a/ui/src/views/infra/HostUpdate.vue +++ b/ui/src/views/infra/HostUpdate.vue @@ -64,7 +64,22 @@ - + + + + + + + + + + @@ -149,6 +164,7 @@ export default { methods: { initForm () { this.formRef = ref() + const guestOsRule = this.resource?.guestosrule this.form = reactive({ name: this.resource.name, hosttags: this.resource.explicithosttags, @@ -157,7 +173,9 @@ export default { ? this.resource.storageaccessgroups.split(',') : [], oscategoryid: this.resource.oscategoryid, - externaldetails: this.resourceExternalDetails + externaldetails: this.resourceExternalDetails, + guestosasrule: guestOsRule !== undefined, + guestosrule: guestOsRule }) this.rules = reactive({}) }, @@ -194,7 +212,11 @@ export default { params.id = this.resource.id params.name = values.name params.hosttags = values.hosttags - params.oscategoryid = values.oscategoryid + if (values.guestosasrule === true) { + params.guestosrule = values.guestosrule + } else { + params.oscategoryid = values.oscategoryid || this.osCategories.opts.filter(os => os.name === 'None')[0]?.id + } if (values.istagarule !== undefined) { params.istagarule = values.istagarule } @@ -205,9 +227,11 @@ export default { } else { params.cleanupexternaldetails = true } + + Object.keys(params).forEach((key) => (params[key] == null) && delete params[key]) this.loading = true - postAPI('updateHost', params).then(json => { + postAPI('updateHost', params).then(() => { this.$message.success({ content: `${this.$t('label.action.update.host')} - ${values.name}`, duration: 2 diff --git a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/TagAsRuleHelper.java b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java similarity index 61% rename from utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/TagAsRuleHelper.java rename to utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java index b3013567352..506bbc1ea5c 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/TagAsRuleHelper.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java @@ -16,41 +16,60 @@ //under the License. package org.apache.cloudstack.utils.jsinterpreter; +import com.cloud.utils.StringUtils; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import com.cloud.utils.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +public class GenericRuleHelper { -import com.cloud.utils.exception.CloudRuntimeException; + protected static Logger LOGGER = LogManager.getLogger(GenericRuleHelper.class); -public class TagAsRuleHelper { - - protected static Logger LOGGER = LogManager.getLogger(TagAsRuleHelper.class); - - public static boolean interpretTagAsRule(String rule, String tags, long timeout) { + public static boolean interpretTagAsRule(String rule, String tags, long timeout, String configName) { List tagsPresetVariable = new ArrayList<>(); if (!StringUtils.isEmpty(tags)) { tagsPresetVariable.addAll(Arrays.asList(tags.split(","))); } - try (JsInterpreter jsInterpreter = new JsInterpreter(timeout)) { - jsInterpreter.injectVariable("tags", tagsPresetVariable); - Object scriptReturn = jsInterpreter.executeScript(rule); - if (scriptReturn instanceof Boolean) { - return (Boolean)scriptReturn; - } - } catch (IOException ex) { - String message = String.format("Error while executing script [%s].", rule); - LOGGER.error(message, ex); - throw new CloudRuntimeException(message, ex); + Boolean scriptReturn = interpretRule("tags", tagsPresetVariable, timeout, rule, configName); + + if (scriptReturn != null) { + return scriptReturn; } LOGGER.debug("Result of tag rule [{}] was not a boolean, returning false.", rule); return false; } + public static boolean interpretGuestOsRule(String rule, String vmGuestOs, long timeout, String configName) { + Boolean scriptReturn = interpretRule("vmGuestOs", vmGuestOs, timeout, rule, configName); + + if (scriptReturn != null) { + return scriptReturn; + } + + LOGGER.debug("Result of guest OS rule [{}] was not a boolean, returning false.", rule); + return false; + } + + private static Boolean interpretRule(String variableName, Object variableValue, long timeout, String script, String configName) { + try (JsInterpreter jsInterpreter = new JsInterpreter(timeout, configName)) { + jsInterpreter.injectVariable(variableName, variableValue); + Object scriptReturn = jsInterpreter.executeScript(script); + if (scriptReturn instanceof Boolean) { + return (Boolean) scriptReturn; + } + } catch (IOException ex) { + String message = String.format("Error while executing script [%s].", script); + LOGGER.error(message, ex); + throw new CloudRuntimeException(message, ex); + } + return null; + } + } diff --git a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java index 6e6ef2bbe59..85799e0f561 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java @@ -86,10 +86,10 @@ public class JsInterpreter implements Closeable { */ protected JsInterpreter() { } - public JsInterpreter(long timeout) { + public JsInterpreter(long timeout, String configName) { this.timeout = timeout; - this.timeoutDefaultMessage = String.format( - "Timeout (in milliseconds) defined in the global setting [quota.activationrule.timeout]: [%s].", this.timeout); + this.timeoutDefaultMessage = String.format("Timeout (in milliseconds) defined in the global setting [%s]: [%s].", + configName, this.timeout); if (System.getProperty("nashorn.args") == null) { System.setProperty("nashorn.args", "--no-java --no-syntax-extensions"); diff --git a/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java b/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java index e6ef43f2fc1..e4764b2af41 100644 --- a/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java +++ b/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java @@ -161,7 +161,7 @@ public class JsInterpreterTest { @Test public void executeScriptTestValidScriptShouldPassWithMixedVariables() { - try (JsInterpreter jsInterpreter = new JsInterpreter(1000)) { + try (JsInterpreter jsInterpreter = new JsInterpreter(1000, "timeout.configuration")) { jsInterpreter.injectVariable("x", 10); jsInterpreter.injectVariable("y", "hello"); jsInterpreter.injectVariable("z", true); @@ -174,7 +174,7 @@ public class JsInterpreterTest { } private void runMaliciousScriptFileTest(String script, String filename) { - try (JsInterpreter jsInterpreter = new JsInterpreter(1000)) { + try (JsInterpreter jsInterpreter = new JsInterpreter(1000, "timeout.configuration")) { jsInterpreter.executeScript(script); } catch (CloudRuntimeException ex) { Assert.assertTrue(ex.getMessage().contains("Unable to execute script"));