From b09acb8fd05d9b8bdf93bc817a95a1f4ed53af77 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Wed, 25 Oct 2023 01:58:42 -0300 Subject: [PATCH 01/13] UI: Fix deploy VM wizard vApp properties (#8072) This PR fixes the vApp properties values sent as part of the deployVirtualMachine API through the UI Fixes: #7998 --- ui/src/views/compute/DeployVM.vue | 125 ++++++++++++++++-------------- 1 file changed, 65 insertions(+), 60 deletions(-) diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index 5a962a37f36..9d07cb51db3 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -470,13 +470,13 @@ @@ -485,7 +485,7 @@ @@ -1509,61 +1509,7 @@ export default { }) } - if (this.vm.templateid && this.templateProperties && Object.keys(this.templateProperties).length > 0) { - this.templateProperties.forEach((props, category) => { - props.forEach((property, propertyIndex) => { - if (property.type && property.type === 'boolean') { - this.form['properties.' + this.escapePropertyKey(property.key)] = property.value === 'TRUE' - } else if (property.type && (property.type === 'int' || property.type === 'real')) { - this.form['properties.' + this.escapePropertyKey(property.key)] = property.value - } else if (property.type && property.type === 'string' && property.qualifiers && property.qualifiers.startsWith('ValueMap')) { - this.form['properties.' + this.escapePropertyKey(property.key)] = property.value && property.value.length > 0 - ? property.value - : this.getPropertyQualifiers(property.qualifiers, 'select')[0] - } else if (property.type && property.type === 'string' && property.password) { - this.form['properties.' + this.escapePropertyKey(property.key)] = property.value - this.rules['properties.' + this.escapePropertyKey(property.key)] = [{ - validator: async (rule, value) => { - if (!property.qualifiers) { - return Promise.resolve() - } - var minlength = this.getPropertyQualifiers(property.qualifiers, 'number-select').min - var maxlength = this.getPropertyQualifiers(property.qualifiers, 'number-select').max - var errorMessage = '' - var isPasswordInvalidLength = function () { - return false - } - if (minlength) { - errorMessage = this.$t('message.validate.minlength').replace('{0}', minlength) - isPasswordInvalidLength = function () { - return !value || value.length < minlength - } - } - if (maxlength !== Number.MAX_SAFE_INTEGER) { - if (minlength) { - errorMessage = this.$t('message.validate.range.length').replace('{0}', minlength).replace('{1}', maxlength) - isPasswordInvalidLength = function () { - return !value || (maxlength < value.length || value.length < minlength) - } - } else { - errorMessage = this.$t('message.validate.maxlength').replace('{0}', maxlength) - isPasswordInvalidLength = function () { - return !value || value.length > maxlength - } - } - } - if (isPasswordInvalidLength()) { - return Promise.reject(errorMessage) - } - return Promise.resolve() - } - }] - } else { - this.form['properties.' + this.escapePropertyKey(property.key)] = property.value - } - }) - }) - } + this.updateFormProperties() if (this.vm.templateid && this.templateLicenses && this.templateLicenses.length > 0) { this.rules.licensesaccepted = [{ @@ -2582,6 +2528,64 @@ export default { this.dataPreFill.memory = params.memory this.handleSearchFilter('serviceOfferings', params) }, + updateFormProperties () { + if (this.vm.templateid && this.templateProperties && Object.keys(this.templateProperties).length > 0) { + this.form.properties = {} + Object.keys(this.templateProperties).forEach((category, categoryIndex) => { + this.templateProperties[category].forEach((property, _) => { + if (property.type && property.type === 'boolean') { + this.form.properties[this.escapePropertyKey(property.key)] = property.value === 'TRUE' + } else if (property.type && (property.type === 'int' || property.type === 'real')) { + this.form.properties[this.escapePropertyKey(property.key)] = property.value + } else if (property.type && property.type === 'string' && property.qualifiers && property.qualifiers.startsWith('ValueMap')) { + this.form.properties[this.escapePropertyKey(property.key)] = property.value && property.value.length > 0 + ? property.value + : this.getPropertyQualifiers(property.qualifiers, 'select')[0] + } else if (property.type && property.type === 'string' && property.password) { + this.form.properties[this.escapePropertyKey(property.key)] = property.value + this.rules['properties.' + this.escapePropertyKey(property.key)] = [{ + validator: async (rule, value) => { + if (!property.qualifiers) { + return Promise.resolve() + } + var minlength = this.getPropertyQualifiers(property.qualifiers, 'number-select').min + var maxlength = this.getPropertyQualifiers(property.qualifiers, 'number-select').max + var errorMessage = '' + var isPasswordInvalidLength = function () { + return false + } + if (minlength) { + errorMessage = this.$t('message.validate.minlength').replace('{0}', minlength) + isPasswordInvalidLength = function () { + return !value || value.length < minlength + } + } + if (maxlength !== Number.MAX_SAFE_INTEGER) { + if (minlength) { + errorMessage = this.$t('message.validate.range.length').replace('{0}', minlength).replace('{1}', maxlength) + isPasswordInvalidLength = function () { + return !value || (maxlength < value.length || value.length < minlength) + } + } else { + errorMessage = this.$t('message.validate.maxlength').replace('{0}', maxlength) + isPasswordInvalidLength = function () { + return !value || value.length > maxlength + } + } + } + if (isPasswordInvalidLength()) { + return Promise.reject(errorMessage) + } + return Promise.resolve() + } + }] + } else { + this.form.properties[this.escapePropertyKey(property.key)] = property.value + } + }) + }) + } + }, updateTemplateParameters () { if (this.template) { this.templateNics = this.fetchTemplateNics(this.template) @@ -2599,6 +2603,7 @@ export default { this.updateComputeOffering(null) // reset as existing selection may be incompatible } }, 500) + this.updateFormProperties() } }, onSelectTemplateConfigurationId (value) { From ae724b9d4c234968965c145a23bcf1b24e2a9052 Mon Sep 17 00:00:00 2001 From: Harikrishna Date: Wed, 25 Oct 2023 11:01:01 +0530 Subject: [PATCH 02/13] Fix EULA section while parsing OVF file (#8081) * Fix EULA section while parsing OVF file * Updated unit test to check both the cases * Added separate tests Fixes #8039 --- .../cloud/agent/api/storage/OVFHelper.java | 6 +- .../agent/api/storage/OVFHelperTest.java | 131 ++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/cloud/agent/api/storage/OVFHelper.java b/api/src/main/java/com/cloud/agent/api/storage/OVFHelper.java index c8313ccd664..d4daf0e4270 100644 --- a/api/src/main/java/com/cloud/agent/api/storage/OVFHelper.java +++ b/api/src/main/java/com/cloud/agent/api/storage/OVFHelper.java @@ -807,9 +807,11 @@ public class OVFHelper { String eulaLicense = null; for (int i = 0; i < eulaChildNodes.getLength(); i++) { Node eulaItem = eulaChildNodes.item(i); - if (eulaItem.getNodeName().equalsIgnoreCase("Info")) { + if (eulaItem.getNodeName().equalsIgnoreCase("Info") + || eulaItem.getNodeName().endsWith(":Info")) { eulaInfo = eulaItem.getTextContent(); - } else if (eulaItem.getNodeName().equalsIgnoreCase("License")) { + } else if (eulaItem.getNodeName().equalsIgnoreCase("License") + || eulaItem.getNodeName().endsWith(":License")) { eulaLicense = eulaItem.getTextContent(); } } diff --git a/api/src/test/java/com/cloud/agent/api/storage/OVFHelperTest.java b/api/src/test/java/com/cloud/agent/api/storage/OVFHelperTest.java index f52d3045e40..4554e7150c4 100644 --- a/api/src/test/java/com/cloud/agent/api/storage/OVFHelperTest.java +++ b/api/src/test/java/com/cloud/agent/api/storage/OVFHelperTest.java @@ -393,6 +393,131 @@ public class OVFHelperTest { "\n" + ""; + private String eulaSectionsWithOVFprefix = + "\n" + + "\n" + + " end-user license agreement\n" + + " END USER LICENSE AGREEMENT\n" + + "\n" + + "IMPORTANT: PLEASE READ THIS END USER LICENSE AGREEMENT CAREFULLY. IT IS VERY IMPORTANT THAT YOU CHECK THAT YOU ARE PURCHASING CISCO SOFTWARE OR EQUIPMENT FROM AN APPROVED SOURCE AND THAT YOU, OR THE ENTITY YOU REPRESENT (COLLECTIVELY, THE \"CUSTOMER\") HAVE BEEN REGISTERED AS THE END USER FOR THE PURPOSES OF THIS CISCO END USER LICENSE AGREEMENT. IF YOU ARE NOT REGISTERED AS THE END USER YOU HAVE NO LICENSE TO USE THE SOFTWARE AND THE LIMITED WARRANTY IN THIS END USER LICENSE AGREEMENT DOES NOT APPLY. ASSUMING YOU HAVE PURCHASED FROM AN APPROVED SOURCE, DOWNLOADING, INSTALLING OR USING CISCO OR CISCO-SUPPLIED SOFTWARE CONSTITUTES ACCEPTANCE OF THIS AGREEMENT.\n" + + "\n" + + "CISCO SYSTEMS, INC. OR ITS AFFILIATE LICENSING THE SOFTWARE (\"CISCO\") IS WILLING TO LICENSE THIS SOFTWARE TO YOU ONLY UPON THE CONDITION THAT YOU PURCHASED THE SOFTWARE FROM AN APPROVED SOURCE AND THAT YOU ACCEPT ALL OF THE TERMS CONTAINED IN THIS END USER LICENSE AGREEMENT PLUS ANY ADDITIONAL LIMITATIONS ON THE LICENSE SET FORTH IN A SUPPLEMENTAL LICENSE AGREEMENT ACCOMPANYING THE PRODUCT, MADE AVAILABLE AT THE TIME OF YOUR ORDER, OR POSTED ON THE CISCO WEBSITE AT www.cisco.com/go/terms (COLLECTIVELY THE \"AGREEMENT\"). TO THE EXTENT OF ANY CONFLICT BETWEEN THE TERMS OF THIS END USER LICENSE AGREEMENT AND ANY SUPPLEMENTAL LICENSE AGREEMENT, THE SUPPLEMENTAL LICENSE AGREEMENT SHALL APPLY. BY DOWNLOADING, INSTALLING, OR USING THE SOFTWARE, YOU ARE REPRESENTING THAT YOU PURCHASED THE SOFTWARE FROM AN APPROVED SOURCE AND BINDING YOURSELF TO THE AGREEMENT. IF YOU DO " + + "NOT AGREE TO ALL OF THE TERMS OF THE AGREEMENT, THEN CISCO IS UNWILLING TO LICENSE THE SOFTWARE TO YOU AND (A) YOU MAY NOT DOWNLOAD, INSTALL OR USE THE SOFTWARE, AND (B) YOU MAY RETURN THE SOFTWARE (INCLUDING ANY UNOPENED CD PACKAGE AND ANY WRITTEN MATERIALS) FOR A FULL REFUND, OR, IF THE SOFTWARE AND WRITTEN MATERIALS ARE SUPPLIED AS PART OF ANOTHER PRODUCT, YOU MAY RETURN THE ENTIRE PRODUCT FOR A FULL REFUND. YOUR RIGHT TO RETURN AND REFUND EXPIRES 30 DAYS AFTER PURCHASE FROM AN APPROVED SOURCE, AND APPLIES ONLY IF YOU ARE THE ORIGINAL AND REGISTERED END USER PURCHASER. FOR THE PURPOSES OF THIS END USER LICENSE AGREEMENT, AN \"APPROVED SOURCE\" MEANS (A) CISCO; OR (B) A DISTRIBUTOR OR SYSTEMS INTEGRATOR AUTHORIZED BY CISCO TO DISTRIBUTE / SELL CISCO EQUIPMENT, SOFTWARE AND SERVICES WITHIN YOUR TERRITORY TO END " + + "USERS; OR (C) A RESELLER AUTHORIZED BY ANY SUCH DISTRIBUTOR OR SYSTEMS INTEGRATOR IN ACCORDANCE WITH THE TERMS OF THE DISTRIBUTOR'S AGREEMENT WITH CISCO TO DISTRIBUTE / SELL THE CISCO EQUIPMENT, SOFTWARE AND SERVICES WITHIN YOUR TERRITORY TO END USERS.\n" + + "\n" + + "THE FOLLOWING TERMS OF THE AGREEMENT GOVERN CUSTOMER'S USE OF THE SOFTWARE (DEFINED BELOW), EXCEPT TO THE EXTENT: (A) THERE IS A SEPARATE SIGNED CONTRACT BETWEEN CUSTOMER AND CISCO GOVERNING CUSTOMER'S USE OF THE SOFTWARE, OR (B) THE SOFTWARE INCLUDES A SEPARATE \"CLICK-ACCEPT\" LICENSE AGREEMENT OR THIRD PARTY LICENSE AGREEMENT AS PART OF THE INSTALLATION OR DOWNLOAD PROCESS GOVERNING CUSTOMER'S USE OF THE SOFTWARE. TO THE EXTENT OF A CONFLICT BETWEEN THE PROVISIONS OF THE FOREGOING DOCUMENTS, THE ORDER OF PRECEDENCE SHALL BE (1)THE SIGNED CONTRACT, (2) THE CLICK-ACCEPT AGREEMENT OR THIRD PARTY LICENSE AGREEMENT, AND (3) THE AGREEMENT. FOR PURPOSES OF THE AGREEMENT, \"SOFTWARE\" SHALL MEAN COMPUTER PROGRAMS, INCLUDING FIRMWARE AND COMPUTER PROGRAMS EMBEDDED IN CISCO EQUIPMENT, AS PROVIDED TO CUSTOMER BY AN APPROVED SOURCE, AND ANY UPGRADES, UPDATES, BUG FIXES " + + "OR MODIFIED VERSIONS THERETO (COLLECTIVELY, \"UPGRADES\"), ANY OF THE SAME WHICH HAS BEEN RELICENSED UNDER THE CISCO SOFTWARE TRANSFER AND RE-LICENSING POLICY (AS MAY BE AMENDED BY CISCO FROM TIME TO TIME) OR BACKUP COPIES OF ANY OF THE FOREGOING.\n" + + "\n" + + "License. Conditioned upon compliance with the terms and conditions of the Agreement, Cisco grants to Customer a nonexclusive and nontransferable license to use for Customer's internal business purposes the Software and the Documentation for which Customer has paid the required license fees to an Approved Source. \"Documentation\" means written information (whether contained in user or technical manuals, training materials, specifications or otherwise) pertaining to the Software and made available by an Approved Source with the Software in any manner (including on CD-Rom, or on-line). In order to use the Software, Customer may be required to input a registration number or product authorization key and register Customer's copy of the Software online at Cisco's website to obtain the necessary license key or license file.\n" + + "\n" + + "Customer's license to use the Software shall be limited to, and Customer shall not use the Software in excess of, a single hardware chassis or card or such other limitations as are set forth in the applicable Supplemental License Agreement or in the applicable purchase order which has been accepted by an Approved Source and for which Customer has paid to an Approved Source the required license fee (the \"Purchase Order\").\n" + + "\n" + + "Unless otherwise expressly provided in the Documentation or any applicable Supplemental License Agreement, Customer shall use the Software solely as embedded in, for execution on, or (where the applicable Documentation permits installation on non-Cisco equipment) for communication with Cisco equipment owned or leased by Customer and used for Customer's internal business purposes. No other licenses are granted by implication, estoppel or otherwise.\n" + + "\n" + + "For evaluation or beta copies for which Cisco does not charge a license fee, the above requirement to pay license fees does not apply.\n" + + "\n" + + "General Limitations. This is a license, not a transfer of title, to the Software and Documentation, and Cisco retains ownership of all copies of the Software and Documentation. Customer acknowledges that the Software and Documentation contain trade secrets of Cisco or its suppliers or licensors, including but not limited to the specific internal design and structure of individual programs and associated interface information. Except as otherwise expressly provided under the Agreement, Customer shall only use the Software in connection with the use of Cisco equipment purchased by the Customer from an Approved Source and Customer shall have no right, and Customer specifically agrees not to:\n" + + "\n" + + "(i) transfer, assign or sublicense its license rights to any other person or entity (other than in compliance with any Cisco relicensing/transfer policy then in force), or use the Software on Cisco equipment not purchased by the Customer from an Approved Source or on secondhand Cisco equipment, and Customer acknowledges that any attempted transfer, assignment, sublicense or use shall be void;\n" + + "\n" + + "(ii) make error corrections to or otherwise modify or adapt the Software or create derivative works based upon the Software, or permit third parties to do the same;\n" + + "\n" + + "(iii) reverse engineer or decompile, decrypt, disassemble or otherwise reduce the Software to human-readable form, except to the extent otherwise expressly permitted under applicable law notwithstanding this restriction or except to the extent that Cisco is legally required to permit such specific activity pursuant to any applicable open source license;\n" + + "\n" + + "(iv) publish any results of benchmark tests run on the Software;\n" + + "\n" + + "(v) use or permit the Software to be used to perform services for third parties, whether on a service bureau or time sharing basis or otherwise, without the express written authorization of Cisco; or\n" + + "\n" + + "(vi) disclose, provide, or otherwise make available trade secrets contained within the Software and Documentation in any form to any third party without the prior written consent of Cisco. Customer shall implement reasonable security measures to protect such trade secrets.\n" + + "\n" + + "To the extent required by applicable law, and at Customer's written request, Cisco shall provide Customer with the interface information needed to achieve interoperability between the Software and another independently created program, on payment of Cisco's applicable fee, if any. Customer shall observe strict obligations of confidentiality with respect to such information and shall use such information in compliance with any applicable terms and conditions upon which Cisco makes such information available.\n" + + "\n" + + "Software, Upgrades and Additional Copies. NOTWITHSTANDING ANY OTHER PROVISION OF THE AGREEMENT: (1) CUSTOMER HAS NO LICENSE OR RIGHT TO MAKE OR USE ANY ADDITIONAL COPIES OR UPGRADES UNLESS CUSTOMER, AT THE TIME OF MAKING OR ACQUIRING SUCH COPY OR UPGRADE, ALREADY HOLDS A VALID LICENSE TO THE ORIGINAL SOFTWARE AND HAS PAID THE APPLICABLE FEE TO AN APPROVED SOURCE FOR THE UPGRADE OR ADDITIONAL COPIES; (2) USE OF UPGRADES IS LIMITED TO CISCO EQUIPMENT SUPPLIED BY AN APPROVED SOURCE FOR WHICH CUSTOMER IS THE ORIGINAL END USER PURCHASER OR LESSEE OR OTHERWISE HOLDS A VALID LICENSE TO USE THE SOFTWARE WHICH IS BEING UPGRADED; AND (3) THE MAKING AND USE OF ADDITIONAL COPIES IS LIMITED TO NECESSARY BACKUP PURPOSES ONLY.\n" + + "\n" + + "Proprietary Notices. Customer agrees to maintain and reproduce all copyright, proprietary, and other notices on all copies, in any form, of the Software in the same form and manner that such copyright and other proprietary notices are included on the Software. Except as expressly authorized in the Agreement, Customer shall not make any copies or duplicates of any Software without the prior written permission of Cisco.\n" + + "\n" + + "Term and Termination. The Agreement and the license granted herein shall remain effective until terminated. Customer may terminate the Agreement and the license at any time by destroying all copies of Software and any Documentation. Customer's rights under the Agreement will terminate immediately without notice from Cisco if Customer fails to comply with any provision of the Agreement. Upon termination, Customer shall destroy all copies of Software and Documentation in its possession or control. All confidentiality obligations of Customer, all restrictions and limitations imposed on the Customer under the section titled \"General Limitations\" and all limitations of liability and disclaimers and restrictions of warranty shall survive termination of this Agreement. In addition, the provisions of the sections titled \"U.S. Government End User Purchasers\" and \"General Terms Applicable to the Limited Warranty Statement " + + "and End User License Agreement\" shall survive termination of the Agreement.\n" + + "\n" + + "Customer Records. Customer grants to Cisco and its independent accountants the right to examine Customer's books, records and accounts during Customer's normal business hours to verify compliance with this Agreement. In the event such audit discloses non-compliance with this Agreement, Customer shall promptly pay to Cisco the appropriate license fees, plus the reasonable cost of conducting the audit.\n" + + "\n" + + "Export, Re-Export, Transfer and Use Controls. The Software, Documentation and technology or direct products thereof (hereafter referred to as Software and Technology), supplied by Cisco under the Agreement are subject to export controls under the laws and regulations of the United States (\"U.S.\") and any other applicable countries' laws and regulations. Customer shall comply with such laws and regulations governing export, re-export, import, transfer and use of Cisco Software and Technology and will obtain all required U.S. and local authorizations, permits, or licenses. Cisco and Customer each agree to provide the other information, support documents, and assistance as may reasonably be required by the other in connection with securing authorizations or licenses. Information regarding compliance with export, re-export, transfer and use may be located at the following URL: " + + "www.cisco.com/web/about/doing_business/legal/global_export_trade/general_export/contract_compliance.html\n" + + "\n" + + "U.S. Government End User Purchasers. The Software and Documentation qualify as \"commercial items,\" as that term is defined at Federal Acquisition Regulation (\"FAR\") (48 C.F.R.) 2.101, consisting of \"commercial computer software\" and \"commercial computer software documentation\" as such terms are used in FAR 12.212. Consistent with FAR 12.212 and DoD FAR Supp. 227.7202-1 through 227.7202-4, and notwithstanding any other FAR or other contractual clause to the contrary in any agreement into which the Agreement may be incorporated, Customer may provide to Government end user or, if the Agreement is direct, Government end user will acquire, the Software and Documentation with only those rights set forth in the Agreement. Use of either the Software or Documentation or both constitutes agreement by the Government that the Software and Documentation are \"commercial computer software\" and \"commercial computer " + + "software documentation,\" and constitutes acceptance of the rights and restrictions herein.\n" + + "\n" + + "Identified Components; Additional Terms. The Software may contain or be delivered with one or more components, which may include third-party components, identified by Cisco in the Documentation, readme.txt file, third-party click-accept or elsewhere (e.g. on www.cisco.com) (the \"Identified Component(s)\") as being subject to different license agreement terms, disclaimers of warranties, limited warranties or other terms and conditions (collectively, \"Additional Terms\") than those set forth herein. You agree to the applicable Additional Terms for any such Identified Component(s).\n" + + "\n" + + "Limited Warranty\n" + + "\n" + + "Subject to the limitations and conditions set forth herein, Cisco warrants that commencing from the date of shipment to Customer (but in case of resale by an Approved Source other than Cisco, commencing not more than ninety (90) days after original shipment by Cisco), and continuing for a period of the longer of (a) ninety (90) days or (b) the warranty period (if any) expressly set forth as applicable specifically to software in the warranty card accompanying the product of which the Software is a part (the \"Product\") (if any): (a) the media on which the Software is furnished will be free of defects in materials and workmanship under normal use; and (b) the Software substantially conforms to the Documentation. The date of shipment of a Product by Cisco is set forth on the packaging material in which the Product is shipped. Except for the foregoing, the Software is provided \"AS IS\". This limited warranty extends only to the " + + "Software purchased from an Approved Source by a Customer who is the first registered end user. Customer's sole and exclusive remedy and the entire liability of Cisco and its suppliers under this limited warranty will be (i) replacement of defective media and/or (ii) at Cisco's option, repair, replacement, or refund of the purchase price of the Software, in both cases subject to the condition that any error or defect constituting a breach of this limited warranty is reported to the Approved Source supplying the Software to Customer, within the warranty period. Cisco or the Approved Source supplying the Software to Customer may, at its option, require return of the Software and/or Documentation as a condition to the remedy. In no event does Cisco warrant that the Software is error free or that Customer will be able to operate the Software without problems or interruptions. In addition, due to the continual development of new " + + "techniques for intruding upon and attacking networks, Cisco does not warrant that the Software or any equipment, system or network on which the Software is used will be free of vulnerability to intrusion or attack.\n" + + "\n" + + "Restrictions. This warranty does not apply if the Software, Product or any other equipment upon which the Software is authorized to be used (a) has been altered, except by Cisco or its authorized representative, (b) has not been installed, operated, repaired, or maintained in accordance with instructions supplied by Cisco, (c) has been subjected to abnormal physical or electrical stress, abnormal environmental conditions, misuse, negligence, or accident; or (d) is licensed for beta, evaluation, testing or demonstration purposes. The Software warranty also does not apply to (e) any temporary Software modules; (f) any Software not posted on Cisco's Software Center; (g) any Software that Cisco expressly provides on an \"AS IS\" basis on Cisco's Software Center; (h) any Software for which an Approved Source does not receive a license fee; and (i) Software supplied by any third party which is not an Approved Source.\n" + + "\n" + + "DISCLAIMER OF WARRANTY\n" + + "\n" + + "EXCEPT AS SPECIFIED IN THIS WARRANTY SECTION, ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS, AND WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OR CONDITION OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, SATISFACTORY QUALITY, NON-INTERFERENCE, ACCURACY OF INFORMATIONAL CONTENT, OR ARISING FROM A COURSE OF DEALING, LAW, USAGE, OR TRADE PRACTICE, ARE HEREBY EXCLUDED TO THE EXTENT ALLOWED BY APPLICABLE LAW AND ARE EXPRESSLY DISCLAIMED BY CISCO, ITS SUPPLIERS AND LICENSORS. TO THE EXTENT THAT ANY OF THE SAME CANNOT BE EXCLUDED, SUCH IMPLIED CONDITION, REPRESENTATION AND/OR WARRANTY IS LIMITED IN DURATION TO THE EXPRESS WARRANTY PERIOD REFERRED TO IN THE \"LIMITED WARRANTY\" SECTION ABOVE. BECAUSE SOME STATES OR JURISDICTIONS DO NOT ALLOW LIMITATIONS ON HOW LONG AN IMPLIED WARRANTY LASTS, THE ABOVE LIMITATION MAY NOT APPLY IN SUCH STATES. THIS WARRANTY GIVES CUSTOMER SPECIFIC LEGAL RIGHTS, " + + "AND CUSTOMER MAY ALSO HAVE OTHER RIGHTS WHICH VARY FROM JURISDICTION TO JURISDICTION. This disclaimer and exclusion shall apply even if the express warranty set forth above fails of its essential purpose.\n" + + "\n" + + "Disclaimer of Liabilities-Limitation of Liability. IF YOU ACQUIRED THE SOFTWARE IN THE UNITED STATES, LATIN AMERICA, CANADA, JAPAN OR THE CARIBBEAN, NOTWITHSTANDING ANYTHING ELSE IN THE AGREEMENT TO THE CONTRARY, ALL LIABILITY OF CISCO, ITS AFFILIATES, OFFICERS, DIRECTORS, EMPLOYEES, AGENTS, SUPPLIERS AND LICENSORS COLLECTIVELY, TO CUSTOMER, WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE), BREACH OF WARRANTY OR OTHERWISE, SHALL NOT EXCEED THE PRICE PAID BY CUSTOMER TO ANY APPROVED SOURCE FOR THE SOFTWARE THAT GAVE RISE TO THE CLAIM OR IF THE SOFTWARE IS PART OF ANOTHER PRODUCT, THE PRICE PAID FOR SUCH OTHER PRODUCT. THIS LIMITATION OF LIABILITY FOR SOFTWARE IS CUMULATIVE AND NOT PER INCIDENT (I.E. THE EXISTENCE OF TWO OR MORE CLAIMS WILL NOT ENLARGE THIS LIMIT).\n" + + "\n" + + "IF YOU ACQUIRED THE SOFTWARE IN EUROPE, THE MIDDLE EAST, AFRICA, ASIA OR OCEANIA, NOTWITHSTANDING ANYTHING ELSE IN THE AGREEMENT TO THE CONTRARY, ALL LIABILITY OF CISCO, ITS AFFILIATES, OFFICERS, DIRECTORS, EMPLOYEES, AGENTS, SUPPLIERS AND LICENSORS COLLECTIVELY, TO CUSTOMER, WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE), BREACH OF WARRANTY OR OTHERWISE, SHALL NOT EXCEED THE PRICE PAID BY CUSTOMER TO CISCO FOR THE SOFTWARE THAT GAVE RISE TO THE CLAIM OR IF THE SOFTWARE IS PART OF ANOTHER PRODUCT, THE PRICE PAID FOR SUCH OTHER PRODUCT. THIS LIMITATION OF LIABILITY FOR SOFTWARE IS CUMULATIVE AND NOT PER INCIDENT (I.E. THE EXISTENCE OF TWO OR MORE CLAIMS WILL NOT ENLARGE THIS LIMIT). NOTHING IN THE AGREEMENT SHALL LIMIT (I) THE LIABILITY OF CISCO, ITS AFFILIATES, OFFICERS, DIRECTORS, EMPLOYEES, AGENTS, SUPPLIERS AND LICENSORS TO CUSTOMER FOR PERSONAL INJURY OR DEATH CAUSED BY THEIR NEGLIGENCE, (II) CISCO'S LIABILITY FOR FRAUDULENT" + + " MISREPRESENTATION, OR (III) ANY LIABILITY OF CISCO WHICH CANNOT BE EXCLUDED UNDER APPLICABLE LAW.\n" + + "\n" + + "Disclaimer of Liabilities-Waiver of Consequential Damages and Other Losses. IF YOU ACQUIRED THE SOFTWARE IN THE UNITED STATES, LATIN AMERICA, THE CARIBBEAN OR CANADA, REGARDLESS OF WHETHER ANY REMEDY SET FORTH HEREIN FAILS OF ITS ESSENTIAL PURPOSE OR OTHERWISE, IN NO EVENT WILL CISCO OR ITS SUPPLIERS BE LIABLE FOR ANY LOST REVENUE, PROFIT, OR LOST OR DAMAGED DATA, BUSINESS INTERRUPTION, LOSS OF CAPITAL, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE DAMAGES HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY OR WHETHER ARISING OUT OF THE USE OF OR INABILITY TO USE SOFTWARE OR OTHERWISE AND EVEN IF CISCO OR ITS SUPPLIERS OR LICENSORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES OR JURISDICTIONS DO NOT ALLOW LIMITATION OR EXCLUSION OF CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU.\n" + + "\n" + + "IF YOU ACQUIRED THE SOFTWARE IN JAPAN, EXCEPT FOR LIABILITY ARISING OUT OF OR IN CONNECTION WITH DEATH OR PERSONAL INJURY, FRAUDULENT MISREPRESENTATION, AND REGARDLESS OF WHETHER ANY REMEDY SET FORTH HEREIN FAILS OF ITS ESSENTIAL PURPOSE OR OTHERWISE, IN NO EVENT WILL CISCO, ITS AFFILIATES, OFFICERS, DIRECTORS, EMPLOYEES, AGENTS, SUPPLIERS AND LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT, OR LOST OR DAMAGED DATA, BUSINESS INTERRUPTION, LOSS OF CAPITAL, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE DAMAGES HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY OR WHETHER ARISING OUT OF THE USE OF OR INABILITY TO USE SOFTWARE OR OTHERWISE AND EVEN IF CISCO OR ANY APPROVED SOURCE OR THEIR SUPPLIERS OR LICENSORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n" + + "\n" + + "IF YOU ACQUIRED THE SOFTWARE IN EUROPE, THE MIDDLE EAST, AFRICA, ASIA OR OCEANIA, IN NO EVENT WILL CISCO, ITS AFFILIATES, OFFICERS, DIRECTORS, EMPLOYEES, AGENTS, SUPPLIERS AND LICENSORS, BE LIABLE FOR ANY LOST REVENUE, LOST PROFIT, OR LOST OR DAMAGED DATA, BUSINESS INTERRUPTION, LOSS OF CAPITAL, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE DAMAGES, HOWSOEVER ARISING, INCLUDING, WITHOUT LIMITATION, IN CONTRACT, TORT (INCLUDING NEGLIGENCE) OR WHETHER ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE, EVEN IF, IN EACH CASE, CISCO, ITS AFFILIATES, OFFICERS, DIRECTORS, EMPLOYEES, AGENTS, SUPPLIERS AND LICENSORS, HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES OR JURISDICTIONS DO NOT ALLOW LIMITATION OR EXCLUSION OF CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT FULLY APPLY TO YOU. THE FOREGOING EXCLUSION SHALL NOT APPLY TO ANY LIABILITY ARISING OUT OF OR IN " + + "CONNECTION WITH: (I) DEATH OR PERSONAL INJURY, (II) FRAUDULENT MISREPRESENTATION, OR (III) CISCO'S LIABILITY IN CONNECTION WITH ANY TERMS THAT CANNOT BE EXCLUDED UNDER APPLICABLE LAW.\n" + + "\n" + + "Customer acknowledges and agrees that Cisco has set its prices and entered into the Agreement in reliance upon the disclaimers of warranty and the limitations of liability set forth herein, that the same reflect an allocation of risk between the parties (including the risk that a contract remedy may fail of its essential purpose and cause consequential loss), and that the same form an essential basis of the bargain between the parties.\n" + + "\n" + + "Controlling Law, Jurisdiction. If you acquired, by reference to the address on the purchase order accepted by the Approved Source, the Software in the United States, Latin America, or the Caribbean, the Agreement and warranties (\"Warranties\") are controlled by and construed under the laws of the State of California, United States of America, notwithstanding any conflicts of law provisions; and the state and federal courts of California shall have exclusive jurisdiction over any claim arising under the Agreement or Warranties. If you acquired the Software in Canada, unless expressly prohibited by local law, the Agreement and Warranties are controlled by and construed under the laws of the Province of Ontario, Canada, notwithstanding any conflicts of law provisions; and the courts of the Province of Ontario shall have exclusive jurisdiction over any claim arising under the Agreement or Warranties. If you acquired the Software in " + + "Europe, the Middle East, Africa, Asia or Oceania (excluding Australia), unless expressly prohibited by local law, the Agreement and Warranties are controlled by and construed under the laws of England, notwithstanding any conflicts of law provisions; and the English courts shall have exclusive jurisdiction over any claim arising under the Agreement or Warranties. In addition, if the Agreement is controlled by the laws of England, no person who is not a party to the Agreement shall be entitled to enforce or take the benefit of any of its terms under the Contracts (Rights of Third Parties) Act 1999. If you acquired the Software in Japan, unless expressly prohibited by local law, the Agreement and Warranties are controlled by and construed under the laws of Japan, notwithstanding any conflicts of law provisions; and the Tokyo District Court of Japan shall have exclusive jurisdiction over any claim arising under the Agreement or Warranties. " + + "If you acquired the Software in Australia, unless expressly prohibited by local law, the Agreement and Warranties are controlled by and construed under the laws of the State of New South Wales, Australia, notwithstanding any conflicts of law provisions; and the State and federal courts of New South Wales shall have exclusive jurisdiction over any claim arising under the Agreement or Warranties. If you acquired the Software in any other country, unless expressly prohibited by local law, the Agreement and Warranties are controlled by and construed under the laws of the State of California, United States of America, notwithstanding any conflicts of law provisions; and the state and federal courts of California shall have exclusive jurisdiction over any claim arising under the Agreement or Warranties.\n" + + "\n" + + "For all countries referred to above, the parties specifically disclaim the application of the UN Convention on Contracts for the International Sale of Goods. Notwithstanding the foregoing, either party may seek interim injunctive relief in any court of appropriate jurisdiction with respect to any alleged breach of such party's intellectual property or proprietary rights. If any portion hereof is found to be void or unenforceable, the remaining provisions of the Agreement and Warranties shall remain in full force and effect. Except as expressly provided herein, the Agreement constitutes the entire agreement between the parties with respect to the license of the Software and Documentation and supersedes any conflicting or additional terms contained in any Purchase Order or elsewhere, all of which terms are excluded. The Agreement has been written in the English language, and the parties agree that the English version will govern.\n" + + "\n" + + "Product warranty terms and other information applicable to Cisco products are available at the following URL: www.cisco.com/go/warranty\n" + + "\n" + + "Cisco and the Cisco logo are trademarks or registered trademarks of Cisco and/or its affiliates in the U.S. and other countries. To view a list of Cisco trademarks, go to this URL: www.cisco.com/go/trademarks. Third-party trademarks mentioned are the property of their respective owners. The use of the word partner does not imply a partnership relationship between Cisco and any other company. (1110R)\n" + + "\n" + + "© 1998, 2001, 2003, 2008-2014 Cisco Systems, Inc. All rights reserved.\n" + + "\n" + + "\n" + + " supplemental end-user license agreement\n" + + " SUPPLEMENTAL END USER LICENSE AGREEMENT FOR VIRTUAL SOFTWARE PRODUCTS\n" + + "\n" + + "IMPORTANT: READ CAREFULLY\n" + + "\n" + + "This Supplemental End User License Agreement (\"SEULA\") contains additional terms and conditions for the Software licensed under the End User License Agreement (\"EULA\") between you and Cisco (collectively, the \"Agreement\"). Capitalized terms used in this SEULA but not defined will have the meanings assigned to them in the EULA. To the extent that there is a conflict between the terms and conditions of the EULA and this SEULA, the terms and conditions of this SEULA will take precedence. In addition to the limitations set forth in the EULA on your access and use of the Software, you agree to comply at all times with the terms and conditions provided in this SEULA.\n" + + "\n" + + "DOWNLOADING, INSTALLING, OR USING THE SOFTWARE CONSTITUTES ACCEPTANCE OF THE AGREEMENT, AND YOU ARE BINDING YOURSELF AND THE BUSINESS ENTITY THAT YOU REPRESENT (COLLECTIVELY, \"CUSTOMER\") TO THE AGREEMENT. IF YOU DO NOT AGREE TO ALL OF THE TERMS OF THE AGREEMENT, THEN CISCO IS UNWILLING TO LICENSE THE SOFTWARE TO YOU AND (A) YOU MAY NOT DOWNLOAD, INSTALL OR USE THE SOFTWARE, AND (B) YOU MAY RETURN THE SOFTWARE (INCLUDING ANY UNOPENED CD PACKAGE AND ANY WRITTEN MATERIALS) FOR A FULL REFUND, OR, IF THE SOFTWARE AND WRITTEN MATERIALS ARE SUPPLIED AS PART OF ANOTHER PRODUCT, YOU MAY RETURN THE ENTIRE PRODUCT FOR A FULL REFUND. YOUR RIGHT TO RETURN AND REFUND EXPIRES 30 DAYS AFTER PURCHASE FROM CISCO OR AN AUTHORIZED CISCO RESELLER, AND APPLIES ONLY IF YOU ARE THE ORIGINAL END USER PURCHASER.\n" + + "\n" + + "Definitions\n" + + "\"CPU\" means a central processing unit that encompasses part of a Server.\n" + + "\"Failover Pair\" means a primary Instance and a standby Instance with the same Software configuration where the standby Instance can take over in case of failure of the primary Instance.\n" + + "\"Instance\" means a single copy of the Software. Each copy of the Software loaded into memory is an Instance.\n" + + "\"Server\" means a single physical computer or device on a network that manages or provides network resources for multiple users.\n" + + "\"Service Provider\" means a company that provides information technology services to external end user customers.\n" + + "\"Software\" means Cisco's Adaptive Security Virtual Appliance (\"ASAv\"), Adaptive Security Appliance 1000V Cloud Firewall Software (\"ASA 1000V\"), Nexus 1000V series switch products, Virtual Security Gateway products, or other Cisco virtual software products that Cisco includes under this SEULA.\n" + + "\"vCPU\" means a virtual central processing resource assigned to the VM by the underlying virtualization technology.\n" + + "\"Virtual Machine\" or \"VM\" means a software container that can run its own operating system and execute applications like a Server.\n" + + "\n" + + "Additional License Terms and Conditions\n" + + "1. Cisco hereby grants Customer the right to install and use the Software on single or multiple Cisco or non-Cisco Servers or on Virtual Machines. In order to use the Software Customer may be required to input a registration number or product activation key and register each Instance online at Cisco's website in order to obtain the necessary entitlements.\n" + + "2. Customer shall pay a unit license fee to Cisco or an authorized Cisco reseller, as applicable, for each Instance installed on a Cisco or non-Cisco Server CPU, vCPU or Virtual Machine, as determined by Cisco.\n" + + "3. For the ASA 1000V, Customer is licensed the number of Instances equal to the number of CPUs covered by the unit license fee. If Customer deploys a Failover Pair, then the fee for the additional standby Instance is included in the fee for each primary Instance.\n" + + "4. If Customer is a Service Provider, Customer may use the Software under the terms of this Agreement for the purpose of delivering hosted information technology services to Customer's end user customers, subject to payment of the required license fee(s).\n" + + "5. Customer may also use the Software under the terms of this Agreement to deliver hosted information technology services to Customer affiliates, subject to payment of the required license fee(s).\n" + + "6. If the Software is subject to Cisco's Smart Licensing program, Cisco will be able to assess if Customer is using the Software within the limits and entitlements paid for by Customer. If the Smart Licensing program is applicable, Customer will be required to enter into a separate terms of service agreement relating to Smart Licensing.\n" + + "\n" + + ""; + private String productSectionWithCategories = "\n" + "\n" + @@ -731,6 +856,12 @@ public class OVFHelperTest { Assert.assertEquals(2, eulas.size()); } + @Test + public void testGetOVFEulaSectionValidOVFwithOVFprefix() throws IOException, SAXException { + List eulas = ovfHelper.getOVFEulaSectionFromXmlString(eulaSectionsWithOVFprefix); + Assert.assertEquals(2, eulas.size()); + } + @Test public void testGetOVFPropertiesWithCategories() throws IOException, SAXException { List props = ovfHelper.getOVFPropertiesFromXmlString(productSectionWithCategories); From 8fe13f91a10cc326eb33f3aaf4ba3900b9efd1ba Mon Sep 17 00:00:00 2001 From: Aakash Sinha Date: Wed, 25 Oct 2023 14:32:57 +0530 Subject: [PATCH 03/13] OSType response: isuserdefined as string not bool #8127 (#8128) Co-authored-by: Aakash Sinha --- .../org/apache/cloudstack/api/response/GuestOSResponse.java | 6 +++--- server/src/main/java/com/cloud/api/ApiResponseHelper.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/response/GuestOSResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/GuestOSResponse.java index 6ad5e2ac828..f870a2f0d94 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/GuestOSResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/GuestOSResponse.java @@ -53,7 +53,7 @@ public class GuestOSResponse extends BaseResponse { @SerializedName(ApiConstants.IS_USER_DEFINED) @Param(description = "is the guest OS user defined") - private String isUserDefined; + private Boolean isUserDefined; @SerializedName(ApiConstants.FOR_DISPLAY) @Param(description = "is the guest OS visible for the users") @@ -99,11 +99,11 @@ public class GuestOSResponse extends BaseResponse { this.description = description; } - public String getIsUserDefined() { + public Boolean getIsUserDefined() { return isUserDefined; } - public void setIsUserDefined(String isUserDefined) { + public void setIsUserDefined(Boolean isUserDefined) { this.isUserDefined = isUserDefined; } diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 7d80cd1a6da..0d00fece4c3 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -3730,7 +3730,7 @@ public class ApiResponseHelper implements ResponseGenerator { response.setName(guestOS.getDisplayName()); response.setDescription(guestOS.getDisplayName()); response.setId(guestOS.getUuid()); - response.setIsUserDefined(String.valueOf(guestOS.getIsUserDefined())); + response.setIsUserDefined(guestOS.getIsUserDefined()); response.setForDisplay(guestOS.getForDisplay()); GuestOSCategoryVO category = ApiDBUtils.findGuestOsCategoryById(guestOS.getCategoryId()); if (category != null) { From ebf140962218cb1fb2f66850198224e22f4b9c83 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Wed, 25 Oct 2023 22:04:30 +0530 Subject: [PATCH 04/13] dashboard: on admin dashboard use red-dot icon when hosts are in alert (#8144) This improves the function to not show the alert status on host icon when there are no hosts in alert state. Instead a colour/theme matching exclaimation is show next to the host icon, otherwise show green tick. Signed-off-by: Rohit Yadav --- ui/src/views/dashboard/CapacityDashboard.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/views/dashboard/CapacityDashboard.vue b/ui/src/views/dashboard/CapacityDashboard.vue index b6fa854efb5..a174e60b709 100644 --- a/ui/src/views/dashboard/CapacityDashboard.vue +++ b/ui/src/views/dashboard/CapacityDashboard.vue @@ -112,7 +112,8 @@ :value-style="{ color: $config.theme['@primary-color'] }"> From 27ae7d8bc48170bdeacb5021e2058ea3ed0b1b94 Mon Sep 17 00:00:00 2001 From: Vishesh Date: Thu, 26 Oct 2023 00:57:10 +0530 Subject: [PATCH 05/13] Fixup: register vmscheduler configkey (#8116) This PR registers the global setting VMScheduledJobExpireInterval. --- .../cloudstack/vm/schedule/VMSchedulerImpl.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/apache/cloudstack/vm/schedule/VMSchedulerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/schedule/VMSchedulerImpl.java index 34548fc9258..5d25f36ca31 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/schedule/VMSchedulerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/schedule/VMSchedulerImpl.java @@ -34,6 +34,8 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.user.vm.RebootVMCmd; import org.apache.cloudstack.api.command.user.vm.StartVMCmd; import org.apache.cloudstack.api.command.user.vm.StopVMCmd; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher; import org.apache.cloudstack.framework.jobs.AsyncJobManager; import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; @@ -58,7 +60,7 @@ import java.util.Map; import java.util.Timer; import java.util.TimerTask; -public class VMSchedulerImpl extends ManagerBase implements VMScheduler { +public class VMSchedulerImpl extends ManagerBase implements VMScheduler, Configurable { private static Logger LOGGER = Logger.getLogger(VMSchedulerImpl.class); @Inject private VMScheduledJobDao vmScheduledJobDao; @@ -82,6 +84,16 @@ public class VMSchedulerImpl extends ManagerBase implements VMScheduler { asyncJobDispatcher = dispatcher; } + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{VMScheduledJobExpireInterval}; + } + + @Override + public String getConfigComponentName() { + return VMScheduler.class.getSimpleName(); + } + @Override public void removeScheduledJobs(List vmScheduleIds) { if (vmScheduleIds == null || vmScheduleIds.isEmpty()) { From fb76fd10f0ec94f4470a00dad5f6f55c4974c76a Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Thu, 26 Oct 2023 02:06:35 -0300 Subject: [PATCH 06/13] Add winterhazel to .asf.yaml collaborators (#8147) Added winterhazel as a collaborator in .asf.yaml to be able to report issues with tags, and be assigned to issues and PRs. --- .asf.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.asf.yaml b/.asf.yaml index 10df7ffaa24..1b8e74f7e59 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -59,5 +59,6 @@ github: - BryanMLima - SadiJr - JoaoJandre + - winterhazel protected_branches: ~ From 68e1d46698ef620d2a2aa69de4a6b47f1e510923 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 26 Oct 2023 11:17:52 +0530 Subject: [PATCH 07/13] reduce collaborators count to 10 (#8149) Maximum 10 collaborators are supported by ASF infra. soreana is already a committer now and can be removed from this list. Signed-off-by: Abhishek Kumar --- .asf.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.asf.yaml b/.asf.yaml index 1b8e74f7e59..ce89a03d9ce 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -53,7 +53,6 @@ github: - rajujith - alexandremattioli - vishesh92 - - soreana - GaOrtiga - acs-robot - BryanMLima From ea90848429d5b7e164876ff0f15e3e40bf1b5248 Mon Sep 17 00:00:00 2001 From: Vishesh Date: Thu, 26 Oct 2023 11:48:18 +0530 Subject: [PATCH 08/13] Feature: Add support for DRS in a Cluster (#7723) This pull request (PR) implements a Distributed Resource Scheduler (DRS) for a CloudStack cluster. The primary objective of this feature is to enable automatic resource optimization and workload balancing within the cluster by live migrating the VMs as per configuration. Administrators can also execute DRS manually for a cluster, using the UI or the API. Adds support for two algorithms - condensed & balanced. Algorithms are pluggable allowing ACS Administrators to have customized control over scheduling. Implementation There are three top level components: Scheduler A timer task which: Generate DRS plan for clusters Process DRS plan Remove old DRS plan records DRS Execution We go through each VM in the cluster and use the specified algorithm to check if DRS is required and to calculate cost, benefit & improvement of migrating that VM to another host in the cluster. On the basis of cost, benefit & improvement, the best migration is selected for the current iteration and the VM is migrated. The maximum number of iterations (live migrations) possible on the cluster is defined by drs.iterations which is defined as a percentage (as a value between 0 and 1) of total number of workloads. Algorithm Every algorithms implements two methods: needsDrs - to check if drs is required for cluster getMetrics - to calculate cost, benefit & improvement of a migrating a VM to another host. Algorithms Condensed - Packs all the VMs on minimum number of hosts in the cluster. Balanced - Distributes the VMs evenly across hosts in the cluster. Algorithms use drs.level to decide the amount of imbalance to allow in the cluster. APIs Added listClusterDrsPlan id - ID of the DRS plan to list clusterid - to list plans for a cluster id generateClusterDrsPlan id - cluster id iterations - The maximum number of iterations in a DRS job defined as a percentage (as a value between 0 and 1) of total number of workloads. Defaults to value of cluster's drs.iterations setting. executeClusterDrsPlan id - ID of the cluster for which DRS plan is to be executed. migrateto - This parameter specifies the mapping between a vm and a host to migrate that VM. Format of this parameter: migrateto[vm-index].vm=&migrateto[vm-index].host=. Config Keys Added ClusterDrsPlanExpireInterval Key drs.plan.expire.interval Scope Global Default Value 30 days Description The interval in days after which old DRS records will be cleaned up. ClusterDrsEnabled Key drs.automatic.enable Scope Cluster Default Value false Description Enable/disable automatic DRS on a cluster. ClusterDrsInterval Key drs.automatic.interval Scope Cluster Default Value 60 minutes Description The interval in minutes after which a periodic background thread will schedule DRS for a cluster. ClusterDrsIterations Key drs.max.migrations Scope Cluster Default Value 50 Description Maximum number of live migrations in a DRS execution. ClusterDrsAlgorithm Key drs.algorithm Scope Cluster Default Value condensed Description DRS algorithm to execute on the cluster. This PR implements two algorithms - balanced & condensed. ClusterDrsLevel Key drs.imbalance Scope Cluster Default Value 0.5 Description Percentage (as a value between 0.0 and 1.0) of imbalance allowed in the cluster. 1.0 means no imbalance is allowed and 0.0 means imbalance is allowed. ClusterDrsMetric Key drs.imbalance.metric Scope Cluster Default Value memory Description The cluster imbalance metric to use when checking the drs.imbalance.threshold. Possible values are memory and cpu. --- .github/workflows/ci.yml | 3 +- api/pom.xml | 5 + .../main/java/com/cloud/event/EventTypes.java | 5 + .../com/cloud/server/ManagementService.java | 2 + .../java/com/cloud/vm/VirtualMachine.java | 3 + .../java/com/cloud/vm/VmDetailConstants.java | 2 + .../affinity/AffinityGroupProcessor.java | 8 + .../affinity/AffinityProcessorBase.java | 9 + .../apache/cloudstack/api/ApiConstants.java | 2 + .../cluster/ExecuteClusterDrsPlanCmd.java | 147 +++ .../cluster/GenerateClusterDrsPlanCmd.java | 85 ++ .../admin/cluster/ListClusterDrsPlanCmd.java | 62 ++ .../ClusterDrsPlanMigrationResponse.java | 75 ++ .../api/response/ClusterDrsPlanResponse.java | 83 ++ .../cluster/ClusterDrsAlgorithm.java | 167 ++++ .../cloudstack/cluster/ClusterDrsPlan.java | 48 + .../cluster/ClusterDrsPlanMigration.java | 25 + .../cloudstack/cluster/ClusterDrsService.java | 108 +++ client/pom.xml | 10 + .../cloudstack/cluster/module.properties | 21 + ...-lifecycle-cluster-context-inheritable.xml | 31 + .../spring-core-registry-core-context.xml | 4 + .../cluster/ClusterDrsPlanMigrationVO.java | 105 +++ .../cloudstack/cluster/ClusterDrsPlanVO.java | 103 +++ .../cluster/dao/ClusterDrsPlanDao.java | 41 + .../cluster/dao/ClusterDrsPlanDaoImpl.java | 107 +++ .../dao/ClusterDrsPlanMigrationDao.java | 33 + .../dao/ClusterDrsPlanMigrationDaoImpl.java | 70 ++ ...spring-engine-schema-core-daos-context.xml | 2 + .../META-INF/db/schema-41810to41900.sql | 37 + .../affinity/ExplicitDedicationProcessor.java | 2 +- .../affinity/HostAffinityProcessor.java | 45 +- .../affinity/HostAffinityProcessorTest.java | 5 +- .../affinity/HostAntiAffinityProcessor.java | 2 +- .../NonStrictHostAffinityProcessor.java | 2 +- plugins/drs/cluster/balanced/pom.xml | 33 + .../apache/cloudstack/cluster/Balanced.java | 99 ++ .../cloudstack/balanced/module.properties | 18 + .../balanced/spring-balanced-context.xml | 33 + .../cloudstack/cluster/BalancedTest.java | 227 +++++ plugins/drs/cluster/condensed/pom.xml | 33 + .../apache/cloudstack/cluster/Condensed.java | 98 ++ .../cloudstack/condensed/module.properties | 18 + .../condensed/spring-condensed-context.xml | 33 + .../cloudstack/cluster/CondensedTest.java | 221 +++++ plugins/pom.xml | 3 + .../main/java/com/cloud/api/ApiDBUtils.java | 4 + .../cloud/api/query/dao/UserVmJoinDao.java | 3 + .../api/query/dao/UserVmJoinDaoImpl.java | 15 + .../ConfigurationManagerImpl.java | 2 + .../cloud/server/ManagementServerImpl.java | 40 +- .../cluster/ClusterDrsServiceImpl.java | 849 ++++++++++++++++++ .../spring-server-core-managers-context.xml | 4 + .../cluster/ClusterDrsServiceImplTest.java | 440 +++++++++ test/integration/smoke/test_cluster_drs.py | 267 ++++++ tools/marvin/marvin/lib/base.py | 29 + ui/public/locales/en.json | 11 + ui/src/components/view/DedicateData.vue | 2 +- ui/src/config/section/infra/clusters.js | 19 + ui/src/views/infra/ClusterDRSTab.vue | 297 ++++++ 60 files changed, 4227 insertions(+), 30 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ExecuteClusterDrsPlanCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/GenerateClusterDrsPlanCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ListClusterDrsPlanCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanMigrationResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsAlgorithm.java create mode 100644 api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlan.java create mode 100644 api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanMigration.java create mode 100644 api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsService.java create mode 100644 core/src/main/resources/META-INF/cloudstack/cluster/module.properties create mode 100644 core/src/main/resources/META-INF/cloudstack/cluster/spring-core-lifecycle-cluster-context-inheritable.xml create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanMigrationVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanDao.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanDaoImpl.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanMigrationDao.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanMigrationDaoImpl.java create mode 100644 plugins/drs/cluster/balanced/pom.xml create mode 100644 plugins/drs/cluster/balanced/src/main/java/org/apache/cloudstack/cluster/Balanced.java create mode 100644 plugins/drs/cluster/balanced/src/main/resources/META-INF/cloudstack/balanced/module.properties create mode 100644 plugins/drs/cluster/balanced/src/main/resources/META-INF/cloudstack/balanced/spring-balanced-context.xml create mode 100644 plugins/drs/cluster/balanced/src/test/java/org/apache/cloudstack/cluster/BalancedTest.java create mode 100644 plugins/drs/cluster/condensed/pom.xml create mode 100644 plugins/drs/cluster/condensed/src/main/java/org/apache/cloudstack/cluster/Condensed.java create mode 100644 plugins/drs/cluster/condensed/src/main/resources/META-INF/cloudstack/condensed/module.properties create mode 100644 plugins/drs/cluster/condensed/src/main/resources/META-INF/cloudstack/condensed/spring-condensed-context.xml create mode 100644 plugins/drs/cluster/condensed/src/test/java/org/apache/cloudstack/cluster/CondensedTest.java create mode 100644 server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java create mode 100644 server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java create mode 100644 test/integration/smoke/test_cluster_drs.py create mode 100644 ui/src/views/infra/ClusterDRSTab.vue diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b17ab33185..20f25dadf7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,8 @@ jobs: smoke/test_domain_network_offerings smoke/test_domain_service_offerings smoke/test_domain_vpc_offerings", - "smoke/test_dynamicroles + "smoke/test_cluster_drs + smoke/test_dynamicroles smoke/test_enable_account_settings_for_domain smoke/test_enable_role_based_users_in_projects smoke/test_events_resource diff --git a/api/pom.xml b/api/pom.xml index ff9083afa09..427fbd24231 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -36,6 +36,11 @@ com.google.code.gson gson + + org.apache.commons + commons-math3 + ${cs.commons-math3.version} + javax.servlet javax.servlet-api diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 67b6ce3f54c..b3d9b44ec7f 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -678,6 +678,11 @@ public class EventTypes { //Usage related events public static final String EVENT_USAGE_REMOVE_USAGE_RECORDS = "USAGE.REMOVE.USAGE.RECORDS"; + // DRS Events + public static final String EVENT_CLUSTER_DRS = "CLUSTER.DRS"; + public static final String EVENT_CLUSTER_DRS_GENERATE = "CLUSTER.DRS.GENERATE"; + + // Netscaler Service Package events public static final String EVENT_NETSCALER_SERVICEPACKAGE_ADD = "NETSCALER.SERVICEPACKAGE.ADD"; public static final String EVENT_NETSCALER_SERVICEPACKAGE_DELETE = "NETSCALER.SERVICEPACKAGE.DELETE"; diff --git a/api/src/main/java/com/cloud/server/ManagementService.java b/api/src/main/java/com/cloud/server/ManagementService.java index a01a22172e7..e87f6b362da 100644 --- a/api/src/main/java/com/cloud/server/ManagementService.java +++ b/api/src/main/java/com/cloud/server/ManagementService.java @@ -441,6 +441,8 @@ public interface ManagementService { */ Ternary, Integer>, List, Map> listHostsForMigrationOfVM(Long vmId, Long startIndex, Long pageSize, String keyword); + Ternary, Integer>, List, Map> listHostsForMigrationOfVM(VirtualMachine vm, Long startIndex, Long pageSize, String keyword, List vmList); + /** * List storage pools for live migrating of a volume. The API returns list of all pools in the cluster to which the * volume can be migrated. Current pool is not included in the list. In case of vSphere datastore cluster storage pools, diff --git a/api/src/main/java/com/cloud/vm/VirtualMachine.java b/api/src/main/java/com/cloud/vm/VirtualMachine.java index 34c7a42b9a2..e7c5efb773b 100644 --- a/api/src/main/java/com/cloud/vm/VirtualMachine.java +++ b/api/src/main/java/com/cloud/vm/VirtualMachine.java @@ -315,6 +315,9 @@ public interface VirtualMachine extends RunningOn, ControlledEntity, Partition, @Override Long getHostId(); + + void setHostId(Long hostId); + /** * @return should HA be enabled for this machine? */ diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java index add2518321b..83c7529b22b 100644 --- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java +++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java @@ -73,6 +73,8 @@ public interface VmDetailConstants { String CONFIG_DRIVE_LOCATION = "configDriveLocation"; + String SKIP_DRS = "skipFromDRS"; + // VM import with nic, disk and custom params for custom compute offering String NIC = "nic"; String NETWORK = "network"; diff --git a/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupProcessor.java b/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupProcessor.java index f3ff1dd37f1..077953d0931 100644 --- a/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupProcessor.java +++ b/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupProcessor.java @@ -21,8 +21,11 @@ import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeploymentPlanner.ExcludeList; import com.cloud.exception.AffinityConflictException; import com.cloud.utils.component.Adapter; +import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; +import java.util.List; + public interface AffinityGroupProcessor extends Adapter { /** @@ -35,7 +38,12 @@ public interface AffinityGroupProcessor extends Adapter { * deployment plan that tells you where it's being deployed to. * @param avoid * avoid these data centers, pods, clusters, or hosts. + * @param vmList + * list of virtual machines objects according to which the affinity group should be processed. + * This can be used to process a theoretical state in some cases like generating DRS plans */ + void process(VirtualMachineProfile vm, DeploymentPlan plan, ExcludeList avoid, List vmList) throws AffinityConflictException; + void process(VirtualMachineProfile vm, DeploymentPlan plan, ExcludeList avoid) throws AffinityConflictException; /** diff --git a/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java b/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java index d48b5fd7556..9995d8039e1 100644 --- a/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java +++ b/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java @@ -21,14 +21,23 @@ import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeploymentPlanner.ExcludeList; import com.cloud.exception.AffinityConflictException; import com.cloud.utils.component.AdapterBase; +import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; +import java.util.Collections; +import java.util.List; + public class AffinityProcessorBase extends AdapterBase implements AffinityGroupProcessor { protected String _type; @Override public void process(VirtualMachineProfile vm, DeploymentPlan plan, ExcludeList avoid) throws AffinityConflictException { + process(vm, plan, avoid, Collections.emptyList()); + } + + @Override + public void process(VirtualMachineProfile vm, DeploymentPlan plan, ExcludeList avoid, List vmList) throws AffinityConflictException { } 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 9503f9b76fb..3fbd43b143a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -292,6 +292,7 @@ public class ApiConstants { public static final String MIN_CPU_NUMBER = "mincpunumber"; public static final String MIN_MEMORY = "minmemory"; public static final String MIGRATION_TYPE = "migrationtype"; + public static final String MIGRATIONS = "migrations"; public static final String MEMORY = "memory"; public static final String MODE = "mode"; public static final String NAME = "name"; @@ -661,6 +662,7 @@ public class ApiConstants { public static final String SPECIFY_IP_RANGES = "specifyipranges"; 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 SORT_BY = "sortby"; public static final String CHANGE_CIDR = "changecidr"; public static final String PURPOSE = "purpose"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ExecuteClusterDrsPlanCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ExecuteClusterDrsPlanCmd.java new file mode 100644 index 00000000000..60f2c2b1dee --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ExecuteClusterDrsPlanCmd.java @@ -0,0 +1,147 @@ +/* + * 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.admin.cluster; + +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.host.Host; +import com.cloud.user.Account; +import com.cloud.utils.UuidUtils; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ClusterDrsPlanResponse; +import org.apache.cloudstack.api.response.ClusterResponse; +import org.apache.cloudstack.cluster.ClusterDrsService; +import org.apache.commons.collections.MapUtils; + +import javax.inject.Inject; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +@APICommand(name = "executeClusterDrsPlan", + description = "Execute DRS for a cluster. If there is another plan in progress for the same cluster, " + + "this command will fail.", + responseObject = ClusterDrsPlanResponse.class, since = "4.19.0", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class ExecuteClusterDrsPlanCmd extends BaseAsyncCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ClusterResponse.class, required = true, + description = "ID of cluster") + private Long id; + + @Parameter( + name = ApiConstants.MIGRATE_TO, + type = CommandType.MAP, + description = "Virtual Machine to destination host mapping. This parameter specifies the mapping between " + + "a vm and a host to migrate that VM. clusterid is required if this parameter is set." + + "Format of this parameter: migrateto[vm-index].vm=&migrateto[vm-index].host= " + + "Where, [vm-index] indicates the index to identify the vm that you want to migrate, " + + "vm= indicates the UUID of the vm that you want to migrate, and " + + "host= indicates the UUID of the host where you want to migrate the vm. " + + "Example: migrateto[0].vm=<71f43cd6-69b0-4d3b-9fbc-67f50963d60b>" + + "&migrateto[0].host=" + + "&migrateto[1].vm=<88de0173-55c0-4c1c-a269-83d0279eeedf>" + + "&migrateto[1].host=<95d6e97c-6766-4d67-9a30-c449c15011d1>" + + "&migrateto[2].vm=<1b331390-59f2-4796-9993-bf11c6e76225>" + + "&migrateto[2].host=<41fdb564-9d3b-447d-88ed-7628f7640cbc>") + private Map migrateVmTo; + + @Inject + private ClusterDrsService clusterDrsService; + + public Map getVmToHostMap() { + Map vmToHostMap = new HashMap<>(); + if (MapUtils.isNotEmpty(migrateVmTo)) { + Collection allValues = migrateVmTo.values(); + Iterator iter = allValues.iterator(); + while (iter.hasNext()) { + HashMap vmToHost = (HashMap) iter.next(); + + String vmId = vmToHost.get("vm"); + String hostId = vmToHost.get("host"); + + VirtualMachine vm; + Host host; + if (UuidUtils.isUuid(vmId)) { + vm = _entityMgr.findByUuid(VirtualMachine.class, vmId); + } else { + vm = _entityMgr.findById(VirtualMachine.class, Long.parseLong(vmId)); + } + + if (UuidUtils.isUuid(hostId)) { + host = _entityMgr.findByUuid(Host.class, hostId); + } else { + host = _entityMgr.findById(Host.class, Long.parseLong(hostId)); + } + + if (vm == null || host == null) { + throw new InvalidParameterValueException( + String.format("Unable to find the vm/host for vmId=%s, destHostId=%s", vmId, hostId)); + } + + vmToHostMap.put(vm, host); + } + } + return vmToHostMap; + } + + @Override + public void execute() { + ClusterDrsPlanResponse response = clusterDrsService.executeDrsPlan(this); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public Long getApiResourceId() { + return getId(); + } + + public Long getId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.Cluster; + } + + + @Override + public String getEventType() { + return EventTypes.EVENT_CLUSTER_DRS; + } + + @Override + public String getEventDescription() { + return String.format("Executing DRS plan for cluster: %d", getId()); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/GenerateClusterDrsPlanCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/GenerateClusterDrsPlanCmd.java new file mode 100644 index 00000000000..69a6c113405 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/GenerateClusterDrsPlanCmd.java @@ -0,0 +1,85 @@ +/* + * 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.admin.cluster; + +import com.cloud.user.Account; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ClusterDrsPlanResponse; +import org.apache.cloudstack.api.response.ClusterResponse; +import org.apache.cloudstack.cluster.ClusterDrsService; + +import javax.inject.Inject; + +import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsMaxMigrations; + +@APICommand(name = "generateClusterDrsPlan", description = "Generate DRS plan for a cluster", + responseObject = ClusterDrsPlanResponse.class, since = "4.19.0", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class GenerateClusterDrsPlanCmd extends BaseCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ClusterResponse.class, required = true, + description = "the ID of the Cluster") + private Long id; + + @Parameter(name = ApiConstants.MIGRATIONS, type = CommandType.INTEGER, + description = "Maximum number of VMs to migrate for a DRS execution. Defaults to value of cluster's drs.vm.migrations setting") + private Integer migrations; + + @Inject + private ClusterDrsService clusterDrsService; + + public Integer getMaxMigrations() { + if (migrations == null) { + return ClusterDrsMaxMigrations.valueIn(getId()); + } + return migrations; + } + + public Long getId() { + return id; + } + + @Override + public void execute() { + final ClusterDrsPlanResponse response = clusterDrsService.generateDrsPlan(this); + response.setResponseName(getCommandName()); + response.setObjectName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public Long getApiResourceId() { + return getId(); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.Cluster; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ListClusterDrsPlanCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ListClusterDrsPlanCmd.java new file mode 100644 index 00000000000..d34805ae2e3 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ListClusterDrsPlanCmd.java @@ -0,0 +1,62 @@ +/* + * 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.admin.cluster; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ClusterDrsPlanResponse; +import org.apache.cloudstack.api.response.ClusterResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.cluster.ClusterDrsService; + +import javax.inject.Inject; + +@APICommand(name = "listClusterDrsPlan", description = "List DRS plans for a clusters", + responseObject = ClusterDrsPlanResponse.class, since = "4.19.0", requestHasSensitiveInfo = false) +public class ListClusterDrsPlanCmd extends BaseListCmd { + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ClusterDrsPlanResponse.class, + description = "ID of the drs plan") + private Long id; + + @Parameter(name = ApiConstants.CLUSTER_ID, type = CommandType.UUID, entityType = ClusterResponse.class, + description = "ID of the cluster") + private Long clusterId; + + @Inject + private ClusterDrsService clusterDrsService; + + public Long getId() { + return id; + } + + public Long getClusterId() { + return clusterId; + } + + @Override + public void execute() { + ListResponse response = clusterDrsService.listDrsPlan(this); + response.setResponseName(getCommandName()); + response.setObjectName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanMigrationResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanMigrationResponse.java new file mode 100644 index 00000000000..4114c228e26 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanMigrationResponse.java @@ -0,0 +1,75 @@ +/* + * 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 org.apache.cloudstack.jobs.JobInfo; + +public class ClusterDrsPlanMigrationResponse extends BaseResponse { + @SerializedName(ApiConstants.VIRTUAL_MACHINE_ID) + @Param(description = "VM to migrate") + String vmId; + + @SerializedName(ApiConstants.VIRTUAL_MACHINE_NAME) + @Param(description = "VM to migrate") + String vmName; + + @SerializedName("sourcehostid") + @Param(description = "Original host for VM migration") + String srcHostId; + + @SerializedName("sourcehostname") + @Param(description = "Original host for VM migration") + String srcHostName; + + @SerializedName("destinationhostid") + @Param(description = "Destination host for VM migration") + String destHostId; + + @SerializedName("destinationhostname") + @Param(description = "Destination host for VM migration") + String destHostName; + + @SerializedName(ApiConstants.JOB_ID) + @Param(description = "id of VM migration async job") + private Long jobId; + + @SerializedName(ApiConstants.JOB_STATUS) + @Param(description = "Job status of VM migration async job") + private JobInfo.Status jobStatus; + + + public ClusterDrsPlanMigrationResponse(String vmId, String vmName, String srcHostId, String srcHostName, + String destHostId, String destHostName, Long jobId, + JobInfo.Status jobStatus) { + this.vmId = vmId; + this.vmName = vmName; + this.srcHostId = srcHostId; + this.srcHostName = srcHostName; + this.destHostId = destHostId; + this.destHostName = destHostName; + this.jobId = jobId; + this.jobStatus = jobStatus; + this.setObjectName(ApiConstants.MIGRATIONS); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanResponse.java new file mode 100644 index 00000000000..2a7fed7da4c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanResponse.java @@ -0,0 +1,83 @@ +/* + * 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 org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.cluster.ClusterDrsPlan; + +import java.util.Date; +import java.util.List; + +@EntityReference(value = ClusterDrsPlan.class) +public class ClusterDrsPlanResponse extends BaseResponse { + @SerializedName(ApiConstants.MIGRATIONS) + @Param(description = "List of migrations") + List migrationPlans; + + @SerializedName(ApiConstants.ID) + @Param(description = "unique ID of the drs plan for cluster") + private String id; + + @SerializedName(ApiConstants.CLUSTER_ID) + @Param(description = "Id of the cluster") + private String clusterId; + + @SerializedName("eventid") + @Param(description = "Start event Id of the DRS Plan") + private String eventId; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "Type of DRS Plan (Automated or Manual))") + private ClusterDrsPlan.Type type; + + @SerializedName(ApiConstants.STATUS) + @Param(description = "Status of DRS Plan") + private ClusterDrsPlan.Status status; + + @SerializedName(ApiConstants.CREATED) + private Date created; + + + public ClusterDrsPlanResponse(String clusterId, ClusterDrsPlan plan, String eventId, + List migrationPlans) { + this.clusterId = clusterId; + this.eventId = eventId; + if (plan != null) { + this.id = plan.getUuid(); + this.type = plan.getType(); + this.status = plan.getStatus(); + this.created = plan.getCreated(); + } + this.migrationPlans = migrationPlans; + this.setObjectName("drsPlan"); + } + + public List getMigrationPlans() { + return migrationPlans; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsAlgorithm.java b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsAlgorithm.java new file mode 100644 index 00000000000..ad5edc9224b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsAlgorithm.java @@ -0,0 +1,167 @@ +/* + * 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.cluster; + +import com.cloud.host.Host; +import com.cloud.offering.ServiceOffering; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.component.Adapter; +import com.cloud.vm.VirtualMachine; +import org.apache.commons.math3.stat.descriptive.moment.Mean; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; + +import javax.naming.ConfigurationException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public interface ClusterDrsAlgorithm extends Adapter { + + /** + * Determines whether a DRS operation is needed for a given cluster and host-VM + * mapping. + * + * @param clusterId + * the ID of the cluster to check + * @param cpuList + * a list of CPU allocated values for each host in the cluster + * @param memoryList + * a list of memory allocated values for each host in the cluster + * + * @return true if a DRS operation is needed, false otherwise + * + * @throws ConfigurationException + * if there is an error in the configuration + */ + boolean needsDrs(long clusterId, List cpuList, List memoryList) throws ConfigurationException; + + + /** + * Determines the metrics for a given virtual machine and destination host in a DRS cluster. + * + * @param clusterId + * the ID of the cluster to check + * @param vm + * the virtual machine to check + * @param serviceOffering + * the service offering for the virtual machine + * @param destHost + * the destination host for the virtual machine + * @param hostCpuUsedMap + * a map of host IDs to the amount of CPU used on each host + * @param hostMemoryUsedMap + * a map of host IDs to the amount of memory used on each host + * @param requiresStorageMotion + * whether storage motion is required for the virtual machine + * + * @return a ternary containing improvement, cost, benefit + */ + Ternary getMetrics(long clusterId, VirtualMachine vm, ServiceOffering serviceOffering, + Host destHost, Map hostCpuUsedMap, + Map hostMemoryUsedMap, Boolean requiresStorageMotion); + + /** + * Calculates the imbalance of the cluster after a virtual machine migration. + * + * @param serviceOffering + * the service offering for the virtual machine + * @param vm + * the virtual machine being migrated + * @param destHost + * the destination host for the virtual machine + * @param hostCpuUsedMap + * a map of host IDs to the amount of CPU used on each host + * @param hostMemoryUsedMap + * a map of host IDs to the amount of memory used on each host + * + * @return a pair containing the CPU and memory imbalance of the cluster after the migration + */ + default Pair getImbalancePostMigration(ServiceOffering serviceOffering, VirtualMachine vm, + Host destHost, Map hostCpuUsedMap, + Map hostMemoryUsedMap) { + List postCpuList = new ArrayList<>(); + List postMemoryList = new ArrayList<>(); + final int vmCpu = serviceOffering.getCpu() * serviceOffering.getSpeed(); + final long vmRam = serviceOffering.getRamSize() * 1024L * 1024L; + + for (Long hostId : hostCpuUsedMap.keySet()) { + long cpu = hostCpuUsedMap.get(hostId); + long memory = hostMemoryUsedMap.get(hostId); + if (hostId == destHost.getId()) { + postCpuList.add(cpu + vmCpu); + postMemoryList.add(memory + vmRam); + } else if (hostId.equals(vm.getHostId())) { + postCpuList.add(cpu - vmCpu); + postMemoryList.add(memory - vmRam); + } else { + postCpuList.add(cpu); + postMemoryList.add(memory); + } + } + return new Pair<>(getClusterImbalance(postCpuList), getClusterImbalance(postMemoryList)); + } + + /** + * The cluster imbalance is defined as the percentage deviation from the mean + * for a configured metric of the cluster. The standard deviation is used as a + * mathematical tool to normalize the metric data for all the resource and the + * percentage deviation provides an easy tool to compare a cluster’s current + * state against the defined imbalance threshold. Because this is essentially a + * percentage, the value is a number between 0.0 and 1.0. + * Cluster Imbalance, Ic = σc / mavg , where σc is the standard deviation and + * mavg is the mean metric value for the cluster. + */ + default Double getClusterImbalance(List metricList) { + Double clusterMeanMetric = getClusterMeanMetric(metricList); + Double clusterStandardDeviation = getClusterStandardDeviation(metricList, clusterMeanMetric); + return clusterStandardDeviation / clusterMeanMetric; + } + + /** + * Mean is the average of a collection or set of metrics. In context of a DRS + * cluster, the cluster metrics defined as the average metrics value for some + * metric (such as CPU, memory etc.) for every resource such as host. + * Cluster Mean Metric, mavg = (∑mi) / N, where mi is a measurable metric for a + * resource ‘i’ in a cluster with total N number of resources. + */ + default Double getClusterMeanMetric(List metricList) { + return new Mean().evaluate(metricList.stream().mapToDouble(i -> i).toArray()); + } + + /** + * Standard deviation is defined as the square root of the absolute squared sum + * of difference of a metric from its mean for every resource divided by the + * total number of resources. In context of the DRS, the cluster standard + * deviation is the standard deviation based on a metric of resources in a + * cluster such as for the allocation or utilisation CPU/memory metric of hosts + * in a cluster. + * Cluster Standard Deviation, σc = sqrt((∑∣mi−mavg∣^2) / N), where mavg is the + * mean metric value and mi is a measurable metric for some resource ‘i’ in the + * cluster with total N number of resources. + */ + default Double getClusterStandardDeviation(List metricList, Double mean) { + if (mean != null) { + return new StandardDeviation(false).evaluate(metricList.stream().mapToDouble(i -> i).toArray(), mean); + } else { + return new StandardDeviation(false).evaluate(metricList.stream().mapToDouble(i -> i).toArray()); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlan.java b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlan.java new file mode 100644 index 00000000000..bc1bea12752 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlan.java @@ -0,0 +1,48 @@ +/* + * 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.cluster; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import java.util.Date; + +public interface ClusterDrsPlan extends Identity, InternalIdentity { + + long getClusterId(); + + Type getType(); + + Date getCreated(); + + Status getStatus(); + + String getUuid(); + + long getEventId(); + + enum Type { + AUTOMATED, MANUAL + } + + enum Status { + UNDER_REVIEW, READY, IN_PROGRESS, COMPLETED + } +} diff --git a/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanMigration.java b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanMigration.java new file mode 100644 index 00000000000..755406926fd --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanMigration.java @@ -0,0 +1,25 @@ +/* + * 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.cluster; + +import org.apache.cloudstack.api.InternalIdentity; + +public interface ClusterDrsPlanMigration extends InternalIdentity { +} diff --git a/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsService.java b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsService.java new file mode 100644 index 00000000000..91be8c535a4 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsService.java @@ -0,0 +1,108 @@ +/* + * 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.cluster; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.component.Manager; +import com.cloud.utils.concurrency.Scheduler; +import org.apache.cloudstack.api.command.admin.cluster.ExecuteClusterDrsPlanCmd; +import org.apache.cloudstack.api.command.admin.cluster.GenerateClusterDrsPlanCmd; +import org.apache.cloudstack.api.command.admin.cluster.ListClusterDrsPlanCmd; +import org.apache.cloudstack.api.response.ClusterDrsPlanResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +public interface ClusterDrsService extends Manager, Configurable, Scheduler { + + ConfigKey ClusterDrsPlanExpireInterval = new ConfigKey<>(Integer.class, "drs.plan.expire.interval", + ConfigKey.CATEGORY_ADVANCED, "30", "The interval in days after which the DRS events will be cleaned up.", + false, ConfigKey.Scope.Global, null, "Expire interval for old DRS plans", null, null, null); + + ConfigKey ClusterDrsEnabled = new ConfigKey<>(Boolean.class, "drs.automatic.enable", + ConfigKey.CATEGORY_ADVANCED, "false", "Enable/disable automatic DRS on a cluster.", true, + ConfigKey.Scope.Cluster, null, "Enable automatic DRS", null, null, null); + + ConfigKey ClusterDrsInterval = new ConfigKey<>(Integer.class, "drs.automatic.interval", + ConfigKey.CATEGORY_ADVANCED, "60", + "The interval in minutes after which a periodic background thread will schedule DRS for a cluster.", true, + ConfigKey.Scope.Cluster, null, "Interval for Automatic DRS ", null, null, null); + + ConfigKey ClusterDrsMaxMigrations = new ConfigKey<>(Integer.class, "drs.max.migrations", + ConfigKey.CATEGORY_ADVANCED, "50", + "Maximum number of live migrations in a DRS execution.", + true, ConfigKey.Scope.Cluster, null, "Maximum number of migrations for DRS", null, null, null); + + ConfigKey ClusterDrsAlgorithm = new ConfigKey<>(String.class, "drs.algorithm", + ConfigKey.CATEGORY_ADVANCED, "balanced", "The DRS algorithm to be executed on the cluster. Possible values are condensed, balanced.", + true, ConfigKey.Scope.Cluster, null, "DRS algorithm", null, null, + null, ConfigKey.Kind.Select, "condensed,balanced"); + + ConfigKey ClusterDrsImbalanceThreshold = new ConfigKey<>(Float.class, "drs.imbalance", + ConfigKey.CATEGORY_ADVANCED, "0.4", + "Value of imbalance allowed in the cluster. 1.0 means no imbalance is allowed and 0.0 means full imbalance is allowed", + true, ConfigKey.Scope.Cluster, null, "DRS imbalance", null, null, null); + + ConfigKey ClusterDrsMetric = new ConfigKey<>(String.class, "drs.metric", ConfigKey.CATEGORY_ADVANCED, + "memory", + "The allocated resource metric used to measure imbalance in a cluster. Possible values are memory, cpu.", + true, ConfigKey.Scope.Cluster, null, "DRS metric", null, null, null, ConfigKey.Kind.Select, + "memory,cpu"); + + /** + * Generate a DRS plan for a cluster and save it as per the parameters + * + * @param cmd + * the GenerateClusterDrsPlanCmd object containing the command parameters + * + * @return a ClusterDrsPlanResponse object containing information regarding the migrations + */ + ClusterDrsPlanResponse generateDrsPlan(GenerateClusterDrsPlanCmd cmd); + + /** + * Executes a DRS plan for a cluster. + * + * @param cmd + * the ExecuteClusterDrsPlanCmd object containing the ID of the cluster and the map of virtual + * machines to hosts + * + * @return ClusterDrsPlanResponse response object + * + * @throws InvalidParameterValueException + * if there is already a plan in READY or IN_PROGRESS state for the + * cluster or if the + * cluster cannot be found by ID + */ + ClusterDrsPlanResponse executeDrsPlan(ExecuteClusterDrsPlanCmd cmd); + + /** + * Lists DRS plans for a cluster or a specific plan. + * + * @param cmd + * the ListClusterDrsPlanCmd object containing the ID of the cluster or the ID of the plan + * + * @return a ListResponse object containing a list of ClusterDrsPlanResponse objects and the total number of plans + * + * @throws InvalidParameterValueException + * if both clusterId and planId are specified or if the cluster cannot be + * found by ID + */ + ListResponse listDrsPlan(ListClusterDrsPlanCmd cmd); +} diff --git a/client/pom.xml b/client/pom.xml index 8ff1fea9adb..c9b6e9172c1 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -527,6 +527,16 @@ cloud-plugin-network-globodns ${project.version} + + org.apache.cloudstack + cloud-plugin-cluster-drs-balanced + ${project.version} + + + org.apache.cloudstack + cloud-plugin-cluster-drs-condensed + ${project.version} + org.apache.cloudstack cloud-plugin-database-quota diff --git a/core/src/main/resources/META-INF/cloudstack/cluster/module.properties b/core/src/main/resources/META-INF/cloudstack/cluster/module.properties new file mode 100644 index 00000000000..e1b7a0e6eb2 --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/cluster/module.properties @@ -0,0 +1,21 @@ +# +# 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. +# + +name=cluster +parent=core diff --git a/core/src/main/resources/META-INF/cloudstack/cluster/spring-core-lifecycle-cluster-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/cluster/spring-core-lifecycle-cluster-context-inheritable.xml new file mode 100644 index 00000000000..6278c0fc816 --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/cluster/spring-core-lifecycle-cluster-context-inheritable.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index a7f384c76a9..2b2caeaaa66 100644 --- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -346,4 +346,8 @@ + + + diff --git a/engine/schema/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanMigrationVO.java b/engine/schema/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanMigrationVO.java new file mode 100644 index 00000000000..eab2e555d69 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanMigrationVO.java @@ -0,0 +1,105 @@ +/* + * 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.cluster; + +import org.apache.cloudstack.jobs.JobInfo; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "cluster_drs_plan_migration") +public class ClusterDrsPlanMigrationVO implements ClusterDrsPlanMigration { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + Long id; + + @Column(name = "plan_id", nullable = false) + private long planId; + + @Column(name = "vm_id", nullable = false) + private long vmId; + + @Column(name = "src_host_id", nullable = false) + private long srcHostId; + + @Column(name = "dest_host_id", nullable = false) + private long destHostId; + + @Column(name = "job_id") + private Long jobId; + + @Column(name = "status") + private JobInfo.Status status; + + + public ClusterDrsPlanMigrationVO(long planId, long vmId, long srcHostId, long destHostId) { + this.planId = planId; + this.vmId = vmId; + this.srcHostId = srcHostId; + this.destHostId = destHostId; + } + + protected ClusterDrsPlanMigrationVO() { + + } + + public long getId() { + return id; + } + + public long getPlanId() { + return planId; + } + + public long getVmId() { + return vmId; + } + + public long getSrcHostId() { + return srcHostId; + } + + public long getDestHostId() { + return destHostId; + } + + public Long getJobId() { + return jobId; + } + + public void setJobId(long jobId) { + this.jobId = jobId; + } + + public JobInfo.Status getStatus() { + return status; + } + + public void setStatus(JobInfo.Status status) { + this.status = status; + } + +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanVO.java b/engine/schema/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanVO.java new file mode 100644 index 00000000000..0ce25ae90fe --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/cluster/ClusterDrsPlanVO.java @@ -0,0 +1,103 @@ +/* + * 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.cluster; + +import com.cloud.utils.db.GenericDao; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "cluster_drs_plan") +public class ClusterDrsPlanVO implements ClusterDrsPlan { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + Long id; + + @Column(name = "uuid", nullable = false) + String uuid; + + @Column(name = GenericDao.CREATED_COLUMN) + Date created; + + @Column(name = "cluster_id") + private long clusterId; + + @Column(name = "event_id") + private long eventId; + + @Column(name = "type") + private Type type; + + @Column(name = "status") + private Status status; + + public ClusterDrsPlanVO(long clusterId, long eventId, Type type, Status status) { + uuid = UUID.randomUUID().toString(); + this.clusterId = clusterId; + this.eventId = eventId; + this.type = type; + this.status = status; + } + + protected ClusterDrsPlanVO() { + uuid = UUID.randomUUID().toString(); + } + + public long getId() { + return id; + } + + public long getEventId() { + return eventId; + } + + public long getClusterId() { + return clusterId; + } + + public Type getType() { + return type; + } + + public Date getCreated() { + return created; + } + + @Override + public Status getStatus() { + return status; + } + + public String getUuid() { + return uuid; + } + + public void setStatus(Status status) { + this.status = status; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanDao.java b/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanDao.java new file mode 100644 index 00000000000..2fa524824ef --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanDao.java @@ -0,0 +1,41 @@ +/* + * 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.cluster.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.cluster.ClusterDrsPlan; +import org.apache.cloudstack.cluster.ClusterDrsPlanVO; + +import java.util.Date; +import java.util.List; + +public interface ClusterDrsPlanDao extends GenericDao { + List listByStatus(ClusterDrsPlan.Status status); + + List listByClusterIdAndStatus(Long clusterId, ClusterDrsPlan.Status status); + + ClusterDrsPlanVO listLatestPlanForClusterId(Long clusterId); + + Pair, Integer> searchAndCount(Long clusterId, Long planId, Long startIndex, + Long pageSizeVal); + + int expungeBeforeDate(Date date); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanDaoImpl.java new file mode 100644 index 00000000000..8683258e485 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanDaoImpl.java @@ -0,0 +1,107 @@ +/* + * 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.cluster.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.cluster.ClusterDrsPlan; +import org.apache.cloudstack.cluster.ClusterDrsPlanVO; + +import java.util.Date; +import java.util.List; + +public class ClusterDrsPlanDaoImpl extends GenericDaoBase implements ClusterDrsPlanDao { + public ClusterDrsPlanDaoImpl() { + } + + @Override + public List listByStatus(ClusterDrsPlan.Status status) { + SearchBuilder sb; + sb = createSearchBuilder(); + sb.and(ApiConstants.STATUS, sb.entity().getStatus(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters(ApiConstants.STATUS, status); + return search(sc, null); + } + + @Override + public List listByClusterIdAndStatus(Long clusterId, ClusterDrsPlan.Status status) { + SearchBuilder sb; + sb = createSearchBuilder(); + sb.and(ApiConstants.CLUSTER_ID, sb.entity().getClusterId(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.STATUS, sb.entity().getStatus(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters(ApiConstants.CLUSTER_ID, clusterId); + sc.setParameters(ApiConstants.STATUS, status); + return search(sc, null); + } + + @Override + public ClusterDrsPlanVO listLatestPlanForClusterId(Long clusterId) { + SearchBuilder sb; + sb = createSearchBuilder(); + sb.and(ApiConstants.CLUSTER_ID, sb.entity().getClusterId(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters(ApiConstants.CLUSTER_ID, clusterId); + Filter filter = new Filter(ClusterDrsPlanVO.class, "id", false, 0L, 1L); + List plans = listBy(sc, filter); + if (plans != null && !plans.isEmpty()) { + return plans.get(0); + } + return null; + } + + @Override + public Pair, Integer> searchAndCount(Long clusterId, Long planId, Long startIndex, + Long pageSizeVal) { + SearchBuilder sb; + sb = createSearchBuilder(); + sb.and(ApiConstants.CLUSTER_ID, sb.entity().getClusterId(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.ID, sb.entity().getId(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + if (clusterId != null) { + sc.setParameters(ApiConstants.CLUSTER_ID, clusterId); + } + if (planId != null) { + sc.setParameters(ApiConstants.ID, planId); + } + Filter filter = new Filter(ClusterDrsPlanVO.class, "id", false, startIndex, pageSizeVal); + return searchAndCount(sc, filter); + } + + @Override + public int expungeBeforeDate(Date date) { + SearchBuilder sb; + sb = createSearchBuilder(); + sb.and(ApiConstants.CREATED, sb.entity().getCreated(), SearchCriteria.Op.LT); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters(ApiConstants.CREATED, date); + return expunge(sc); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanMigrationDao.java b/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanMigrationDao.java new file mode 100644 index 00000000000..a6f51ca955a --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanMigrationDao.java @@ -0,0 +1,33 @@ +/* + * 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.cluster.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.cluster.ClusterDrsPlanMigrationVO; + +import java.util.List; + +public interface ClusterDrsPlanMigrationDao extends GenericDao { + List listByPlanId(long planId); + + List listPlanMigrationsToExecute(Long id); + + List listPlanMigrationsInProgress(Long id); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanMigrationDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanMigrationDaoImpl.java new file mode 100644 index 00000000000..c1edce1d60a --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/cluster/dao/ClusterDrsPlanMigrationDaoImpl.java @@ -0,0 +1,70 @@ +/* + * 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.cluster.dao; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.cluster.ClusterDrsPlanMigrationVO; +import org.apache.cloudstack.jobs.JobInfo; + +import java.util.List; + +public class ClusterDrsPlanMigrationDaoImpl extends GenericDaoBase implements ClusterDrsPlanMigrationDao { + public ClusterDrsPlanMigrationDaoImpl() { + } + + @Override + public List listByPlanId(long planId) { + SearchBuilder sb = createSearchBuilder(); + sb.and("planId", sb.entity().getPlanId(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("planId", planId); + Filter filter = new Filter(ClusterDrsPlanMigrationVO.class, "id", true, null, null); + return search(sc, filter); + } + + @Override + public List listPlanMigrationsToExecute(Long id) { + SearchBuilder sb = createSearchBuilder(); + sb.and("planId", sb.entity().getPlanId(), SearchCriteria.Op.EQ); + sb.and("status", sb.entity().getStatus(), SearchCriteria.Op.NULL); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("planId", id); + Filter filter = new Filter(ClusterDrsPlanMigrationVO.class, "id", true, null, null); + return search(sc, filter); + } + + @Override + public List listPlanMigrationsInProgress(Long id) { + SearchBuilder sb = createSearchBuilder(); + sb.and("planId", sb.entity().getPlanId(), SearchCriteria.Op.EQ); + sb.and("status", sb.entity().getStatus(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("planId", id); + sc.setParameters("status", JobInfo.Status.IN_PROGRESS); + Filter filter = new Filter(ClusterDrsPlanMigrationVO.class, "id", true, null, null); + return search(sc, filter); + } +} diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index c00bbc15567..647be81098c 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -278,4 +278,6 @@ + + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql index b1353161548..c14730adb86 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql @@ -184,6 +184,43 @@ ALTER TABLE `cloud`.`kubernetes_cluster` MODIFY COLUMN `kubernetes_version_id` b -- Set removed state for all removed accounts UPDATE `cloud`.`account` SET state='removed' WHERE `removed` IS NOT NULL; +-- Add tables for Cluster DRS +DROP TABLE IF EXISTS `cloud`.`cluster_drs_plan`; +CREATE TABLE `cloud`.`cluster_drs_plan` ( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `cluster_id` bigint unsigned NOT NULL, + `event_id` bigint unsigned NOT NULL, + `uuid` varchar(40) NOT NULL COMMENT 'schedule uuid', + `type` varchar(20) NOT NULL COMMENT 'type of plan', + `status` varchar(20) NOT NULL COMMENT 'status of plan', + `created` datetime NOT NULL COMMENT 'date created', + PRIMARY KEY (`id`), + INDEX `i_cluster_drs_plan__cluster_id_status`(`cluster_id`, `status`), + INDEX `i_cluster_drs_plan__status`(`status`), + INDEX `i_cluster_drs_plan__created`(`created`), + CONSTRAINT `fk_cluster_drs_plan__cluster_id` FOREIGN KEY (`cluster_id`) REFERENCES `cluster`(`id`) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8; + +DROP TABLE IF EXISTS `cloud`.`cluster_drs_plan_migration`; +CREATE TABLE `cloud`.`cluster_drs_plan_migration` ( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `plan_id` bigint unsigned NOT NULL, + `vm_id` bigint unsigned NOT NULL, + `src_host_id` bigint unsigned NOT NULL, + `dest_host_id` bigint unsigned NOT NULL, + `job_id` bigint unsigned NULL, + `status` varchar(20) NULL COMMENT 'status of async job', + PRIMARY KEY (`id`), + INDEX `i_cluster_drs_plan_migration__plan_id_status`(`plan_id`, `status`), + CONSTRAINT `fk_cluster_drs_plan_migration__plan_id` FOREIGN KEY (`plan_id`) REFERENCES `cluster_drs_plan`(`id`) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8; + +INSERT INTO `cloud`.`configuration_subgroup` (`name`, `keywords`, `precedence`, `group_id`) VALUES ('DRS', 'drs', 4, (SELECT id FROM `cloud`.`configuration_group` WHERE `name` = 'Miscellaneous')); + +UPDATE `cloud`.`configuration` + SET subgroup_id = (SELECT id FROM `cloud`.`configuration_subgroup` WHERE name = 'DRS') + WHERE name IN ('drs.automatic.enable', 'drs.algorithm', 'drs.automatic.interval', 'drs.max.migrations', 'drs.imbalance', 'drs.metric', 'drs.plan.expire.interval'); + -- Add table for snapshot zone reference CREATE TABLE `cloud`.`snapshot_zone_ref` ( `id` bigint unsigned NOT NULL auto_increment, diff --git a/plugins/affinity-group-processors/explicit-dedication/src/main/java/org/apache/cloudstack/affinity/ExplicitDedicationProcessor.java b/plugins/affinity-group-processors/explicit-dedication/src/main/java/org/apache/cloudstack/affinity/ExplicitDedicationProcessor.java index 8070a7409b5..95283028441 100644 --- a/plugins/affinity-group-processors/explicit-dedication/src/main/java/org/apache/cloudstack/affinity/ExplicitDedicationProcessor.java +++ b/plugins/affinity-group-processors/explicit-dedication/src/main/java/org/apache/cloudstack/affinity/ExplicitDedicationProcessor.java @@ -86,7 +86,7 @@ public class ExplicitDedicationProcessor extends AffinityProcessorBase implement * This IncludeList is then used to update the avoid list for a given data center. */ @Override - public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid) throws AffinityConflictException { + public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid, List vmList) throws AffinityConflictException { VirtualMachine vm = vmProfile.getVirtualMachine(); List vmGroupMappings = _affinityGroupVMMapDao.findByVmIdType(vm.getId(), getType()); DataCenter dc = _dcDao.findById(vm.getDataCenterId()); diff --git a/plugins/affinity-group-processors/host-affinity/src/main/java/org/apache/cloudstack/affinity/HostAffinityProcessor.java b/plugins/affinity-group-processors/host-affinity/src/main/java/org/apache/cloudstack/affinity/HostAffinityProcessor.java index 0e7c536b0f9..07c1dd5ff88 100644 --- a/plugins/affinity-group-processors/host-affinity/src/main/java/org/apache/cloudstack/affinity/HostAffinityProcessor.java +++ b/plugins/affinity-group-processors/host-affinity/src/main/java/org/apache/cloudstack/affinity/HostAffinityProcessor.java @@ -16,14 +16,16 @@ // under the License. package org.apache.cloudstack.affinity; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.HashSet; import java.util.ArrayList; import javax.inject.Inject; -import com.cloud.vm.VMInstanceVO; import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; @@ -50,37 +52,56 @@ public class HostAffinityProcessor extends AffinityProcessorBase implements Affi protected AffinityGroupVMMapDao _affinityGroupVMMapDao; @Override - public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid) throws AffinityConflictException { + public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid, List vmList) throws AffinityConflictException { VirtualMachine vm = vmProfile.getVirtualMachine(); List vmGroupMappings = _affinityGroupVMMapDao.findByVmIdType(vm.getId(), getType()); if (CollectionUtils.isNotEmpty(vmGroupMappings)) { for (AffinityGroupVMMapVO vmGroupMapping : vmGroupMappings) { - processAffinityGroup(vmGroupMapping, plan, vm); + processAffinityGroup(vmGroupMapping, plan, vm, vmList); } } } + /** * Process Affinity Group for VM deployment */ - protected void processAffinityGroup(AffinityGroupVMMapVO vmGroupMapping, DeploymentPlan plan, VirtualMachine vm) { + protected void processAffinityGroup(AffinityGroupVMMapVO vmGroupMapping, DeploymentPlan plan, VirtualMachine vm, List vmList) { AffinityGroupVO group = _affinityGroupDao.findById(vmGroupMapping.getAffinityGroupId()); s_logger.debug("Processing affinity group " + group.getName() + " for VM Id: " + vm.getId()); List groupVMIds = _affinityGroupVMMapDao.listVmIdsByAffinityGroup(group.getId()); groupVMIds.remove(vm.getId()); - List preferredHosts = getPreferredHostsFromGroupVMIds(groupVMIds); + List preferredHosts = getPreferredHostsFromGroupVMIds(groupVMIds, vmList); plan.setPreferredHosts(preferredHosts); } + + /** + * Process Affinity Group for VM deployment + */ + protected void processAffinityGroup(AffinityGroupVMMapVO vmGroupMapping, DeploymentPlan plan, VirtualMachine vm) { + processAffinityGroup(vmGroupMapping, plan, vm, Collections.emptyList()); + } + /** * Get host ids set from vm ids list */ + protected Set getHostIdSet(List vmIds) { + return getHostIdSet(vmIds, Collections.emptyList()); + } + + protected Set getHostIdSet(List vmIds, List vmList) { Set hostIds = new HashSet<>(); + Map vmIdVmMap = getVmIdVmMap(vmList); for (Long groupVMId : vmIds) { - VMInstanceVO groupVM = _vmInstanceDao.findById(groupVMId); + VirtualMachine groupVM = vmIdVmMap.get(groupVMId); + if (groupVM == null) { + groupVM = _vmInstanceDao.findById(groupVMId); + } + if (groupVM != null && groupVM.getHostId() != null) { hostIds.add(groupVM.getHostId()); } @@ -88,11 +109,19 @@ public class HostAffinityProcessor extends AffinityProcessorBase implements Affi return hostIds; } + protected Map getVmIdVmMap(List vmList) { + Map vmIdVmMap = new HashMap<>(); + for (VirtualMachine vm : vmList) { + vmIdVmMap.put(vm.getId(), vm); + } + return vmIdVmMap; + } + /** * Get preferred host ids list from the affinity group VMs */ - protected List getPreferredHostsFromGroupVMIds(List vmIds) { - return new ArrayList<>(getHostIdSet(vmIds)); + protected List getPreferredHostsFromGroupVMIds(List vmIds, List vmList) { + return new ArrayList<>(getHostIdSet(vmIds, vmList)); } @Override diff --git a/plugins/affinity-group-processors/host-affinity/src/test/java/org/apache/cloudstack/affinity/HostAffinityProcessorTest.java b/plugins/affinity-group-processors/host-affinity/src/test/java/org/apache/cloudstack/affinity/HostAffinityProcessorTest.java index 5dc9270e69e..66f3a37ae1b 100644 --- a/plugins/affinity-group-processors/host-affinity/src/test/java/org/apache/cloudstack/affinity/HostAffinityProcessorTest.java +++ b/plugins/affinity-group-processors/host-affinity/src/test/java/org/apache/cloudstack/affinity/HostAffinityProcessorTest.java @@ -36,6 +36,7 @@ import org.mockito.Spy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static org.junit.Assert.assertEquals; @@ -139,7 +140,7 @@ public class HostAffinityProcessorTest { @Test public void testGetPreferredHostsFromGroupVMIdsMultipleVMs() { List list = new ArrayList<>(Arrays.asList(GROUP_VM_1_ID, GROUP_VM_2_ID)); - List preferredHosts = processor.getPreferredHostsFromGroupVMIds(list); + List preferredHosts = processor.getPreferredHostsFromGroupVMIds(list, Collections.emptyList()); assertNotNull(preferredHosts); assertEquals(1, preferredHosts.size()); assertEquals(HOST_ID, preferredHosts.get(0)); @@ -148,7 +149,7 @@ public class HostAffinityProcessorTest { @Test public void testGetPreferredHostsFromGroupVMIdsEmptyVMsList() { List list = new ArrayList<>(); - List preferredHosts = processor.getPreferredHostsFromGroupVMIds(list); + List preferredHosts = processor.getPreferredHostsFromGroupVMIds(list, Collections.emptyList()); assertNotNull(preferredHosts); assertTrue(preferredHosts.isEmpty()); } diff --git a/plugins/affinity-group-processors/host-anti-affinity/src/main/java/org/apache/cloudstack/affinity/HostAntiAffinityProcessor.java b/plugins/affinity-group-processors/host-anti-affinity/src/main/java/org/apache/cloudstack/affinity/HostAntiAffinityProcessor.java index 21ac1ac3e5a..2a3c5796dda 100644 --- a/plugins/affinity-group-processors/host-anti-affinity/src/main/java/org/apache/cloudstack/affinity/HostAntiAffinityProcessor.java +++ b/plugins/affinity-group-processors/host-anti-affinity/src/main/java/org/apache/cloudstack/affinity/HostAntiAffinityProcessor.java @@ -63,7 +63,7 @@ public class HostAntiAffinityProcessor extends AffinityProcessorBase implements protected VMReservationDao _reservationDao; @Override - public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid) throws AffinityConflictException { + public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid, List vmList) throws AffinityConflictException { VirtualMachine vm = vmProfile.getVirtualMachine(); List vmGroupMappings = _affinityGroupVMMapDao.findByVmIdType(vm.getId(), getType()); diff --git a/plugins/affinity-group-processors/non-strict-host-affinity/src/main/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessor.java b/plugins/affinity-group-processors/non-strict-host-affinity/src/main/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessor.java index a51102105eb..cdb3447f5a5 100644 --- a/plugins/affinity-group-processors/non-strict-host-affinity/src/main/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessor.java +++ b/plugins/affinity-group-processors/non-strict-host-affinity/src/main/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessor.java @@ -63,7 +63,7 @@ public class NonStrictHostAffinityProcessor extends AffinityProcessorBase implem private int vmCapacityReleaseInterval; @Override - public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid) throws AffinityConflictException { + public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid, List vmList) throws AffinityConflictException { VirtualMachine vm = vmProfile.getVirtualMachine(); List vmGroupMappings = affinityGroupVMMapDao.findByVmIdType(vm.getId(), getType()); diff --git a/plugins/drs/cluster/balanced/pom.xml b/plugins/drs/cluster/balanced/pom.xml new file mode 100644 index 00000000000..aaec36bbac6 --- /dev/null +++ b/plugins/drs/cluster/balanced/pom.xml @@ -0,0 +1,33 @@ + + + + + 4.0.0 + Apache CloudStack Plugin - Cluster DRS Algorithm - Balanced + cloud-plugin-cluster-drs-balanced + + org.apache.cloudstack + cloudstack-plugins + 4.19.0.0-SNAPSHOT + ../../../pom.xml + + diff --git a/plugins/drs/cluster/balanced/src/main/java/org/apache/cloudstack/cluster/Balanced.java b/plugins/drs/cluster/balanced/src/main/java/org/apache/cloudstack/cluster/Balanced.java new file mode 100644 index 00000000000..dc15a820560 --- /dev/null +++ b/plugins/drs/cluster/balanced/src/main/java/org/apache/cloudstack/cluster/Balanced.java @@ -0,0 +1,99 @@ +/* + * 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.cluster; + +import com.cloud.host.Host; +import com.cloud.offering.ServiceOffering; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.component.AdapterBase; +import com.cloud.vm.VirtualMachine; + +import javax.naming.ConfigurationException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsImbalanceThreshold; +import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsMetric; + +public class Balanced extends AdapterBase implements ClusterDrsAlgorithm { + + @Override + public String getName() { + return "balanced"; + } + + @Override + public boolean needsDrs(long clusterId, List cpuList, List memoryList) throws ConfigurationException { + Double cpuImbalance = getClusterImbalance(cpuList); + Double memoryImbalance = getClusterImbalance(memoryList); + double threshold = getThreshold(clusterId); + String metric = ClusterDrsMetric.valueIn(clusterId); + switch (metric) { + case "cpu": + return cpuImbalance > threshold; + case "memory": + return memoryImbalance > threshold; + default: + throw new ConfigurationException( + String.format("Invalid metric: %s for cluster: %d", metric, clusterId)); + } + } + + private double getThreshold(long clusterId) throws ConfigurationException { + return 1.0 - ClusterDrsImbalanceThreshold.valueIn(clusterId); + } + + @Override + public Ternary getMetrics(long clusterId, VirtualMachine vm, + ServiceOffering serviceOffering, Host destHost, + Map hostCpuUsedMap, Map hostMemoryUsedMap, + Boolean requiresStorageMotion) { + Double preCpuImbalance = getClusterImbalance(new ArrayList<>(hostCpuUsedMap.values())); + Double preMemoryImbalance = getClusterImbalance(new ArrayList<>(hostMemoryUsedMap.values())); + + Pair imbalancePair = getImbalancePostMigration(serviceOffering, vm, destHost, hostCpuUsedMap, + hostMemoryUsedMap); + Double postCpuImbalance = imbalancePair.first(); + Double postMemoryImbalance = imbalancePair.second(); + + // This needs more research to determine the cost and benefit of a migration + // TODO: Cost should be a factor of the VM size and the host capacity + // TODO: Benefit should be a factor of the VM size and the host capacity and the number of VMs on the host + double cost = 0.0; + double benefit = 1.0; + + String metric = ClusterDrsMetric.valueIn(clusterId); + final double improvement; + switch (metric) { + case "cpu": + improvement = preCpuImbalance - postCpuImbalance; + break; + case "memory": + improvement = preMemoryImbalance - postMemoryImbalance; + break; + default: + improvement = preCpuImbalance + preMemoryImbalance - postCpuImbalance - postMemoryImbalance; + } + + return new Ternary<>(improvement, cost, benefit); + } +} diff --git a/plugins/drs/cluster/balanced/src/main/resources/META-INF/cloudstack/balanced/module.properties b/plugins/drs/cluster/balanced/src/main/resources/META-INF/cloudstack/balanced/module.properties new file mode 100644 index 00000000000..77d8d206971 --- /dev/null +++ b/plugins/drs/cluster/balanced/src/main/resources/META-INF/cloudstack/balanced/module.properties @@ -0,0 +1,18 @@ +# 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. +name=balanced +parent=cluster diff --git a/plugins/drs/cluster/balanced/src/main/resources/META-INF/cloudstack/balanced/spring-balanced-context.xml b/plugins/drs/cluster/balanced/src/main/resources/META-INF/cloudstack/balanced/spring-balanced-context.xml new file mode 100644 index 00000000000..8c58f62097a --- /dev/null +++ b/plugins/drs/cluster/balanced/src/main/resources/META-INF/cloudstack/balanced/spring-balanced-context.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/plugins/drs/cluster/balanced/src/test/java/org/apache/cloudstack/cluster/BalancedTest.java b/plugins/drs/cluster/balanced/src/test/java/org/apache/cloudstack/cluster/BalancedTest.java new file mode 100644 index 00000000000..4b84049e49a --- /dev/null +++ b/plugins/drs/cluster/balanced/src/test/java/org/apache/cloudstack/cluster/BalancedTest.java @@ -0,0 +1,227 @@ +/* + * 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.cluster; + +import com.cloud.host.Host; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.utils.Ternary; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.naming.ConfigurationException; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsImbalanceThreshold; +import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsMetric; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class BalancedTest { + + @InjectMocks + Balanced balanced; + + VirtualMachine vm1, vm2, vm3; + + Host destHost; + + ServiceOfferingVO serviceOffering; + + long clusterId = 1L; + + Map> hostVmMap; + + List cpuList, memoryList; + + Map hostCpuUsedMap, hostMemoryUsedMap; + + + @Mock + private ServiceOfferingDao serviceOfferingDao; + + + private AutoCloseable closeable; + + @Before + public void setUp() throws NoSuchFieldException, IllegalAccessException { + closeable = MockitoAnnotations.openMocks(this); + + + vm1 = Mockito.mock(VirtualMachine.class); + vm2 = Mockito.mock(VirtualMachine.class); + vm3 = Mockito.mock(VirtualMachine.class); // vm to migrate + + destHost = Mockito.mock(Host.class); + hostVmMap = new HashMap<>(); + hostVmMap.put(1L, Collections.singletonList(vm1)); + hostVmMap.put(2L, Arrays.asList(vm2, vm3)); + + serviceOffering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(vm3.getHostId()).thenReturn(2L); + + Mockito.when(destHost.getId()).thenReturn(1L); + + Mockito.when(serviceOffering.getCpu()).thenReturn(1); + Mockito.when(serviceOffering.getSpeed()).thenReturn(1000); + Mockito.when(serviceOffering.getRamSize()).thenReturn(512); + + overrideDefaultConfigValue(ClusterDrsImbalanceThreshold, "_defaultValue", "0.5"); + + cpuList = Arrays.asList(1L, 2L); + memoryList = Arrays.asList(512L, 2048L); + + hostCpuUsedMap = new HashMap<>(); + hostCpuUsedMap.put(1L, 1000L); + hostCpuUsedMap.put(2L, 2000L); + + hostMemoryUsedMap = new HashMap<>(); + hostMemoryUsedMap.put(1L, 512L * 1024L * 1024L); + hostMemoryUsedMap.put(2L, 2048L * 1024L * 1024L); + } + + private void overrideDefaultConfigValue(final ConfigKey configKey, final String name, + final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = ConfigKey.class.getDeclaredField(name); + f.setAccessible(true); + f.set(configKey, o); + } + + @After + public void tearDown() throws Exception { + closeable.close(); + } + + /** + * needsDrs tests + *

Scenarios to test for needsDrs + *

1. cluster with cpu metric + *

2. cluster with memory metric + *

3. cluster with "unknown" metric + *

+ *

CPU imbalance = 0.333 + *

Memory imbalance = 0.6 + */ + + /* + 1. cluster with cpu metric + 0.3333 > 0.5 -> False + */ + @Test + public void needsDrsWithCpu() throws ConfigurationException, NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "cpu"); + assertFalse(balanced.needsDrs(clusterId, cpuList, memoryList)); + } + + /* + 2. cluster with memory metric + 0.6 > 0.5 -> True + */ + @Test + public void needsDrsWithMemory() throws ConfigurationException, NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "memory"); + assertTrue(balanced.needsDrs(clusterId, cpuList, memoryList)); + } + + /* 3. cluster with "unknown" metric */ + @Test + public void needsDrsWithUnknown() throws ConfigurationException, NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "unknown"); + assertThrows(ConfigurationException.class, () -> balanced.needsDrs(clusterId, cpuList, memoryList)); + } + + /** + * getMetrics tests + *

Scenarios to test for getMetrics + *

1. cluster with cpu metric + *

2. cluster with memory metric + *

3. cluster with default metric + *

+ *

Pre + *

CPU imbalance = 0.333333 + *

Memory imbalance = 0.6 + *

+ *

Post + *

CPU imbalance = 0.3333 + *

Memory imbalance = 0.2 + *

+ *

Cost 512.0 + *

Benefit (0.6-0.2) * 8192 = 3276.8 + */ + + /* + 1. cluster with cpu metric + improvement = 0.3333 - 0.3333 = 0.0 + */ + @Test + public void getMetricsWithCpu() throws NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "cpu"); + Ternary result = balanced.getMetrics(clusterId, vm3, serviceOffering, destHost, + hostCpuUsedMap, hostMemoryUsedMap, false); + assertEquals(0.0, result.first(), 0.01); + assertEquals(0.0, result.second(), 0.0); + assertEquals(1.0, result.third(), 0.0); + } + + /* + 2. cluster with memory metric + improvement = 0.6 - 0.2 = 0.4 + */ + @Test + public void getMetricsWithMemory() throws NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "memory"); + Ternary result = balanced.getMetrics(clusterId, vm3, serviceOffering, destHost, + hostCpuUsedMap, hostMemoryUsedMap, false); + assertEquals(0.4, result.first(), 0.01); + assertEquals(0, result.second(), 0.0); + assertEquals(1, result.third(), 0.0); + } + + /* + 3. cluster with default metric + improvement = 0.3333 + 0.6 - 0.3333 - 0.2 = 0.4 + */ + @Test + public void getMetricsWithDefault() throws NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "both"); + Ternary result = balanced.getMetrics(clusterId, vm3, serviceOffering, destHost, + hostCpuUsedMap, hostMemoryUsedMap, false); + assertEquals(0.4, result.first(), 0.01); + assertEquals(0, result.second(), 0.0); + assertEquals(1, result.third(), 0.0); + } +} diff --git a/plugins/drs/cluster/condensed/pom.xml b/plugins/drs/cluster/condensed/pom.xml new file mode 100644 index 00000000000..ea8acdc6d1b --- /dev/null +++ b/plugins/drs/cluster/condensed/pom.xml @@ -0,0 +1,33 @@ + + + + + 4.0.0 + Apache CloudStack Plugin - Cluster DRS Algorithm - Condensed + cloud-plugin-cluster-drs-condensed + + org.apache.cloudstack + cloudstack-plugins + 4.19.0.0-SNAPSHOT + ../../../pom.xml + + diff --git a/plugins/drs/cluster/condensed/src/main/java/org/apache/cloudstack/cluster/Condensed.java b/plugins/drs/cluster/condensed/src/main/java/org/apache/cloudstack/cluster/Condensed.java new file mode 100644 index 00000000000..aefd11905ef --- /dev/null +++ b/plugins/drs/cluster/condensed/src/main/java/org/apache/cloudstack/cluster/Condensed.java @@ -0,0 +1,98 @@ +/* + * 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.cluster; + +import com.cloud.host.Host; +import com.cloud.offering.ServiceOffering; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.component.AdapterBase; +import com.cloud.vm.VirtualMachine; + +import javax.naming.ConfigurationException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsImbalanceThreshold; +import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsMetric; + +public class Condensed extends AdapterBase implements ClusterDrsAlgorithm { + + @Override + public String getName() { + return "condensed"; + } + + @Override + public boolean needsDrs(long clusterId, List cpuList, List memoryList) throws ConfigurationException { + Double cpuImbalance = getClusterImbalance(cpuList); + Double memoryImbalance = getClusterImbalance(memoryList); + double threshold = getThreshold(clusterId); + String metric = ClusterDrsMetric.valueIn(clusterId); + switch (metric) { + case "cpu": + return cpuImbalance < threshold; + case "memory": + return memoryImbalance < threshold; + default: + throw new ConfigurationException( + String.format("Invalid metric: %s for cluster: %d", metric, clusterId)); + } + } + + private double getThreshold(long clusterId) throws ConfigurationException { + return ClusterDrsImbalanceThreshold.valueIn(clusterId); + } + + @Override + public Ternary getMetrics(long clusterId, VirtualMachine vm, + ServiceOffering serviceOffering, Host destHost, + Map hostCpuUsedMap, Map hostMemoryUsedMap, + Boolean requiresStorageMotion) { + Double preCpuImbalance = getClusterImbalance(new ArrayList<>(hostCpuUsedMap.values())); + Double preMemoryImbalance = getClusterImbalance(new ArrayList<>(hostMemoryUsedMap.values())); + + Pair imbalancePair = getImbalancePostMigration(serviceOffering, vm, destHost, hostCpuUsedMap, + hostMemoryUsedMap); + Double postCpuImbalance = imbalancePair.first(); + Double postMemoryImbalance = imbalancePair.second(); + + // This needs more research to determine the cost and benefit of a migration + // TODO: Cost should be a factor of the VM size and the host capacity + // TODO: Benefit should be a factor of the VM size and the host capacity and the number of VMs on the host + double cost = 0; + double benefit = 1; + + String metric = ClusterDrsMetric.valueIn(clusterId); + double improvement; + switch (metric) { + case "cpu": + improvement = postCpuImbalance - preCpuImbalance; + break; + case "memory": + improvement = postMemoryImbalance - preMemoryImbalance; + break; + default: + improvement = postCpuImbalance + postMemoryImbalance - preCpuImbalance - preMemoryImbalance; + } + return new Ternary<>(improvement, cost, benefit); + } +} diff --git a/plugins/drs/cluster/condensed/src/main/resources/META-INF/cloudstack/condensed/module.properties b/plugins/drs/cluster/condensed/src/main/resources/META-INF/cloudstack/condensed/module.properties new file mode 100644 index 00000000000..0581736b17b --- /dev/null +++ b/plugins/drs/cluster/condensed/src/main/resources/META-INF/cloudstack/condensed/module.properties @@ -0,0 +1,18 @@ +# 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. +name=condensed +parent=cluster diff --git a/plugins/drs/cluster/condensed/src/main/resources/META-INF/cloudstack/condensed/spring-condensed-context.xml b/plugins/drs/cluster/condensed/src/main/resources/META-INF/cloudstack/condensed/spring-condensed-context.xml new file mode 100644 index 00000000000..dffa7d85da7 --- /dev/null +++ b/plugins/drs/cluster/condensed/src/main/resources/META-INF/cloudstack/condensed/spring-condensed-context.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/plugins/drs/cluster/condensed/src/test/java/org/apache/cloudstack/cluster/CondensedTest.java b/plugins/drs/cluster/condensed/src/test/java/org/apache/cloudstack/cluster/CondensedTest.java new file mode 100644 index 00000000000..d8cf581768a --- /dev/null +++ b/plugins/drs/cluster/condensed/src/test/java/org/apache/cloudstack/cluster/CondensedTest.java @@ -0,0 +1,221 @@ +/* + * 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.cluster; + +import com.cloud.host.Host; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.utils.Ternary; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.naming.ConfigurationException; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsImbalanceThreshold; +import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsMetric; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class CondensedTest { + + @InjectMocks + Condensed condensed; + + VirtualMachine vm1, vm2, vm3; + + Host destHost; + + ServiceOfferingVO serviceOffering; + + long clusterId = 1L; + + Map> hostVmMap; + + List cpuList, memoryList; + + Map hostCpuUsedMap, hostMemoryUsedMap; + + + private AutoCloseable closeable; + + @Before + public void setUp() throws NoSuchFieldException, IllegalAccessException { + closeable = MockitoAnnotations.openMocks(this); + + vm1 = Mockito.mock(VirtualMachine.class); + vm2 = Mockito.mock(VirtualMachine.class); + vm3 = Mockito.mock(VirtualMachine.class); // vm to migrate + + destHost = Mockito.mock(Host.class); + hostVmMap = new HashMap<>(); + hostVmMap.put(1L, Collections.singletonList(vm1)); + hostVmMap.put(2L, Arrays.asList(vm2, vm3)); + + serviceOffering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(vm3.getHostId()).thenReturn(2L); + + Mockito.when(destHost.getId()).thenReturn(1L); + + Mockito.when(serviceOffering.getCpu()).thenReturn(1); + Mockito.when(serviceOffering.getSpeed()).thenReturn(1000); + Mockito.when(serviceOffering.getRamSize()).thenReturn(512); + + overrideDefaultConfigValue(ClusterDrsImbalanceThreshold, "_defaultValue", "0.5"); + + cpuList = Arrays.asList(1L, 2L); + memoryList = Arrays.asList(512L, 2048L); + + hostCpuUsedMap = new HashMap<>(); + hostCpuUsedMap.put(1L, 1000L); + hostCpuUsedMap.put(2L, 2000L); + + hostMemoryUsedMap = new HashMap<>(); + hostMemoryUsedMap.put(1L, 512L * 1024L * 1024L); + hostMemoryUsedMap.put(2L, 2048L * 1024L * 1024L); + } + + private void overrideDefaultConfigValue(final ConfigKey configKey, + final String name, + final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = ConfigKey.class.getDeclaredField(name); + f.setAccessible(true); + f.set(configKey, o); + } + + @After + public void tearDown() throws Exception { + closeable.close(); + } + + /** + *

needsDrs tests + *

Scenarios to test for needsDrs + *

1. cluster with cpu metric + *

2. cluster with memory metric + *

3. cluster with "unknown" metric + *

+ *

CPU imbalance = 0.333 + *

Memory imbalance = 0.6 + */ + + /* + 1. cluster with cpu metric + 0.3333 < 0.5 -> True + */ + @Test + public void needsDrsWithCpu() throws ConfigurationException, NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "cpu"); + assertTrue(condensed.needsDrs(clusterId, cpuList, memoryList)); + } + + /* + 2. cluster with memory metric + 0.6 < 0.5 -> False + */ + @Test + public void needsDrsWithMemory() throws ConfigurationException, NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "memory"); + assertFalse(condensed.needsDrs(clusterId, cpuList, memoryList)); + } + + /* 3. cluster with "unknown" metric */ + @Test + public void needsDrsWithUnknown() throws ConfigurationException, NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "unknown"); + assertThrows(ConfigurationException.class, () -> condensed.needsDrs(clusterId, cpuList, memoryList)); + } + + /** + * getMetrics tests + *

Scenarios to test for getMetrics + *

1. cluster with cpu metric + *

2. cluster with memory metric + *

3. cluster with default metric + *

+ *

Pre + *

CPU imbalance = 0.333333 + *

Memory imbalance = 0.6 + *

+ *

Post + *

CPU imbalance = 0.3333 + *

Memory imbalance = 0.2 + *

+ *

Cost 512.0 + *

Benefit (0.2-0.6) * 8192 = -3276.8 + */ + + /* + 1. cluster with cpu metric + improvement = 0.3333 - 0.3333 = 0.0 + */ + @Test + public void getMetricsWithCpu() throws NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "cpu"); + Ternary result = condensed.getMetrics(clusterId, vm3, serviceOffering, destHost, + hostCpuUsedMap, hostMemoryUsedMap, false); + assertEquals(0.0, result.first(), 0.0); + assertEquals(0, result.second(), 0.0); + assertEquals(1, result.third(), 0.0); + } + + /* + 2. cluster with memory metric + improvement = 0.2 - 0.6 = -0.4 + */ + @Test + public void getMetricsWithMemory() throws NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "memory"); + Ternary result = condensed.getMetrics(clusterId, vm3, serviceOffering, destHost, + hostCpuUsedMap, hostMemoryUsedMap, false); + assertEquals(-0.4, result.first(), 0.01); + assertEquals(0, result.second(), 0.0); + assertEquals(1, result.third(), 0.0); + } + + /* + 3. cluster with default metric + improvement = 0.3333 + 0.2 - 0.3333 - 0.6 = -0.4 + */ + @Test + public void getMetricsWithDefault() throws NoSuchFieldException, IllegalAccessException { + overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "both"); + Ternary result = condensed.getMetrics(clusterId, vm3, serviceOffering, destHost, + hostCpuUsedMap, hostMemoryUsedMap, false); + assertEquals(-0.4, result.first(), 0.0001); + assertEquals(0, result.second(), 0.0); + assertEquals(1, result.third(), 0.0); + } +} diff --git a/plugins/pom.xml b/plugins/pom.xml index d0661c01a2c..af131f08669 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -73,6 +73,9 @@ deployment-planners/user-concentrated-pod deployment-planners/user-dispersing + drs/cluster/balanced + drs/cluster/condensed + event-bus/inmemory event-bus/kafka event-bus/rabbitmq diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index d30e8b82920..ddfd0671820 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -1864,6 +1864,10 @@ public class ApiDBUtils { return s_userVmJoinDao.newUserVmView(userVms); } + public static List newUserVmView(VirtualMachine... vms) { + return s_userVmJoinDao.newUserVmView(vms); + } + public static SecurityGroupResponse newSecurityGroupResponse(SecurityGroupJoinVO vsg, Account caller) { return s_securityGroupJoinDao.newSecurityGroupResponse(vsg, caller); } diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDao.java index 652f51bcb3d..6356addcb4f 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDao.java @@ -19,6 +19,7 @@ package com.cloud.api.query.dao; import java.util.List; import java.util.Set; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.api.ApiConstants.VMDetails; import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.response.UserVmResponse; @@ -37,6 +38,8 @@ public interface UserVmJoinDao extends GenericDao { List newUserVmView(UserVm... userVms); + List newUserVmView(VirtualMachine... vms); + List searchByIds(Long... ids); List listActiveByIsoId(Long isoId); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index b97ae779abc..2914df28ce2 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -27,6 +27,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -619,4 +620,18 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation newUserVmView(VirtualMachine... vms) { + + Hashtable userVmDataHash = new Hashtable<>(); + for (VirtualMachine vm : vms) { + if (!userVmDataHash.containsKey(vm.getId())) { + userVmDataHash.put(vm.getId(), vm); + } + } + + Set vmIdSet = userVmDataHash.keySet(); + return searchByIds(vmIdSet.toArray(new Long[vmIdSet.size()])); + } + } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 18cebff87fe..acf57a788a0 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -89,6 +89,7 @@ import org.apache.cloudstack.api.command.admin.zone.CreateZoneCmd; import org.apache.cloudstack.api.command.admin.zone.DeleteZoneCmd; import org.apache.cloudstack.api.command.admin.zone.UpdateZoneCmd; import org.apache.cloudstack.api.command.user.network.ListNetworkOfferingsCmd; +import org.apache.cloudstack.cluster.ClusterDrsService; import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.config.Configuration; import org.apache.cloudstack.context.CallContext; @@ -571,6 +572,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati weightBasedParametersForValidation.add(Config.AgentLoadThreshold.key()); weightBasedParametersForValidation.add(Config.VmUserDispersionWeight.key()); weightBasedParametersForValidation.add(CapacityManager.SecondaryStorageCapacityThreshold.key()); + weightBasedParametersForValidation.add(ClusterDrsService.ClusterDrsImbalanceThreshold.key()); } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 02d958a1518..50824d11d58 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -1330,7 +1330,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe return new Pair, Integer>(result.first(), result.second()); } - protected Pair> filterUefiHostsForMigration(List allHosts, List filteredHosts, VMInstanceVO vm) { + protected Pair> filterUefiHostsForMigration(List allHosts, List filteredHosts, VirtualMachine vm) { UserVmDetailVO userVmDetailVO = _UserVmDetailsDao.findDetail(vm.getId(), ApiConstants.BootType.UEFI.toString()); if (userVmDetailVO != null && (ApiConstants.BootMode.LEGACY.toString().equalsIgnoreCase(userVmDetailVO.getValue()) || @@ -1350,9 +1350,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe return new Pair<>(true, filteredHosts); } - @Override - public Ternary, Integer>, List, Map> listHostsForMigrationOfVM(final Long vmId, final Long startIndex, final Long pageSize, - final String keyword) { + private void validateVmForHostMigration(VirtualMachine vm) { final Account caller = getCaller(); if (!_accountMgr.isRootAdmin(caller.getId())) { if (s_logger.isDebugEnabled()) { @@ -1361,10 +1359,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe throw new PermissionDeniedException("No permission to migrate VM, Only Root Admin can migrate a VM!"); } - final VMInstanceVO vm = _vmInstanceDao.findById(vmId); if (vm == null) { - final InvalidParameterValueException ex = new InvalidParameterValueException("Unable to find the VM with given id"); - throw ex; + throw new InvalidParameterValueException("Unable to find the VM with given id"); } if (vm.getState() != State.Running) { @@ -1376,13 +1372,6 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe throw ex; } - if (_serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString()) != null) { - s_logger.info(" Live Migration of GPU enabled VM : " + vm.getInstanceName() + " is not supported"); - // Return empty list. - return new Ternary<>(new Pair<>(new ArrayList(), new Integer(0)), - new ArrayList<>(), new HashMap<>()); - } - if (!LIVE_MIGRATION_SUPPORTING_HYPERVISORS.contains(vm.getHypervisorType())) { if (s_logger.isDebugEnabled()) { s_logger.debug(vm + " is not XenServer/VMware/KVM/Ovm/Hyperv/Ovm3, cannot migrate this VM."); @@ -1393,6 +1382,27 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe if (VirtualMachine.Type.User.equals(vm.getType()) && HypervisorType.LXC.equals(vm.getHypervisorType())) { throw new InvalidParameterValueException("Unsupported Hypervisor Type for User VM migration, we support XenServer/VMware/KVM/Ovm/Hyperv/Ovm3 only"); } + } + + @Override + public Ternary, Integer>, List, Map> listHostsForMigrationOfVM(final Long vmId, final Long startIndex, final Long pageSize, + final String keyword) { + final VMInstanceVO vm = _vmInstanceDao.findById(vmId); + return listHostsForMigrationOfVM(vm, startIndex, pageSize, keyword, Collections.emptyList()); + } + + @Override + public Ternary, Integer>, List, Map> listHostsForMigrationOfVM(final VirtualMachine vm, final Long startIndex, final Long pageSize, + final String keyword, List vmList) { + + validateVmForHostMigration(vm); + + if (_serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString()) != null) { + s_logger.info(" Live Migration of GPU enabled VM : " + vm.getInstanceName() + " is not supported"); + // Return empty list. + return new Ternary<>(new Pair<>(new ArrayList<>(), 0), + new ArrayList<>(), new HashMap<>()); + } final long srcHostId = vm.getHostId(); final Host srcHost = _hostDao.findById(srcHostId); @@ -1531,7 +1541,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe if (vmGroupCount > 0) { for (final AffinityGroupProcessor processor : _affinityProcessors) { - processor.process(vmProfile, plan, excludes); + processor.process(vmProfile, plan, excludes, vmList); } } diff --git a/server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java b/server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java new file mode 100644 index 00000000000..3ebb97ae4f0 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java @@ -0,0 +1,849 @@ +/* + * 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.cluster; + +import com.cloud.api.ApiGsonHelper; +import com.cloud.api.query.dao.HostJoinDao; +import com.cloud.api.query.vo.HostJoinVO; +import com.cloud.dc.ClusterVO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.domain.Domain; +import com.cloud.event.ActionEventUtils; +import com.cloud.event.EventTypes; +import com.cloud.event.EventVO; +import com.cloud.event.dao.EventDao; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.offering.ServiceOffering; +import com.cloud.org.Cluster; +import com.cloud.server.ManagementServer; +import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.user.Account; +import com.cloud.user.User; +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.component.ComponentContext; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.db.GlobalLock; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.command.admin.cluster.ExecuteClusterDrsPlanCmd; +import org.apache.cloudstack.api.command.admin.cluster.GenerateClusterDrsPlanCmd; +import org.apache.cloudstack.api.command.admin.cluster.ListClusterDrsPlanCmd; +import org.apache.cloudstack.api.command.admin.vm.MigrateVMCmd; +import org.apache.cloudstack.api.response.ClusterDrsPlanMigrationResponse; +import org.apache.cloudstack.api.response.ClusterDrsPlanResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.cluster.dao.ClusterDrsPlanDao; +import org.apache.cloudstack.cluster.dao.ClusterDrsPlanMigrationDao; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher; +import org.apache.cloudstack.framework.jobs.AsyncJobManager; +import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; +import org.apache.cloudstack.jobs.JobInfo; +import org.apache.cloudstack.managed.context.ManagedContextTimerTask; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.time.DateUtils; +import org.apache.log4j.Logger; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.stream.Collectors; + +import static com.cloud.org.Grouping.AllocationState.Disabled; + +public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsService, PluggableService { + + private static final Logger logger = Logger.getLogger(ClusterDrsServiceImpl.class); + + private static final String CLUSTER_LOCK_STR = "drs.plan.cluster.%s"; + + AsyncJobDispatcher asyncJobDispatcher; + + @Inject + AsyncJobManager asyncJobManager; + + @Inject + ClusterDao clusterDao; + + @Inject + HostDao hostDao; + + @Inject + EventDao eventDao; + + @Inject + HostJoinDao hostJoinDao; + + @Inject + VMInstanceDao vmInstanceDao; + + @Inject + ClusterDrsPlanDao drsPlanDao; + + @Inject + ClusterDrsPlanMigrationDao drsPlanMigrationDao; + + @Inject + ServiceOfferingDao serviceOfferingDao; + + @Inject + ManagementServer managementServer; + + List drsAlgorithms = new ArrayList<>(); + + Map drsAlgorithmMap = new HashMap<>(); + + public AsyncJobDispatcher getAsyncJobDispatcher() { + return asyncJobDispatcher; + } + + public void setAsyncJobDispatcher(final AsyncJobDispatcher dispatcher) { + asyncJobDispatcher = dispatcher; + } + + public void setDrsAlgorithms(final List drsAlgorithms) { + this.drsAlgorithms = drsAlgorithms; + } + + @Override + public boolean start() { + drsAlgorithmMap.clear(); + for (final ClusterDrsAlgorithm algorithm : drsAlgorithms) { + drsAlgorithmMap.put(algorithm.getName(), algorithm); + } + + final TimerTask schedulerPollTask = new ManagedContextTimerTask() { + @Override + protected void runInContext() { + try { + poll(new Date()); + } catch (final Exception e) { + logger.error("Error while running DRS", e); + } + } + }; + Timer vmSchedulerTimer = new Timer("VMSchedulerPollTask"); + vmSchedulerTimer.schedule(schedulerPollTask, 5000L, 60 * 1000L); + return true; + } + + @Override + public void poll(Date timestamp) { + Date currentTimestamp = DateUtils.round(timestamp, Calendar.MINUTE); + String displayTime = DateUtil.displayDateInTimezone(DateUtil.GMT_TIMEZONE, currentTimestamp); + logger.debug(String.format("ClusterDRS.poll is being called at %s", displayTime)); + + GlobalLock lock = GlobalLock.getInternLock("clusterDRS.poll"); + try { + if (lock.lock(30)) { + try { + updateOldPlanMigrations(); + // Executing processPlans() twice to update the migration status of plans which + // are completed and + // if required generate new plans. + processPlans(); + generateDrsPlanForAllClusters(); + processPlans(); + } finally { + lock.unlock(); + } + } + } finally { + lock.releaseRef(); + } + GlobalLock cleanupLock = GlobalLock.getInternLock("clusterDRS.cleanup"); + try { + if (cleanupLock.lock(30)) { + try { + cleanUpOldDrsPlans(); + } finally { + cleanupLock.unlock(); + } + } + } finally { + cleanupLock.releaseRef(); + } + } + + /** + * Fetches the plans which are in progress and updates their migration status. + */ + void updateOldPlanMigrations() { + List plans = drsPlanDao.listByStatus(ClusterDrsPlan.Status.IN_PROGRESS); + for (ClusterDrsPlanVO plan : plans) { + try { + updateDrsPlanMigrations(plan); + } catch (Exception e) { + logger.error(String.format("Unable to update DRS plan details [id=%d]", plan.getId()), e); + } + } + } + + /** + * Updates the job status of the plan details for the given plan. + * + * @param plan + * the plan to update + */ + void updateDrsPlanMigrations(ClusterDrsPlanVO plan) { + List migrations = drsPlanMigrationDao.listPlanMigrationsInProgress(plan.getId()); + if (migrations == null || migrations.isEmpty()) { + plan.setStatus(ClusterDrsPlan.Status.COMPLETED); + drsPlanDao.update(plan.getId(), plan); + ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, EventVO.LEVEL_INFO, + EventTypes.EVENT_CLUSTER_DRS, true, + String.format("DRS execution task completed for cluster [id=%s]", plan.getClusterId()), + plan.getClusterId(), ApiCommandResourceType.Cluster.toString(), plan.getEventId()); + return; + } + + for (ClusterDrsPlanMigrationVO migration : migrations) { + try { + AsyncJobVO job = asyncJobManager.getAsyncJob(migration.getJobId()); + if (job == null) { + logger.warn(String.format("Unable to find async job [id=%d] for DRS plan migration [id=%d]", + migration.getJobId(), migration.getId())); + migration.setStatus(JobInfo.Status.FAILED); + drsPlanMigrationDao.update(migration.getId(), migration); + continue; + } + if (job.getStatus() != JobInfo.Status.IN_PROGRESS) { + migration.setStatus(job.getStatus()); + drsPlanMigrationDao.update(migration.getId(), migration); + } + } catch (Exception e) { + logger.error(String.format("Unable to update DRS plan migration [id=%d]", migration.getId()), e); + } + } + } + + /** + * Generates DRS for all clusters that meet the criteria for automated DRS. + */ + void generateDrsPlanForAllClusters() { + List clusterList = clusterDao.listAll(); + + for (ClusterVO cluster : clusterList) { + if (cluster.getAllocationState() == Disabled || ClusterDrsEnabled.valueIn( + cluster.getId()).equals(Boolean.FALSE)) { + continue; + } + + ClusterDrsPlanVO lastPlan = drsPlanDao.listLatestPlanForClusterId(cluster.getId()); + + // If the last plan is ready or in progress or was executed within the last interval, skip this cluster. + // This is to avoid generating plans for clusters which are already being processed and to avoid + // generating plans for clusters which have been processed recently.This doesn't consider the type + // (manual or automated) of the last plan. + if (lastPlan != null && (lastPlan.getStatus() == ClusterDrsPlan.Status.READY || + lastPlan.getStatus() == ClusterDrsPlan.Status.IN_PROGRESS || + (lastPlan.getStatus() == ClusterDrsPlan.Status.COMPLETED && + lastPlan.getCreated().compareTo(DateUtils.addMinutes(new Date(), -1 * ClusterDrsInterval.valueIn(cluster.getId()))) > 0) + )) { + continue; + } + + long eventId = ActionEventUtils.onStartedActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, + EventTypes.EVENT_CLUSTER_DRS, + String.format("Generating DRS plan for cluster %s", cluster.getUuid()), cluster.getId(), + ApiCommandResourceType.Cluster.toString(), true, 0); + GlobalLock clusterLock = GlobalLock.getInternLock(String.format(CLUSTER_LOCK_STR, cluster.getId())); + try { + if (clusterLock.lock(30)) { + try { + List> plan = getDrsPlan(cluster, + ClusterDrsMaxMigrations.valueIn(cluster.getId())); + savePlan(cluster.getId(), plan, eventId, ClusterDrsPlan.Type.AUTOMATED, + ClusterDrsPlan.Status.READY); + logger.info(String.format("Generated DRS plan for cluster %s [id=%s]", cluster.getName(), + cluster.getUuid())); + } catch (Exception e) { + logger.error( + String.format("Unable to generate DRS plans for cluster %s [id=%s]", cluster.getName(), + cluster.getUuid()), + e); + } finally { + clusterLock.unlock(); + } + } + } finally { + clusterLock.releaseRef(); + } + } + } + + /** + * Generate DRS plan for the given cluster with the specified iteration percentage. + * + * @param cluster + * The cluster to generate DRS for. + * @param maxIterations + * The percentage of VMs to consider for migration + * during each iteration. Value between 0 and 1. + * + * @return List of Ternary object containing VM to be migrated, source host and + * destination host. + * + * @throws ConfigurationException + * If there is an error in the DRS configuration. + */ + List> getDrsPlan(Cluster cluster, + int maxIterations) throws ConfigurationException { + List> migrationPlan = new ArrayList<>(); + + if (cluster.getAllocationState() == Disabled || maxIterations <= 0) { + return Collections.emptyList(); + } + ClusterDrsAlgorithm algorithm = getDrsAlgorithm(ClusterDrsAlgorithm.valueIn(cluster.getId())); + List hostList = hostDao.findByClusterId(cluster.getId()); + List vmList = new ArrayList<>(vmInstanceDao.listByClusterId(cluster.getId())); + + int iteration = 0; + + Map hostMap = hostList.stream().collect(Collectors.toMap(HostVO::getId, host -> host)); + + Map> hostVmMap = getHostVmMap(hostList, vmList); + Map> originalHostIdVmIdMap = new HashMap<>(); + for (HostVO host : hostList) { + originalHostIdVmIdMap.put(host.getId(), new ArrayList<>()); + for (VirtualMachine vm : hostVmMap.get(host.getId())) { + originalHostIdVmIdMap.get(host.getId()).add(vm.getId()); + } + } + + List hostJoinList = hostJoinDao.searchByIds( + hostList.stream().map(HostVO::getId).toArray(Long[]::new)); + + Map hostCpuMap = hostJoinList.stream().collect(Collectors.toMap(HostJoinVO::getId, + hostJoin -> hostJoin.getCpuUsedCapacity() + hostJoin.getCpuReservedCapacity())); + Map hostMemoryMap = hostJoinList.stream().collect(Collectors.toMap(HostJoinVO::getId, + hostJoin -> hostJoin.getMemUsedCapacity() + hostJoin.getMemReservedCapacity())); + + Map vmIdServiceOfferingMap = new HashMap<>(); + + for (VirtualMachine vm : vmList) { + vmIdServiceOfferingMap.put(vm.getId(), + serviceOfferingDao.findByIdIncludingRemoved(vm.getId(), vm.getServiceOfferingId())); + } + + while (iteration < maxIterations && algorithm.needsDrs(cluster.getId(), new ArrayList<>(hostCpuMap.values()), + new ArrayList<>(hostMemoryMap.values()))) { + Pair bestMigration = getBestMigration(cluster, algorithm, vmList, + vmIdServiceOfferingMap, hostCpuMap, hostMemoryMap); + VirtualMachine vm = bestMigration.first(); + Host destHost = bestMigration.second(); + if (destHost == null || vm == null || originalHostIdVmIdMap.get(destHost.getId()).contains(vm.getId())) { + logger.debug("VM migrating to it's original host or no host found for migration"); + break; + } + + ServiceOffering serviceOffering = vmIdServiceOfferingMap.get(vm.getId()); + migrationPlan.add(new Ternary<>(vm, hostMap.get(vm.getHostId()), hostMap.get(destHost.getId()))); + + hostVmMap.get(vm.getHostId()).remove(vm); + hostVmMap.get(destHost.getId()).add(vm); + hostVmMap.get(vm.getHostId()).remove(vm); + hostVmMap.get(destHost.getId()).add(vm); + + long vmCpu = (long) serviceOffering.getCpu() * serviceOffering.getSpeed(); + long vmMemory = serviceOffering.getRamSize() * 1024L * 1024L; + + hostCpuMap.put(vm.getHostId(), hostCpuMap.get(vm.getHostId()) - vmCpu); + hostCpuMap.put(destHost.getId(), hostCpuMap.get(destHost.getId()) + vmCpu); + hostMemoryMap.put(vm.getHostId(), hostMemoryMap.get(vm.getHostId()) - vmMemory); + hostMemoryMap.put(destHost.getId(), hostMemoryMap.get(destHost.getId()) + vmMemory); + vm.setHostId(destHost.getId()); + iteration++; + } + return migrationPlan; + } + + private ClusterDrsAlgorithm getDrsAlgorithm(String algoName) { + if (drsAlgorithmMap.containsKey(algoName)) { + return drsAlgorithmMap.get(algoName); + } + throw new CloudRuntimeException("Invalid algorithm configured!"); + } + + Map> getHostVmMap(List hostList, List vmList) { + Map> hostVmMap = new HashMap<>(); + for (HostVO host : hostList) { + hostVmMap.put(host.getId(), new ArrayList<>()); + } + for (VirtualMachine vm : vmList) { + hostVmMap.get(vm.getHostId()).add(vm); + } + return hostVmMap; + } + + /** + * Returns the best migration for a given cluster using the specified DRS + * algorithm. + * + * @param cluster + * the cluster to perform DRS on + * @param algorithm + * the DRS algorithm to use + * @param vmList + * the list of virtual machines to consider for + * migration + * @param vmIdServiceOfferingMap + * a map of virtual machine IDs to their + * corresponding service offerings + * @param hostCpuCapacityMap + * a map of host IDs to their corresponding CPU + * capacity + * @param hostMemoryCapacityMap + * a map of host IDs to their corresponding memory + * capacity + * + * @return a pair of the virtual machine and host that represent the best + * migration, or null if no migration is + * possible + */ + Pair getBestMigration(Cluster cluster, ClusterDrsAlgorithm algorithm, + List vmList, + Map vmIdServiceOfferingMap, + Map hostCpuCapacityMap, + Map hostMemoryCapacityMap) { + double improvement = 0; + Pair bestMigration = new Pair<>(null, null); + + for (VirtualMachine vm : vmList) { + if (vm.getType().isUsedBySystem() || vm.getState() != VirtualMachine.State.Running || + (MapUtils.isNotEmpty(vm.getDetails()) && + vm.getDetails().get(VmDetailConstants.SKIP_DRS).equalsIgnoreCase("true")) + ) { + continue; + } + Ternary, Integer>, List, Map> hostsForMigrationOfVM = managementServer + .listHostsForMigrationOfVM( + vm, 0L, 500L, null, vmList); + List compatibleDestinationHosts = hostsForMigrationOfVM.first().first(); + List suitableDestinationHosts = hostsForMigrationOfVM.second(); + + Map requiresStorageMotion = hostsForMigrationOfVM.third(); + + for (Host destHost : compatibleDestinationHosts) { + if (!suitableDestinationHosts.contains(destHost)) { + continue; + } + Ternary metrics = algorithm.getMetrics(cluster.getId(), vm, + vmIdServiceOfferingMap.get(vm.getId()), destHost, hostCpuCapacityMap, hostMemoryCapacityMap, + requiresStorageMotion.get(destHost)); + + Double currentImprovement = metrics.first(); + Double cost = metrics.second(); + Double benefit = metrics.third(); + if (benefit > cost && (currentImprovement > improvement)) { + bestMigration = new Pair<>(vm, destHost); + improvement = currentImprovement; + } + } + } + return bestMigration; + } + + + /** + * Saves a DRS plan for a given cluster and returns the saved plan along with the list of migrations to be executed. + * + * @param clusterId + * the ID of the cluster for which the DRS plan is being saved + * @param plan + * the list of virtual machine migrations to be executed as part of the DRS plan + * @param eventId + * the ID of the event that triggered the DRS plan + * @param type + * the type of the DRS plan + * + * @return a pair of the saved DRS plan and the list of migrations to be executed + */ + Pair> savePlan(Long clusterId, + List> plan, + Long eventId, ClusterDrsPlan.Type type, + ClusterDrsPlan.Status status) { + return Transaction.execute( + (TransactionCallback>>) txStatus -> { + ClusterDrsPlanVO drsPlan = drsPlanDao.persist( + new ClusterDrsPlanVO(clusterId, eventId, type, status)); + List planMigrations = new ArrayList<>(); + for (Ternary migration : plan) { + VirtualMachine vm = migration.first(); + Host srcHost = migration.second(); + Host destHost = migration.third(); + planMigrations.add(drsPlanMigrationDao.persist( + new ClusterDrsPlanMigrationVO(drsPlan.getId(), vm.getId(), srcHost.getId(), + destHost.getId()))); + } + return new Pair<>(drsPlan, planMigrations); + }); + } + + /** + * Processes all DRS plans that are in the READY status. + */ + void processPlans() { + List plans = drsPlanDao.listByStatus(ClusterDrsPlan.Status.READY); + for (ClusterDrsPlanVO plan : plans) { + try { + executeDrsPlan(plan); + } catch (Exception e) { + logger.error(String.format("Unable to execute DRS plan [id=%d]", plan.getId()), e); + } + } + } + + /** + * Executes the DRS plan by migrating virtual machines to their destination hosts. + * If there are no migrations to be executed, the plan is marked as completed. + * + * @param plan + * the DRS plan to be executed + */ + void executeDrsPlan(ClusterDrsPlanVO plan) { + List planMigrations = drsPlanMigrationDao.listPlanMigrationsToExecute(plan.getId()); + if (planMigrations == null || planMigrations.isEmpty()) { + plan.setStatus(ClusterDrsPlan.Status.COMPLETED); + drsPlanDao.update(plan.getId(), plan); + ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, EventVO.LEVEL_INFO, + EventTypes.EVENT_CLUSTER_DRS, true, + String.format("DRS execution task completed for cluster [id=%s]", plan.getClusterId()), + plan.getClusterId(), ApiCommandResourceType.Cluster.toString(), plan.getEventId()); + return; + } + + plan.setStatus(ClusterDrsPlan.Status.IN_PROGRESS); + drsPlanDao.update(plan.getId(), plan); + + for (ClusterDrsPlanMigrationVO migration : planMigrations) { + try { + VirtualMachine vm = vmInstanceDao.findById(migration.getVmId()); + Host host = hostDao.findById(migration.getDestHostId()); + if (vm == null || host == null) { + throw new CloudRuntimeException(String.format("vm %s or host %s is not found", migration.getVmId(), + migration.getDestHostId())); + } + + logger.debug( + String.format("Executing DRS plan %s for vm %s to host %s", plan.getId(), vm.getInstanceName(), + host.getName())); + long jobId = createMigrateVMAsyncJob(vm, host, plan.getEventId()); + AsyncJobVO job = asyncJobManager.getAsyncJob(jobId); + migration.setJobId(jobId); + migration.setStatus(job.getStatus()); + drsPlanMigrationDao.update(migration.getId(), migration); + } catch (Exception e) { + logger.warn(String.format("Unable to execute DRS plan %s due to %s", plan.getUuid(), e.getMessage())); + migration.setStatus(JobInfo.Status.FAILED); + drsPlanMigrationDao.update(migration.getId(), migration); + } + } + } + + /** + * Creates an asynchronous job to migrate a virtual machine to a specified host. + * + * @param vm + * the virtual machine to be migrated + * @param host + * the destination host for the virtual machine + * @param eventId + * the ID of the event that triggered the migration + * + * @return the ID of the created asynchronous job + */ + long createMigrateVMAsyncJob(VirtualMachine vm, Host host, long eventId) { + final Map params = new HashMap<>(); + params.put("ctxUserId", String.valueOf(User.UID_SYSTEM)); + params.put("ctxAccountId", String.valueOf(Account.ACCOUNT_ID_SYSTEM)); + params.put(ApiConstants.CTX_START_EVENT_ID, String.valueOf(eventId)); + params.put(ApiConstants.HOST_ID, String.valueOf(host.getId())); + params.put(ApiConstants.VIRTUAL_MACHINE_ID, String.valueOf(vm.getId())); + + final MigrateVMCmd cmd = new MigrateVMCmd(); + ComponentContext.inject(cmd); + + AsyncJobVO job = new AsyncJobVO("", User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, MigrateVMCmd.class.getName(), + ApiGsonHelper.getBuilder().create().toJson(params), vm.getId(), + ApiCommandResourceType.VirtualMachine.toString(), null); + job.setDispatcher(asyncJobDispatcher.getName()); + + return asyncJobManager.submitAsyncJob(job); + } + + /** + * Removes old DRS migrations records that have expired based on the configured interval. + */ + void cleanUpOldDrsPlans() { + Date date = DateUtils.addDays(new Date(), -1 * ClusterDrsPlanExpireInterval.value()); + int rowsRemoved = drsPlanDao.expungeBeforeDate(date); + logger.debug(String.format("Removed %d old drs migration plans", rowsRemoved)); + } + + @Override + public String getConfigComponentName() { + return ClusterDrsService.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ClusterDrsPlanExpireInterval, ClusterDrsEnabled, ClusterDrsInterval, + ClusterDrsMaxMigrations, ClusterDrsAlgorithm, ClusterDrsImbalanceThreshold, ClusterDrsMetric}; + } + + @Override + public List> getCommands() { + List> cmdList = new ArrayList<>(); + cmdList.add(ListClusterDrsPlanCmd.class); + cmdList.add(GenerateClusterDrsPlanCmd.class); + cmdList.add(ExecuteClusterDrsPlanCmd.class); + return cmdList; + } + + /** + * Generates a DRS plan for the given cluster and returns a list of migration responses. + * + * @param cmd + * the command containing the cluster ID and number of migrations for the DRS plan + * + * @return a list response of migration responses for the generated DRS plan + * + * @throws InvalidParameterValueException + * if the cluster is not found, is disabled, or is not a cloud stack managed cluster, or if the number of + * migrations is invalid + * @throws CloudRuntimeException + * if there is an error scheduling the DRS plan + */ + @Override + public ClusterDrsPlanResponse generateDrsPlan(GenerateClusterDrsPlanCmd cmd) { + Cluster cluster = clusterDao.findById(cmd.getId()); + if (cluster == null) { + throw new InvalidParameterValueException("Unable to find the cluster by id=" + cmd.getId()); + } + if (cluster.getAllocationState() == Disabled) { + throw new InvalidParameterValueException( + String.format("Unable to execute DRS on the cluster %s as it is disabled", cluster.getName())); + } + if (cmd.getMaxMigrations() <= 0) { + throw new InvalidParameterValueException( + String.format("Unable to execute DRS on the cluster %s as the number of migrations [%s] is invalid", + cluster.getName(), cmd.getMaxMigrations())); + } + + try { + List> plan = getDrsPlan(cluster, cmd.getMaxMigrations()); + long eventId = ActionEventUtils.onActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, + Domain.ROOT_DOMAIN, + EventTypes.EVENT_CLUSTER_DRS_GENERATE, + String.format("Generating DRS plan for cluster %s", cluster.getUuid()), cluster.getId(), + ApiCommandResourceType.Cluster.toString()); + List migrations; + ClusterDrsPlanVO drsPlan = new ClusterDrsPlanVO( + cluster.getId(), eventId, ClusterDrsPlan.Type.MANUAL, ClusterDrsPlan.Status.UNDER_REVIEW); + migrations = new ArrayList<>(); + for (Ternary migration : plan) { + VirtualMachine vm = migration.first(); + Host srcHost = migration.second(); + Host destHost = migration.third(); + migrations.add(new ClusterDrsPlanMigrationVO(0L, vm.getId(), srcHost.getId(), destHost.getId())); + } + + CallContext.current().setEventResourceType(ApiCommandResourceType.Cluster); + CallContext.current().setEventResourceId(cluster.getId()); + + String eventUuid = null; + EventVO event = eventDao.findById(drsPlan.getEventId()); + if (event != null) { + eventUuid = event.getUuid(); + } + + return new ClusterDrsPlanResponse( + cluster.getUuid(), drsPlan, eventUuid, getResponseObjectForMigrations(migrations)); + } catch (ConfigurationException e) { + throw new CloudRuntimeException("Unable to schedule DRS", e); + } + } + + /** + * Returns a list of ClusterDrsPlanMigrationResponse objects for the given list of ClusterDrsPlanMigrationVO + * objects. + * + * @param migrations + * the list of ClusterDrsPlanMigrationVO objects + * + * @return a list of ClusterDrsPlanMigrationResponse objects + */ + List getResponseObjectForMigrations(List migrations) { + if (migrations == null) { + return Collections.emptyList(); + } + List responses = new ArrayList<>(); + + for (ClusterDrsPlanMigrationVO migration : migrations) { + VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(migration.getVmId()); + HostVO srcHost = hostDao.findByIdIncludingRemoved(migration.getSrcHostId()); + HostVO destHost = hostDao.findByIdIncludingRemoved(migration.getDestHostId()); + responses.add(new ClusterDrsPlanMigrationResponse( + vm.getUuid(), vm.getInstanceName(), + srcHost.getUuid(), srcHost.getName(), + destHost.getUuid(), destHost.getName(), + migration.getJobId(), migration.getStatus())); + } + + return responses; + } + + @Override + public ClusterDrsPlanResponse executeDrsPlan(ExecuteClusterDrsPlanCmd cmd) { + + Map vmToHostMap = cmd.getVmToHostMap(); + Long clusterId = cmd.getId(); + + if (vmToHostMap.isEmpty()) { + throw new InvalidParameterValueException("migrateto can not be empty."); + } + + Cluster cluster = clusterDao.findById(clusterId); + + if (cluster == null) { + throw new InvalidParameterValueException("cluster not found"); + } + + return executeDrsPlan(cluster, vmToHostMap); + + } + + private ClusterDrsPlanResponse executeDrsPlan(Cluster cluster, Map vmToHostMap) { + // To ensure that no other plan is generated for this cluster, we take a lock + GlobalLock clusterLock = GlobalLock.getInternLock(String.format(CLUSTER_LOCK_STR, cluster.getId())); + ClusterDrsPlanVO drsPlan = null; + List migrations = null; + try { + if (clusterLock.lock(5)) { + try { + List readyPlans = drsPlanDao.listByClusterIdAndStatus(cluster.getId(), + ClusterDrsPlan.Status.READY); + if (readyPlans != null && !readyPlans.isEmpty()) { + throw new InvalidParameterValueException( + String.format( + "Unable to execute DRS plan as there is already a plan [id=%s] in READY state", + readyPlans.get(0).getUuid())); + } + List inProgressPlans = drsPlanDao.listByClusterIdAndStatus(cluster.getId(), + ClusterDrsPlan.Status.IN_PROGRESS); + + if (inProgressPlans != null && !inProgressPlans.isEmpty()) { + throw new InvalidParameterValueException( + String.format("Unable to execute DRS plan as there is already a plan [id=%s] in In " + + "Progress", + inProgressPlans.get(0).getUuid())); + } + + List> plan = new ArrayList<>(); + for (Map.Entry entry : vmToHostMap.entrySet()) { + VirtualMachine vm = entry.getKey(); + Host destHost = entry.getValue(); + Host srcHost = hostDao.findById(vm.getHostId()); + plan.add(new Ternary<>(vm, srcHost, destHost)); + } + + Pair> pair = savePlan(cluster.getId(), plan, + CallContext.current().getStartEventId(), ClusterDrsPlan.Type.MANUAL, + ClusterDrsPlan.Status.READY); + drsPlan = pair.first(); + migrations = pair.second(); + + executeDrsPlan(drsPlan); + } finally { + clusterLock.unlock(); + } + } + } finally { + clusterLock.releaseRef(); + } + + String eventId = null; + if (drsPlan != null) { + EventVO event = eventDao.findById(drsPlan.getEventId()); + eventId = event.getUuid(); + } + + return new ClusterDrsPlanResponse( + cluster.getUuid(), drsPlan, eventId, getResponseObjectForMigrations(migrations)); + } + + @Override + public ListResponse listDrsPlan(ListClusterDrsPlanCmd cmd) { + Long clusterId = cmd.getClusterId(); + Long planId = cmd.getId(); + + if (planId != null && clusterId != null) { + throw new InvalidParameterValueException("Only one of clusterId or planId can be specified"); + } + + ClusterVO cluster = clusterDao.findById(clusterId); + if (clusterId != null && cluster == null) { + throw new InvalidParameterValueException("Unable to find the cluster by id=" + clusterId); + } + + Pair, Integer> result = drsPlanDao.searchAndCount(clusterId, planId, cmd.getStartIndex(), + cmd.getPageSizeVal()); + + ListResponse response = new ListResponse<>(); + List responseList = new ArrayList<>(); + + for (ClusterDrsPlan plan : result.first()) { + if (cluster == null || plan.getClusterId() != cluster.getId()) { + cluster = clusterDao.findById(plan.getClusterId()); + } + List migrations = drsPlanMigrationDao.listByPlanId(plan.getId()); + EventVO event = eventDao.findById(plan.getEventId()); + + responseList.add(new ClusterDrsPlanResponse( + cluster.getUuid(), plan, event.getUuid(), getResponseObjectForMigrations(migrations))); + } + + response.setResponses(responseList, result.second()); + return response; + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index ee676aabbfa..b976ed6a329 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -342,6 +342,10 @@ value="#{affinityProcessorsRegistry.registered}" /> + + + + diff --git a/server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java new file mode 100644 index 00000000000..e82b39a47ec --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java @@ -0,0 +1,440 @@ +/* + * 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.cluster; + +import com.cloud.api.query.dao.HostJoinDao; +import com.cloud.api.query.vo.HostJoinVO; +import com.cloud.dc.ClusterVO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.event.ActionEventUtils; +import com.cloud.event.EventVO; +import com.cloud.event.dao.EventDao; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.offering.ServiceOffering; +import com.cloud.org.Cluster; +import com.cloud.org.Grouping; +import com.cloud.server.ManagementServer; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.db.GlobalLock; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.api.command.admin.cluster.GenerateClusterDrsPlanCmd; +import org.apache.cloudstack.api.response.ClusterDrsPlanMigrationResponse; +import org.apache.cloudstack.api.response.ClusterDrsPlanResponse; +import org.apache.cloudstack.cluster.dao.ClusterDrsPlanDao; +import org.apache.cloudstack.cluster.dao.ClusterDrsPlanMigrationDao; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import javax.naming.ConfigurationException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@RunWith(MockitoJUnitRunner.class) +public class ClusterDrsServiceImplTest { + + @Mock + ClusterDrsAlgorithm condensedAlgorithm; + + @Mock + ManagementServer managementServer; + + @Mock + ClusterDrsAlgorithm balancedAlgorithm; + + @Mock + GenerateClusterDrsPlanCmd cmd; + + AutoCloseable closeable; + + @Mock + private ClusterDao clusterDao; + + @Mock + private ClusterDrsPlanDao drsPlanDao; + + @Mock + private ClusterDrsPlanMigrationDao drsPlanMigrationDao; + + @Mock + private EventDao eventDao; + + @Mock + private HostDao hostDao; + + @Mock + private HostJoinDao hostJoinDao; + + @Mock + private ServiceOfferingDao serviceOfferingDao; + + @Mock + private VMInstanceDao vmInstanceDao; + + @Spy + @InjectMocks + private ClusterDrsServiceImpl clusterDrsService = new ClusterDrsServiceImpl(); + + private MockedStatic globalLockMocked; + + @Before + public void setUp() throws NoSuchFieldException, IllegalAccessException { + closeable = MockitoAnnotations.openMocks(this); + + HashMap drsAlgorithmMap = new HashMap<>(); + drsAlgorithmMap.put("balanced", balancedAlgorithm); + drsAlgorithmMap.put("condensed", condensedAlgorithm); + + clusterDrsService.setDrsAlgorithms(List.of(new ClusterDrsAlgorithm[]{balancedAlgorithm, condensedAlgorithm})); + ReflectionTestUtils.setField(clusterDrsService, "drsAlgorithmMap", drsAlgorithmMap); + Field f = ConfigKey.class.getDeclaredField("_defaultValue"); + f.setAccessible(true); + f.set(clusterDrsService.ClusterDrsAlgorithm, "balanced"); + Mockito.when(cmd.getId()).thenReturn(1L); + + globalLockMocked = Mockito.mockStatic(GlobalLock.class); + GlobalLock lock = Mockito.mock(GlobalLock.class); + Mockito.when(GlobalLock.getInternLock("cluster.drs.1")).thenReturn(lock); + } + + @After + public void tearDown() throws Exception { + globalLockMocked.close(); + closeable.close(); + } + + @Test + public void testGetCommands() { + assertFalse(clusterDrsService.getCommands().isEmpty()); + } + + @Test + public void testGetDrsPlan() throws ConfigurationException { + ClusterVO cluster = Mockito.mock(ClusterVO.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled); + + HostVO host1 = Mockito.mock(HostVO.class); + Mockito.when(host1.getId()).thenReturn(1L); + + HostVO host2 = Mockito.mock(HostVO.class); + Mockito.when(host2.getId()).thenReturn(2L); + + VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class); + Mockito.when(vm1.getId()).thenReturn(1L); + Mockito.when(vm1.getHostId()).thenReturn(1L); + + VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class); + Mockito.when(vm2.getHostId()).thenReturn(2L); + + List hostList = new ArrayList<>(); + hostList.add(host1); + hostList.add(host2); + + HostJoinVO hostJoin1 = Mockito.mock(HostJoinVO.class); + Mockito.when(hostJoin1.getId()).thenReturn(1L); + Mockito.when(hostJoin1.getCpuUsedCapacity()).thenReturn(1000L); + Mockito.when(hostJoin1.getCpuReservedCapacity()).thenReturn(0L); + Mockito.when(hostJoin1.getMemUsedCapacity()).thenReturn(1024L); + Mockito.when(hostJoin1.getMemReservedCapacity()).thenReturn(512L); + + HostJoinVO hostJoin2 = Mockito.mock(HostJoinVO.class); + Mockito.when(hostJoin2.getId()).thenReturn(2L); + Mockito.when(hostJoin2.getCpuUsedCapacity()).thenReturn(1000L); + Mockito.when(hostJoin2.getCpuReservedCapacity()).thenReturn(0L); + Mockito.when(hostJoin2.getMemUsedCapacity()).thenReturn(1024L); + Mockito.when(hostJoin2.getMemReservedCapacity()).thenReturn(512L); + + List vmList = new ArrayList<>(); + vmList.add(vm1); + vmList.add(vm2); + + ServiceOfferingVO serviceOffering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(serviceOffering.getCpu()).thenReturn(1); + Mockito.when(serviceOffering.getRamSize()).thenReturn(1024); + Mockito.when(serviceOffering.getSpeed()).thenReturn(1000); + + Mockito.when(hostDao.findByClusterId(1L)).thenReturn(hostList); + Mockito.when(vmInstanceDao.listByClusterId(1L)).thenReturn(vmList); + Mockito.when(balancedAlgorithm.needsDrs(Mockito.anyLong(), Mockito.anyList(), Mockito.anyList())).thenReturn( + true, false); + Mockito.when( + clusterDrsService.getBestMigration(Mockito.any(Cluster.class), Mockito.any(ClusterDrsAlgorithm.class), + Mockito.anyList(), Mockito.anyMap(), Mockito.anyMap(), Mockito.anyMap())).thenReturn( + new Pair<>(vm1, host2)); + Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(Mockito.anyLong(), Mockito.anyLong())).thenReturn( + serviceOffering); + Mockito.when(hostJoinDao.searchByIds(host1.getId(), host2.getId())).thenReturn(List.of(hostJoin1, hostJoin2)); + + List> iterations = clusterDrsService.getDrsPlan(cluster, 5); + + Mockito.verify(hostDao, Mockito.times(1)).findByClusterId(1L); + Mockito.verify(vmInstanceDao, Mockito.times(1)).listByClusterId(1L); + Mockito.verify(balancedAlgorithm, Mockito.times(2)).needsDrs(Mockito.anyLong(), Mockito.anyList(), + Mockito.anyList()); + + assertEquals(1, iterations.size()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGenerateDrsPlanClusterNotFound() { + Mockito.when(clusterDao.findById(1L)).thenReturn(null); + clusterDrsService.generateDrsPlan(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGenerateDrsPlanClusterDisabled() { + ClusterVO cluster = Mockito.mock(ClusterVO.class); + Mockito.when(cluster.getName()).thenReturn("testCluster"); + Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Disabled); + + Mockito.when(clusterDao.findById(1L)).thenReturn(cluster); + + clusterDrsService.generateDrsPlan(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGenerateDrsPlanClusterNotCloudManaged() { + + ClusterVO cluster = Mockito.mock(ClusterVO.class); + Mockito.when(cluster.getName()).thenReturn("testCluster"); + Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled); + + Mockito.when(clusterDao.findById(1L)).thenReturn(cluster); + + clusterDrsService.generateDrsPlan(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGenerateDrsPlanInvalidIterations() { + ClusterVO cluster = Mockito.mock(ClusterVO.class); + Mockito.when(cluster.getName()).thenReturn("testCluster"); + Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled); + + Mockito.when(clusterDao.findById(1L)).thenReturn(cluster); + Mockito.when(cmd.getMaxMigrations()).thenReturn(0); + + clusterDrsService.generateDrsPlan(cmd); + } + + @Test(expected = CloudRuntimeException.class) + public void testGenerateDrsPlanConfigurationException() throws ConfigurationException { + ClusterVO cluster = Mockito.mock(ClusterVO.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled); + Mockito.when(clusterDao.findById(1L)).thenReturn(cluster); + Mockito.when(clusterDrsService.getDrsPlan(cluster, 5)).thenThrow(new ConfigurationException("test")); + Mockito.when(cmd.getMaxMigrations()).thenReturn(1); + + clusterDrsService.generateDrsPlan(cmd); + } + + @Test + public void testGenerateDrsPlan() throws ConfigurationException { + ClusterVO cluster = Mockito.mock(ClusterVO.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled); + + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + Mockito.when(vm.getId()).thenReturn(1L); + + Host srcHost = Mockito.mock(Host.class); + Mockito.when(srcHost.getId()).thenReturn(1L); + + Host destHost = Mockito.mock(Host.class); + Mockito.when(destHost.getId()).thenReturn(2L); + + Mockito.when(clusterDao.findById(1L)).thenReturn(cluster); + Mockito.when(eventDao.findById(Mockito.anyLong())).thenReturn(Mockito.mock(EventVO.class)); + Mockito.when(cmd.getMaxMigrations()).thenReturn(2); + Mockito.doReturn(List.of(new Ternary<>(vm, srcHost, + destHost))).when(clusterDrsService).getDrsPlan(Mockito.any(Cluster.class), Mockito.anyInt()); + + ClusterDrsPlanMigrationResponse migrationResponse = Mockito.mock(ClusterDrsPlanMigrationResponse.class); + + Mockito.when(clusterDrsService.getResponseObjectForMigrations(Mockito.anyList())).thenReturn( + List.of(migrationResponse)); + + try(MockedStatic ignored = Mockito.mockStatic(ActionEventUtils.class)) { + Mockito.when(ActionEventUtils.onActionEvent(Mockito.anyLong(), Mockito.anyLong(), + Mockito.anyLong(), + Mockito.anyString(), Mockito.anyString(), + Mockito.anyLong(), Mockito.anyString())).thenReturn(1L); + + ClusterDrsPlanResponse response = clusterDrsService.generateDrsPlan( + cmd); + + assertEquals(1L, response.getMigrationPlans().size()); + assertEquals(migrationResponse, response.getMigrationPlans().get(0)); + } + } + + @Test + public void testPoll() { + Mockito.doNothing().when(clusterDrsService).updateOldPlanMigrations(); + Mockito.doNothing().when(clusterDrsService).processPlans(); + Mockito.doNothing().when(clusterDrsService).generateDrsPlanForAllClusters(); + Mockito.doNothing().when(clusterDrsService).cleanUpOldDrsPlans(); + + GlobalLock lock = Mockito.mock(GlobalLock.class); + Mockito.when(lock.lock(Mockito.anyInt())).thenReturn(true); + + Mockito.when(GlobalLock.getInternLock(Mockito.anyString())).thenReturn(lock); + + clusterDrsService.poll(new Date()); + + Mockito.verify(clusterDrsService, Mockito.times(1)).updateOldPlanMigrations(); + Mockito.verify(clusterDrsService, Mockito.times(2)).processPlans(); + Mockito.verify(clusterDrsService, Mockito.times(1)).generateDrsPlanForAllClusters(); + } + + @Test + public void testUpdateOldPlanMigrations() { + ClusterDrsPlanVO drsPlan1 = Mockito.mock(ClusterDrsPlanVO.class); + ClusterDrsPlanVO drsPlan2 = Mockito.mock(ClusterDrsPlanVO.class); + + Mockito.when(drsPlanDao.listByStatus(ClusterDrsPlan.Status.IN_PROGRESS)).thenReturn( + List.of(drsPlan1, drsPlan2)); + + Mockito.doNothing().when(clusterDrsService).updateDrsPlanMigrations(drsPlan1); + Mockito.doNothing().when(clusterDrsService).updateDrsPlanMigrations(drsPlan2); + + clusterDrsService.updateOldPlanMigrations(); + + Mockito.verify(clusterDrsService, Mockito.times(2)).updateDrsPlanMigrations( + Mockito.any(ClusterDrsPlanVO.class)); + } + + @Test + public void testGetBestMigration() { + ClusterVO cluster = Mockito.mock(ClusterVO.class); + Mockito.when(cluster.getId()).thenReturn(1L); + + HostVO destHost = Mockito.mock(HostVO.class); + + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getId()).thenReturn(2L); + + VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class); + Mockito.when(vm1.getId()).thenReturn(1L); + Mockito.when(vm1.getType()).thenReturn(VirtualMachine.Type.User); + Mockito.when(vm1.getState()).thenReturn(VirtualMachine.State.Running); + Mockito.when(vm1.getDetails()).thenReturn(Collections.emptyMap()); + + VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class); + Mockito.when(vm2.getId()).thenReturn(2L); + Mockito.when(vm2.getType()).thenReturn(VirtualMachine.Type.User); + Mockito.when(vm2.getState()).thenReturn(VirtualMachine.State.Running); + Mockito.when(vm2.getDetails()).thenReturn(Collections.emptyMap()); + + List vmList = new ArrayList<>(); + vmList.add(vm1); + vmList.add(vm2); + + Map> hostVmMap = new HashMap<>(); + hostVmMap.put(host.getId(), new ArrayList<>()); + hostVmMap.get(host.getId()).add(vm1); + hostVmMap.get(host.getId()).add(vm2); + + Map vmIdServiceOfferingMap = new HashMap<>(); + + ServiceOffering serviceOffering = Mockito.mock(ServiceOffering.class); + for (VirtualMachine vm : vmList) { + vmIdServiceOfferingMap.put(vm.getId(), serviceOffering); + } + + Mockito.when(managementServer.listHostsForMigrationOfVM(vm1, 0L, 500L, null, vmList)).thenReturn( + new Ternary, Integer>, List, Map>( + new Pair<>(List.of(destHost), 1), List.of(destHost), Map.of(destHost, + false))); + Mockito.when(managementServer.listHostsForMigrationOfVM(vm2, 0L, 500L, null, vmList)).thenReturn( + new Ternary, Integer>, List, Map>( + new Pair<>(List.of(destHost), 1), List.of(destHost), Map.of(destHost, + false))); + Mockito.when(balancedAlgorithm.getMetrics(cluster.getId(), vm1, serviceOffering, destHost, new HashMap<>(), + new HashMap<>(), false)).thenReturn(new Ternary<>(1.0, 0.5, 1.5)); + + Mockito.when(balancedAlgorithm.getMetrics(cluster.getId(), vm2, serviceOffering, destHost, new HashMap<>(), + new HashMap<>(), false)).thenReturn(new Ternary<>(1.0, 2.5, 1.5)); + + Pair bestMigration = clusterDrsService.getBestMigration(cluster, balancedAlgorithm, + vmList, vmIdServiceOfferingMap, new HashMap<>(), new HashMap<>()); + + assertEquals(destHost, bestMigration.second()); + assertEquals(vm1, bestMigration.first()); + } + + @Test + public void testSavePlan() { + Mockito.when(drsPlanDao.persist(Mockito.any(ClusterDrsPlanVO.class))).thenReturn( + Mockito.mock(ClusterDrsPlanVO.class)); + Mockito.when(drsPlanMigrationDao.persist(Mockito.any(ClusterDrsPlanMigrationVO.class))).thenReturn( + Mockito.mock(ClusterDrsPlanMigrationVO.class)); + + clusterDrsService.savePlan(1L, + List.of(new Ternary<>(Mockito.mock(VirtualMachine.class), Mockito.mock(Host.class), + Mockito.mock(Host.class)), + new Ternary<>(Mockito.mock(VirtualMachine.class), Mockito.mock(Host.class), + Mockito.mock(Host.class))), 1L, ClusterDrsPlan.Type.AUTOMATED, + ClusterDrsPlan.Status.READY); + + Mockito.verify(drsPlanDao, Mockito.times(1)).persist(Mockito.any(ClusterDrsPlanVO.class)); + Mockito.verify(drsPlanMigrationDao, Mockito.times(2)).persist(Mockito.any(ClusterDrsPlanMigrationVO.class)); + } + + @Test + public void testProcessPlans() { + Mockito.when(drsPlanDao.listByStatus(ClusterDrsPlan.Status.READY)).thenReturn( + List.of(Mockito.mock(ClusterDrsPlanVO.class), Mockito.mock(ClusterDrsPlanVO.class))); + + Mockito.doNothing().when(clusterDrsService).executeDrsPlan(Mockito.any(ClusterDrsPlanVO.class)); + + clusterDrsService.processPlans(); + + Mockito.verify(clusterDrsService, Mockito.times(2)).executeDrsPlan(Mockito.any(ClusterDrsPlanVO.class)); + } +} diff --git a/test/integration/smoke/test_cluster_drs.py b/test/integration/smoke/test_cluster_drs.py new file mode 100644 index 00000000000..4db6654aa73 --- /dev/null +++ b/test/integration/smoke/test_cluster_drs.py @@ -0,0 +1,267 @@ +# 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. + +""" +Tests DRS on a cluster +""" + +import logging +import time + +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.base import (Cluster, Configurations, Host, Network, NetworkOffering, ServiceOffering, VirtualMachine, + Zone) +from marvin.lib.common import (get_domain, get_zone, get_template) +from marvin.lib.utils import wait_until +from marvin import jsonHelper +from nose.plugins.attrib import attr + + +class TestClusterDRS(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.testClient = super(TestClusterDRS, cls).getClsTestClient() + cls.apiclient = cls.testClient.getApiClient() + cls.services = cls.testClient.getParsedTestDataConfig() + + zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.zone = Zone(zone.__dict__) + cls.template = get_template(cls.apiclient, cls.zone.id) + cls._cleanup = [] + + cls.logger = logging.getLogger("TestClusterDRS") + cls.stream_handler = logging.StreamHandler() + cls.logger.setLevel(logging.DEBUG) + cls.logger.addHandler(cls.stream_handler) + + cls.skipTests = False + clusters = Cluster.list(cls.apiclient, zoneid=cls.zone.id, allocationstate='Enabled') + + if not clusters or not isinstance(clusters, list) or len(clusters) < 1: + cls.logger.debug("This test requires at least 1 (Up and Enabled) cluster in the zone") + cls.skipTests = True + return + + for cluster in clusters: + cls.hosts = Host.list(cls.apiclient, zoneid=cls.zone.id, clusterid=cluster.id, state='Up', + resourcestate='Enabled') + if not cls.hosts or not isinstance(cls.hosts, list) or len(cls.hosts) < 2: + cls.logger.debug("This test requires at least two (Up and Enabled) hosts in the zone") + cls.skipTests = True + return + else: + cls.cluster = Cluster(jsonHelper.jsonDump.dump(cluster)) + break + + cls.domain = get_domain(cls.apiclient) + + # 1. Create large service offering + cls.service_offering = ServiceOffering.create(cls.apiclient, cls.services["service_offerings"]["large"]) + cls._cleanup.append(cls.service_offering) + + # 2. Create a network + cls.services["network"]["name"] = "Test Network" + cls.network_offering = NetworkOffering.create( + cls.apiclient, + cls.services["l2-network_offering"] + ) + cls._cleanup.append(cls.network_offering) + NetworkOffering.update( + cls.network_offering, + cls.apiclient, + id=cls.network_offering.id, + state="enabled" + ) + + cls.network = Network.create( + cls.apiclient, + cls.services["l2-network"], + networkofferingid=cls.network_offering.id, + zoneid=cls.zone.id, + accountid="admin", + domainid=cls.domain.id, + ) + cls._cleanup.append(cls.network) + + @classmethod + def tearDownClass(cls): + super(TestClusterDRS, cls).tearDownClass() + + def setUp(self): + if self.skipTests: + self.skipTest("This test requires at least two (Up and Enabled) hosts in the zone") + self.apiclient = self.testClient.getApiClient() + self.cleanup = [] + + def tearDown(self): + super(TestClusterDRS, self).tearDown() + + @classmethod + def get_vm_host_id(cls, vm_id): + list_vms = VirtualMachine.list(cls.apiclient, id=vm_id) + vm = list_vms[0] + return vm.hostid + + def wait_for_vm_start(self, vm): + """ Wait until vm is Running """ + def check_vm_state(): + vms = VirtualMachine.list( + self.apiclient, + id=vm.id, + listall=True + ) + if isinstance(vms, list): + if vms[0].state == 'Running': + return True, vms[0].state + return False, vms[0].state + + res = wait_until(10, 30, check_vm_state) + if not res: + raise Exception("Failed to wait for VM %s (%s) to be Running" % (vm.name, vm.id)) + return res + + def wait_for_plan_completion(self, plan): + """ Wait until plan is completed """ + def check_plan_status(): + plans = self.cluster.listDrsPlans(self.apiclient, id=plan.id) + if isinstance(plans, list): + if plans[0].status == 'COMPLETED': + return True, plans[0].status + return False, plans[0].status + + res = wait_until(10, 30, check_plan_status) + if not res: + raise Exception("Failed to wait for completion of plan %s" % (plan.id)) + return res + + def get_migrations(self): + """ Wait until migrations are generated. Sometimes it takes a little bit of time for stats to get updated. We generate migrations + until we get at least one migration """ + def generate_migrations(): + drs_plan = self.cluster.generateDrsPlan(self.apiclient, migrations=4) + if len(drs_plan["migrations"]) > 0: + return True, drs_plan["migrations"] + return False, drs_plan["migrations"] + + res, migrations = wait_until(10, 30, generate_migrations) + if not res: + raise Exception("Failed to generate drs migrations") + return migrations + + @attr(tags=["advanced"], required_hardware="false") + def test_01_condensed_drs_algorithm(self): + """ Verify DRS algorithm - condensed""" + # 1. Deploy vm-1 on host 1 + # 2. Deploy vm-2 on host 2 + # 3. Execute DRS to move all VMs on the same host + self.logger.debug("=== Running test_01_condensed_drs_algorithm ===") + + # 1. Deploy vm-1 on host 1 + self.services["virtual_machine"]["name"] = "virtual-machine-1" + self.services["virtual_machine"]["displayname"] = "virtual-machine-1" + self.virtual_machine_1 = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, zoneid=self.zone.id, + networkids=self.network.id, hostid=self.hosts[0].id) + self.cleanup.append(self.virtual_machine_1) + vm_1_host_id = self.get_vm_host_id(self.virtual_machine_1.id) + + # 2. Deploy vm-2 on host 2 + self.services["virtual_machine"]["name"] = "virtual-machine-2" + self.services["virtual_machine"]["displayname"] = "virtual-machine-2" + self.virtual_machine_2 = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, zoneid=self.zone.id, + networkids=self.network.id, hostid=self.hosts[1].id) + vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id) + self.cleanup.append(self.virtual_machine_2) + + self.assertNotEqual(vm_1_host_id, vm_2_host_id, msg="Both VMs should be on different hosts") + self.wait_for_vm_start(self.virtual_machine_1) + self.wait_for_vm_start(self.virtual_machine_2) + + # 3. Generate & execute DRS to move all VMs on the same host + Configurations.update(self.apiclient, "drs.algorithm", "condensed", clusterid=self.cluster.id) + Configurations.update(self.apiclient, "drs.imbalance", "1.0", clusterid=self.cluster.id) + + migrations = self.get_migrations() + vm_to_dest_host_map = { + migration["virtualmachineid"]: migration["destinationhostid"] for migration in migrations + } + + self.assertEqual(len(vm_to_dest_host_map), 1, msg="DRS plan should have 1 migrations") + + executed_plan = self.cluster.executeDrsPlan(self.apiclient, vm_to_dest_host_map) + self.wait_for_plan_completion(executed_plan) + + vm_1_host_id = self.get_vm_host_id(self.virtual_machine_1.id) + vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id) + + self.assertEqual(vm_1_host_id, vm_2_host_id, msg="Both VMs should be on the same host") + + @attr(tags=["advanced"], required_hardware="false") + def test_02_balanced_drs_algorithm(self): + """ Verify DRS algorithm - balanced""" + + # 1. Deploy vm-1 on host 1 + # 2. Deploy vm-2 on host 2 + # 3. Execute DRS to move all VMs on different hosts + + self.logger.debug("=== Running test_02_balanced_drs_algorithm ===") + # 1. Deploy vm-1 on host 1 + self.services["virtual_machine"]["name"] = "virtual-machine-1" + self.services["virtual_machine"]["displayname"] = "virtual-machine-1" + self.virtual_machine_1 = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, zoneid=self.zone.id, + networkids=self.network.id, hostid=self.hosts[0].id) + self.cleanup.append(self.virtual_machine_1) + vm_1_host_id = self.get_vm_host_id(self.virtual_machine_1.id) + + # 2. Deploy vm-2 on host 1 + self.services["virtual_machine"]["name"] = "virtual-machine-2" + self.services["virtual_machine"]["displayname"] = "virtual-machine-2" + self.virtual_machine_2 = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, zoneid=self.zone.id, + networkids=self.network.id, hostid=self.hosts[0].id) + vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id) + self.cleanup.append(self.virtual_machine_2) + + self.assertEqual(vm_1_host_id, vm_2_host_id, msg="Both VMs should be on same hosts") + self.wait_for_vm_start(self.virtual_machine_1) + self.wait_for_vm_start(self.virtual_machine_2) + + # 3. Execute DRS to move all VMs on different hosts + Configurations.update(self.apiclient, "drs.algorithm", "balanced", clusterid=self.cluster.id) + Configurations.update(self.apiclient, "drs.imbalance", "1.0", clusterid=self.cluster.id) + + migrations = self.get_migrations() + vm_to_dest_host_map = { + migration["virtualmachineid"]: migration["destinationhostid"] for migration in migrations + } + + self.assertEqual(len(vm_to_dest_host_map), 1, msg="DRS plan should have 1 migrations") + + executed_plan = self.cluster.executeDrsPlan(self.apiclient, vm_to_dest_host_map) + self.wait_for_plan_completion(executed_plan) + + vm_1_host_id = self.get_vm_host_id(self.virtual_machine_1.id) + vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id) + + self.assertNotEqual(vm_1_host_id, vm_2_host_id, msg="Both VMs should be on different hosts") diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 7f234e0f208..d57f1a7e552 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -3131,6 +3131,35 @@ class Cluster: [setattr(cmd, k, v) for k, v in list(kwargs.items())] return (apiclient.updateCluster(cmd)) + def listDrsPlans(cls, apiclient, **kwargs): + """List drs plans for cluster""" + + cmd = listClusterDrsPlan.listClusterDrsPlanCmd() + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + return apiclient.listClusterDrsPlan(cmd) + + def generateDrsPlan(cls, apiclient, migrations=None): + """Generate a drs plan for cluster""" + + cmd = generateClusterDrsPlan.generateClusterDrsPlanCmd() + cmd.id = cls.id + cmd.migrations = migrations + return apiclient.generateClusterDrsPlan(cmd) + + def executeDrsPlan(cls, apiclient, migrateto=None): + """Execute drs plan on cluster""" + + cmd = executeClusterDrsPlan.executeClusterDrsPlanCmd() + cmd.id = cls.id + if migrateto: + cmd.migrateto = [] + for vm, host in list(migrateto.items()): + cmd.migrateto.append({ + 'vm': vm, + 'host': host + }) + return apiclient.executeClusterDrsPlan(cmd) + class Host: """Manage Host life cycle""" diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 85f6627a103..a10ed69320a 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -668,6 +668,7 @@ "label.destaddressgroupuuid": "Destination Address Group", "label.destcidr": "Destination CIDR", "label.destendport": "Destination End Port", +"label.desthost": "Destination host", "label.destination": "Destination", "label.destinationphysicalnetworkid": "Destination physical network ID", "label.destinationtype": "Destination Type", @@ -757,6 +758,10 @@ "label.download.state": "Download state", "label.dpd": "Dead peer detection", "label.driver": "Driver", +"label.drs": "DRS", +"label.drs.plan": "DRS Plan", +"label.drs.generate.plan": "Generate DRS plan", +"label.drs.no.plan.generated": "No DRS plan has been generated as the cluster is not imbalanced according to the threshold set", "label.duration": "Duration (in sec)", "label.duration.custom": "Custom", "label.duration.1hour": "1 hour", @@ -837,6 +842,7 @@ "label.every": "Every", "label.example": "Example", "label.example.plugin": "ExamplePlugin", +"label.execute": "Execute", "label.expunge": "Expunge", "label.expungevmgraceperiod": "Expunge VM grace period (in sec)", "label.expunged": "Expunged", @@ -1219,6 +1225,7 @@ "label.matchall": "Match all", "label.max.primary.storage": "Max. primary (GiB)", "label.max.secondary.storage": "Max. secondary (GiB)", +"label.max.migrations": "Max. migrations", "label.maxcpu": "Max. CPU cores", "label.maxcpunumber": "Max CPU cores", "label.maxdatavolumeslimit": "Max data volumes limit", @@ -1840,6 +1847,7 @@ "label.softwareversion": "Software version", "label.source.based": "SourceBased", "label.sourcecidr": "Source CIDR", +"label.sourcehost": "Source host", "label.sourceipaddress": "Source IP address", "label.sourceipaddressnetworkid": "Network ID of source IP address", "label.sourcenat": "Source NAT", @@ -1935,6 +1943,7 @@ "label.submit": "Submit", "label.succeeded": "Succeeded", "label.success": "Success", +"label.success.migrations": "Successful migrations", "label.success.set": "Successfully set", "label.success.updated": "Successfully updated", "label.suitability": "Suitability", @@ -2572,6 +2581,8 @@ "message.desc.registered.user.data": "Registered a User Data.", "message.desc.zone": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.", "message.desc.zone.edge": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. An edge zone consists of one or more hosts (each of which provides local storage as primary storage servers). Only shared and L2 networks can be deployed in such zones and functionalities that require secondary storages are not supported.", +"message.drs.plan.description": "The maximum number of live migrations allowed for DRS. Configure DRS under the settings tab before generating a plan or to enable automatic DRS for the cluster.", +"message.drs.plan.executed": "DRS plan executed successfully.", "message.zone.edge.local.storage": "Local storage will be used by default for user VMs and virtual routers", "message.detach.disk": "Are you sure you want to detach this disk?", "message.detach.iso.confirm": "Please confirm that you want to detach the ISO from this virtual instance.", diff --git a/ui/src/components/view/DedicateData.vue b/ui/src/components/view/DedicateData.vue index 0ff01ac34d5..d99b080e00d 100644 --- a/ui/src/components/view/DedicateData.vue +++ b/ui/src/components/view/DedicateData.vue @@ -24,7 +24,7 @@

{{ $t('label.domainid') }}
- {{ dedicatedDomainId }} + {{ dedicatedDomainId }}

{{ $t('label.account') }}
diff --git a/ui/src/config/section/infra/clusters.js b/ui/src/config/section/infra/clusters.js index ca6dde7ec9b..8fc4ebd54a9 100644 --- a/ui/src/config/section/infra/clusters.js +++ b/ui/src/config/section/infra/clusters.js @@ -54,9 +54,18 @@ export default { }, { name: 'settings', component: shallowRef(defineAsyncComponent(() => import('@/components/view/SettingsTab.vue'))) + }, { + name: 'drs', + component: shallowRef(defineAsyncComponent(() => import('@/views/infra/ClusterDRSTab.vue'))) }, { name: 'comments', component: shallowRef(defineAsyncComponent(() => import('@/components/view/AnnotationsTab.vue'))) + }, + { + name: 'events', + resourceType: 'Cluster', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/EventsTab.vue'))), + show: () => { return 'listEvents' in store.getters.apis } }], actions: [ { @@ -113,6 +122,16 @@ export default { defaultArgs: { managedstate: 'Unmanaged' }, show: (record) => { return record.managedstate === 'Managed' } }, + { + api: 'executeDRS', + icon: 'gold-outlined', + label: 'label.action.drs.cluster', + message: 'message.action.drs.cluster', + dataView: true, + defaultArgs: { iterations: null }, + args: ['iterations'], + show: (record) => { return record.managedstate === 'Managed' } + }, { api: 'enableOutOfBandManagementForCluster', icon: 'plus-circle-outlined', diff --git a/ui/src/views/infra/ClusterDRSTab.vue b/ui/src/views/infra/ClusterDRSTab.vue new file mode 100644 index 00000000000..4ff255bf718 --- /dev/null +++ b/ui/src/views/infra/ClusterDRSTab.vue @@ -0,0 +1,297 @@ +// 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. + + + + From 4ff592ac2c151ff9be61b78c82d6508e74a52288 Mon Sep 17 00:00:00 2001 From: Marcus Sorensen Date: Thu, 26 Oct 2023 00:44:09 -0600 Subject: [PATCH 09/13] Use UserVmDao for listVirtualMachines API to increase performance (#8012) Co-authored-by: Marcus Sorensen --- .../com/cloud/utils/db/GenericDaoBase.java | 37 +- .../cloud/utils/db/GenericDaoBaseTest.java | 52 ++ .../com/cloud/api/query/QueryManagerImpl.java | 790 ++++++++++-------- 3 files changed, 540 insertions(+), 339 deletions(-) diff --git a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java index a5ed004c30e..5fd9580342c 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java +++ b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java @@ -1264,6 +1264,11 @@ public abstract class GenericDaoBase extends Compone @DB() protected void addJoins(StringBuilder str, Collection>> joins) { + addJoins(str, joins, new HashMap<>()); + } + + @DB() + protected void addJoins(StringBuilder str, Collection>> joins, Map joinedTableNames) { boolean hasWhereClause = true; int fromIndex = str.lastIndexOf("WHERE"); if (fromIndex == -1) { @@ -1274,18 +1279,27 @@ public abstract class GenericDaoBase extends Compone } for (JoinBuilder> join : joins) { + String joinTableName = join.getSecondAttribute().table; + String joinTableAlias = findNextJoinTableName(joinTableName, joinedTableNames); StringBuilder onClause = new StringBuilder(); onClause.append(" ") .append(join.getType().getName()) .append(" ") - .append(join.getSecondAttribute().table) - .append(" ON ") + .append(joinTableName); + if (!joinTableAlias.equals(joinTableName)) { + onClause.append(" ").append(joinTableAlias); + } + onClause.append(" ON ") .append(join.getFirstAttribute().table) .append(".") .append(join.getFirstAttribute().columnName) - .append("=") - .append(join.getSecondAttribute().table) - .append(".") + .append("="); + if(!joinTableAlias.equals(joinTableName)) { + onClause.append(joinTableAlias); + } else { + onClause.append(joinTableName); + } + onClause.append(".") .append(join.getSecondAttribute().columnName) .append(" "); str.insert(fromIndex, onClause); @@ -1306,11 +1320,22 @@ public abstract class GenericDaoBase extends Compone for (JoinBuilder> join : joins) { if (join.getT().getJoins() != null) { - addJoins(str, join.getT().getJoins()); + addJoins(str, join.getT().getJoins(), joinedTableNames); } } } + protected static String findNextJoinTableName(String tableName, Map usedTableNames) { + if (usedTableNames.containsKey(tableName)) { + Integer tableCounter = usedTableNames.get(tableName); + usedTableNames.put(tableName, ++tableCounter); + tableName = tableName + tableCounter; + } else { + usedTableNames.put(tableName, 0); + } + return tableName; + } + private void removeAndClause(StringBuilder sql) { sql.delete(sql.length() - 4, sql.length()); } diff --git a/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java b/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java index 352ea735aa7..b950501337b 100644 --- a/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java +++ b/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java @@ -18,6 +18,10 @@ package com.cloud.utils.db; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import org.junit.Assert; import org.junit.Before; @@ -36,6 +40,7 @@ public class GenericDaoBaseTest { @Mock SQLException mockedSQLException; + private static final DbTestDao dbTestDao = new DbTestDao(); private static final String INTEGRITY_CONSTRAINT_VIOLATION = "23000"; private static final int DUPLICATE_ENTRY_ERRO_CODE = 1062; @@ -214,4 +219,51 @@ public class GenericDaoBaseTest { Assert.assertEquals(resultSetSize, result); } + + @Test + public void addJoinsTest() { + StringBuilder joinString = new StringBuilder(); + Collection>> joins = new ArrayList<>(); + + Attribute attr1 = new Attribute("table1", "column1"); + Attribute attr2 = new Attribute("table2", "column2"); + Attribute attr3 = new Attribute("table3", "column1"); + Attribute attr4 = new Attribute("table4", "column2"); + + joins.add(new JoinBuilder<>(dbTestDao.createSearchCriteria(), attr1, attr2, JoinBuilder.JoinType.INNER)); + joins.add(new JoinBuilder<>(dbTestDao.createSearchCriteria(), attr3, attr4, JoinBuilder.JoinType.INNER)); + dbTestDao.addJoins(joinString, joins); + + Assert.assertEquals(" INNER JOIN table2 ON table1.column1=table2.column2 INNER JOIN table4 ON table3.column1=table4.column2 ", joinString.toString()); + } + + @Test + public void multiJoinSameTableTest() { + StringBuilder joinString = new StringBuilder(); + Collection>> joins = new ArrayList<>(); + + Attribute tAc1 = new Attribute("tableA", "column1"); + Attribute tAc2 = new Attribute("tableA", "column2"); + Attribute tAc3 = new Attribute("tableA", "column3"); + Attribute tBc2 = new Attribute("tableB", "column2"); + Attribute tCc3 = new Attribute("tableC", "column3"); + Attribute tDc4 = new Attribute("tableD", "column4"); + + joins.add(new JoinBuilder<>(dbTestDao.createSearchCriteria(), tBc2, tAc1, JoinBuilder.JoinType.INNER)); + joins.add(new JoinBuilder<>(dbTestDao.createSearchCriteria(), tCc3, tAc2, JoinBuilder.JoinType.INNER)); + joins.add(new JoinBuilder<>(dbTestDao.createSearchCriteria(), tDc4, tAc3, JoinBuilder.JoinType.INNER)); + dbTestDao.addJoins(joinString, joins); + + Assert.assertEquals(" INNER JOIN tableA ON tableB.column2=tableA.column1 INNER JOIN tableA tableA1 ON tableC.column3=tableA1.column2 INNER JOIN tableA tableA2 ON tableD.column4=tableA2.column3 ", joinString.toString()); + } + + @Test + public void findNextTableNameTest() { + Map usedTables = new HashMap<>(); + + Assert.assertEquals("tableA", GenericDaoBase.findNextJoinTableName("tableA", usedTables)); + Assert.assertEquals("tableA1", GenericDaoBase.findNextJoinTableName("tableA", usedTables)); + Assert.assertEquals("tableA2", GenericDaoBase.findNextJoinTableName("tableA", usedTables)); + Assert.assertEquals("tableA3", GenericDaoBase.findNextJoinTableName("tableA", usedTables)); + } } diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 491104b654c..946b48cf480 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -34,6 +34,22 @@ import java.util.stream.Stream; import javax.inject.Inject; +import com.cloud.host.Host; +import com.cloud.host.dao.HostDao; +import com.cloud.network.as.AutoScaleVmGroupVmMapVO; +import com.cloud.network.as.dao.AutoScaleVmGroupDao; +import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.user.SSHKeyPairVO; +import com.cloud.user.dao.SSHKeyPairDao; +import com.cloud.vm.InstanceGroupVMMapVO; +import com.cloud.vm.NicVO; +import com.cloud.vm.UserVmDetailVO; +import com.cloud.vm.dao.InstanceGroupVMMapDao; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDetailsDao; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker; @@ -118,6 +134,8 @@ import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.BackupOfferingVO; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; @@ -201,7 +219,6 @@ import com.cloud.exception.CloudAuthenticationException; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; import com.cloud.ha.HighAvailabilityManager; -import com.cloud.host.Host; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.RouterHealthCheckResult; @@ -247,7 +264,6 @@ import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.StoragePoolTagsDao; import com.cloud.storage.dao.VMTemplateDao; -import com.cloud.storage.dao.VolumeDao; import com.cloud.tags.ResourceTagVO; import com.cloud.tags.dao.ResourceTagDao; import com.cloud.template.VirtualMachineTemplate.State; @@ -281,6 +297,8 @@ import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; +import static com.cloud.vm.VmDetailConstants.SSH_PUBLIC_KEY; + @Component public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements QueryService, Configurable { @@ -289,7 +307,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private static final String ID_FIELD = "id"; @Inject - private AccountManager _accountMgr; + private AccountManager accountMgr; @Inject private ProjectManager _projectMgr; @@ -316,7 +334,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private UserVmJoinDao _userVmJoinDao; @Inject - private UserVmDao _userVmDao; + private UserVmDao userVmDao; @Inject private VMInstanceDao _vmInstanceDao; @@ -325,7 +343,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private SecurityGroupJoinDao _securityGroupJoinDao; @Inject - private SecurityGroupVMMapDao _securityGroupVMMapDao; + private SecurityGroupVMMapDao securityGroupVMMapDao; @Inject private DomainRouterJoinDao _routerJoinDao; @@ -346,7 +364,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private ProjectAccountJoinDao _projectAccountJoinDao; @Inject - private HostJoinDao _hostJoinDao; + private HostJoinDao hostJoinDao; @Inject private VolumeJoinDao _volumeJoinDao; @@ -429,7 +447,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private AffinityGroupDomainMapDao _affinityGroupDomainMapDao; @Inject - private ResourceTagDao _resourceTagDao; + private ResourceTagDao resourceTagDao; @Inject private DataStoreManager dataStoreManager; @@ -447,7 +465,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private RouterHealthCheckResultDao routerHealthCheckResultDao; @Inject - private PrimaryDataStoreDao _storagePoolDao; + private PrimaryDataStoreDao storagePoolDao; @Inject private StoragePoolDetailsDao _storagePoolDetailsDao; @@ -470,8 +488,37 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private ManagementServerHostDao msHostDao; @Inject - private SnapshotJoinDao snapshotJoinDao; + private NetworkDao networkDao; + @Inject + private NicDao nicDao; + + @Inject + private HostDao hostDao; + + @Inject + private InstanceGroupVMMapDao instanceGroupVMMapDao; + + @Inject + private AffinityGroupVMMapDao affinityGroupVMMapDao; + + @Inject + private UserVmDetailsDao userVmDetailsDao; + + @Inject + private SSHKeyPairDao sshKeyPairDao; + + @Inject + private BackupOfferingDao backupOfferingDao; + + @Inject + private AutoScaleVmGroupDao autoScaleVmGroupDao; + + @Inject + private AutoScaleVmGroupVmMapDao autoScaleVmGroupVmMapDao; + + @Inject + private SnapshotJoinDao snapshotJoinDao; @Inject EntityManager entityManager; @@ -595,13 +642,13 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private Pair, Integer> getUserListInternal(Account caller, List permittedAccounts, boolean listAll, Long id, Object username, Object type, String accountName, Object state, String keyword, Long domainId, boolean recursive, Filter searchFilter) { Ternary domainIdRecursiveListProject = new Ternary(domainId, recursive, null); - _accountMgr.buildACLSearchParameters(caller, id, accountName, null, permittedAccounts, domainIdRecursiveListProject, listAll, false); + accountMgr.buildACLSearchParameters(caller, id, accountName, null, permittedAccounts, domainIdRecursiveListProject, listAll, false); domainId = domainIdRecursiveListProject.first(); Boolean isRecursive = domainIdRecursiveListProject.second(); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); SearchBuilder sb = _userAccountJoinDao.createSearchBuilder(); - _accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); sb.and("username", sb.entity().getUsername(), Op.LIKE); if (id != null && id == 1) { // system user should NOT be searchable @@ -627,7 +674,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q SearchCriteria sc = sb.create(); // building ACL condition - _accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); if (keyword != null) { SearchCriteria ssc = _userAccountJoinDao.createSearchCriteria(); @@ -687,7 +734,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private Pair, Integer> searchForEventsInternal(ListEventsCmd cmd) { Account caller = CallContext.current().getCallingAccount(); - boolean isRootAdmin = _accountMgr.isRootAdmin(caller.getId()); + boolean isRootAdmin = accountMgr.isRootAdmin(caller.getId()); List permittedAccounts = new ArrayList(); Long id = cmd.getId(); @@ -727,12 +774,12 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } if (!isRootAdmin && object instanceof ControlledEntity) { ControlledEntity entity = (ControlledEntity)object; - _accountMgr.checkAccess(CallContext.current().getCallingAccount(), SecurityChecker.AccessType.ListEntry, entity.getAccountId() == caller.getId(), entity); + accountMgr.checkAccess(CallContext.current().getCallingAccount(), SecurityChecker.AccessType.ListEntry, entity.getAccountId() == caller.getId(), entity); } } Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), cmd.isRecursive(), null); - _accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); Long domainId = domainIdRecursiveListProject.first(); Boolean isRecursive = domainIdRecursiveListProject.second(); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); @@ -743,7 +790,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q searchFilter.addOrderBy(EventJoinVO.class, "id", false); SearchBuilder sb = _eventJoinDao.createSearchBuilder(); - _accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); sb.and("levelL", sb.entity().getLevel(), SearchCriteria.Op.LIKE); @@ -762,10 +809,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q SearchCriteria sc = sb.create(); // building ACL condition - _accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); // For end users display only enabled events - if (!_accountMgr.isRootAdmin(caller.getId())) { + if (!accountMgr.isRootAdmin(caller.getId())) { sc.setParameters("displayEvent", true); } @@ -880,14 +927,14 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), cmd.isRecursive(), null); - _accountMgr.buildACLSearchParameters(caller, null, cmd.getAccountName(), projectId, permittedAccounts, domainIdRecursiveListProject, listAll, false); + accountMgr.buildACLSearchParameters(caller, null, cmd.getAccountName(), projectId, permittedAccounts, domainIdRecursiveListProject, listAll, false); Long domainId = domainIdRecursiveListProject.first(); Boolean isRecursive = domainIdRecursiveListProject.second(); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); Filter searchFilter = new Filter(ResourceTagJoinVO.class, "resourceType", false, cmd.getStartIndex(), cmd.getPageSizeVal()); SearchBuilder sb = _resourceTagJoinDao.createSearchBuilder(); - _accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); sb.and("key", sb.entity().getKey(), SearchCriteria.Op.EQ); sb.and("value", sb.entity().getValue(), SearchCriteria.Op.EQ); @@ -902,7 +949,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q // now set the SC criteria... SearchCriteria sc = sb.create(); - _accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); if (key != null) { sc.setParameters("key", key); @@ -952,7 +999,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q List permittedAccounts = new ArrayList(); Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), cmd.isRecursive(), null); - _accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); Long domainId = domainIdRecursiveListProject.first(); Boolean isRecursive = domainIdRecursiveListProject.second(); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); @@ -960,13 +1007,13 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Filter searchFilter = new Filter(InstanceGroupJoinVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); SearchBuilder sb = _vmGroupJoinDao.createSearchBuilder(); - _accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); SearchCriteria sc = sb.create(); - _accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); if (keyword != null) { SearchCriteria ssc = _vmGroupJoinDao.createSearchCriteria(); @@ -997,7 +1044,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q ResponseView respView = ResponseView.Restricted; Account caller = CallContext.current().getCallingAccount(); - if (_accountMgr.isRootAdmin(caller.getId())) { + if (accountMgr.isRootAdmin(caller.getId())) { respView = ResponseView.Full; } List vmResponses = ViewResponseHelper.createUserVmResponse(respView, "virtualmachine", cmd.getDetails(), cmd.getAccumulate(), cmd.getShowUserData(), @@ -1019,21 +1066,79 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } private Pair, Integer> searchForUserVMsInternal(ListVMsCmd cmd) { + Pair, Integer> vmIdPage = searchForUserVMIdsAndCount(cmd); + + Integer count = vmIdPage.second(); + Long[] idArray = vmIdPage.first().toArray(new Long[0]); + + if (count == 0) { + return new Pair<>(new ArrayList<>(), count); + } + + // search vm details by ids + List vms = _userVmJoinDao.searchByIds( idArray); + return new Pair<>(vms, count); + } + + private Pair, Integer> searchForUserVMIdsAndCount(ListVMsCmd cmd) { Account caller = CallContext.current().getCallingAccount(); List permittedAccounts = new ArrayList<>(); - boolean listAll = cmd.listAll(); Long id = cmd.getId(); + Boolean display = cmd.getDisplay(); + String hypervisor = cmd.getHypervisor(); + String state = cmd.getState(); + Long zoneId = cmd.getZoneId(); + Long templateId = cmd.getTemplateId(); + Long serviceOfferingId = cmd.getServiceOfferingId(); + Boolean isHaEnabled = cmd.getHaEnabled(); + String keyword = cmd.getKeyword(); + Long networkId = cmd.getNetworkId(); + Long isoId = cmd.getIsoId(); + String vmHostName = cmd.getName(); + Long hostId = null; + Long podId = null; + Long clusterId = null; + Long groupId = cmd.getGroupId(); + Long vpcId = cmd.getVpcId(); + Long affinityGroupId = cmd.getAffinityGroupId(); + String keyPairName = cmd.getKeyPairName(); + Long securityGroupId = cmd.getSecurityGroupId(); + Long autoScaleVmGroupId = cmd.getAutoScaleVmGroupId(); + Long backupOfferingId = cmd.getBackupOfferingId(); + Long storageId = null; + StoragePoolVO pool = null; Long userId = cmd.getUserId(); Map tags = cmd.getTags(); - Boolean display = cmd.getDisplay(); + + boolean isAdmin = false; + boolean isRootAdmin = false; + + if (accountMgr.isAdmin(caller.getId())) { + isAdmin = true; + } + + if (accountMgr.isRootAdmin(caller.getId())) { + isRootAdmin = true; + podId = (Long) getObjectPossibleMethodValue(cmd, "getPodId"); + clusterId = (Long) getObjectPossibleMethodValue(cmd, "getClusterId"); + hostId = (Long) getObjectPossibleMethodValue(cmd, "getHostId"); + storageId = (Long) getObjectPossibleMethodValue(cmd, "getStorageId"); + if (storageId != null) { + pool = storagePoolDao.findById( storageId); + if (pool == null) { + throw new InvalidParameterValueException("Unable to find specified storage pool"); + } + } + } + Ternary domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); - _accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, listAll, false); + accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, listAll, false); Long domainId = domainIdRecursiveListProject.first(); Boolean isRecursive = domainIdRecursiveListProject.second(); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); - Filter searchFilter = new Filter(UserVmJoinVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + Filter searchFilter = new Filter(UserVmVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); List ids = null; if (cmd.getId() != null) { @@ -1046,302 +1151,319 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q ids = cmd.getIds(); } - // first search distinct vm id by using query criteria and pagination - SearchBuilder sb = _userVmJoinDao.createSearchBuilder(); - sb.select(null, Func.DISTINCT, sb.entity().getId()); // select distinct ids + SearchBuilder userVmSearchBuilder = userVmDao.createSearchBuilder(); + userVmSearchBuilder.select(null, Func.DISTINCT, userVmSearchBuilder.entity().getId()); + accountMgr.buildACLSearchBuilder(userVmSearchBuilder, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); - _accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); - - String hypervisor = cmd.getHypervisor(); - Object name = cmd.getName(); - String state = cmd.getState(); - Object zoneId = cmd.getZoneId(); - Object keyword = cmd.getKeyword(); - boolean isAdmin = false; - boolean isRootAdmin = false; - if (_accountMgr.isAdmin(caller.getId())) { - isAdmin = true; - } - if (_accountMgr.isRootAdmin(caller.getId())) { - isRootAdmin = true; - } - - Object groupId = cmd.getGroupId(); - Object networkId = cmd.getNetworkId(); if (HypervisorType.getType(hypervisor) == HypervisorType.None && hypervisor != null) { // invalid hypervisor type input throw new InvalidParameterValueException("Invalid HypervisorType " + hypervisor); } - Object templateId = cmd.getTemplateId(); - Object isoId = cmd.getIsoId(); - Object vpcId = cmd.getVpcId(); - Object affinityGroupId = cmd.getAffinityGroupId(); - Object keyPairName = cmd.getKeyPairName(); - Object serviceOffId = cmd.getServiceOfferingId(); - Object securityGroupId = cmd.getSecurityGroupId(); - Object backupOfferingId = cmd.getBackupOfferingId(); - Object isHaEnabled = cmd.getHaEnabled(); - Object autoScaleVmGroupId = cmd.getAutoScaleVmGroupId(); - Object pod = null; - Object clusterId = null; - Object hostId = null; - Object storageId = null; - if (_accountMgr.isRootAdmin(caller.getId())) { - pod = getObjectPossibleMethodValue(cmd, "getPodId"); - clusterId = getObjectPossibleMethodValue(cmd, "getClusterId"); - hostId = getObjectPossibleMethodValue(cmd, "getHostId"); - storageId = getObjectPossibleMethodValue(cmd, "getStorageId"); - } - - sb.and("displayName", sb.entity().getDisplayName(), SearchCriteria.Op.LIKE); - sb.and("idIN", sb.entity().getId(), SearchCriteria.Op.IN); - sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); - sb.and("stateEQ", sb.entity().getState(), SearchCriteria.Op.EQ); - sb.and("stateNEQ", sb.entity().getState(), SearchCriteria.Op.NEQ); - sb.and("stateNIN", sb.entity().getState(), SearchCriteria.Op.NIN); - sb.and("dataCenterId", sb.entity().getDataCenterId(), SearchCriteria.Op.EQ); - sb.and("podId", sb.entity().getPodId(), SearchCriteria.Op.EQ); - if (clusterId != null) { - sb.and().op("clusterId", sb.entity().getClusterId(), SearchCriteria.Op.EQ); - sb.or("clusterHostId", sb.entity().getHostId(), Op.IN); - sb.or("clusterLastHostId", sb.entity().getLastHostId(), Op.IN); - sb.cp(); - } - sb.and("hypervisorType", sb.entity().getHypervisorType(), SearchCriteria.Op.EQ); - sb.and("hostIdEQ", sb.entity().getHostId(), SearchCriteria.Op.EQ); - sb.and("templateId", sb.entity().getTemplateId(), SearchCriteria.Op.EQ); - sb.and("isoId", sb.entity().getIsoId(), SearchCriteria.Op.EQ); - sb.and("instanceGroupId", sb.entity().getInstanceGroupId(), SearchCriteria.Op.EQ); - - if (serviceOffId != null) { - sb.and("serviceOfferingId", sb.entity().getServiceOfferingId(), SearchCriteria.Op.EQ); - } - - if (backupOfferingId != null) { - sb.and("backupOfferingId", sb.entity().getBackupOfferingId(), SearchCriteria.Op.EQ); - } - - if (display != null) { - sb.and("display", sb.entity().isDisplayVm(), SearchCriteria.Op.EQ); - } - - if (isHaEnabled != null) { - sb.and("haEnabled", sb.entity().isHaEnabled(), SearchCriteria.Op.EQ); - } - - if (groupId != null && (Long)groupId != -1) { - sb.and("instanceGroupId", sb.entity().getInstanceGroupId(), SearchCriteria.Op.EQ); - } - - if (userId != null) { - sb.and("userId", sb.entity().getUserId(), SearchCriteria.Op.EQ); - } - - if (networkId != null) { - sb.and("networkId", sb.entity().getNetworkId(), SearchCriteria.Op.EQ); - } - - if (vpcId != null && networkId == null) { - sb.and("vpcId", sb.entity().getVpcId(), SearchCriteria.Op.EQ); - } - - if (storageId != null) { - StoragePoolVO poolVO = _storagePoolDao.findById((Long) storageId); - if (poolVO.getPoolType() == Storage.StoragePoolType.DatastoreCluster) { - sb.and("poolId", sb.entity().getPoolId(), SearchCriteria.Op.IN); - } else { - sb.and("poolId", sb.entity().getPoolId(), SearchCriteria.Op.EQ); - } - } - - if (affinityGroupId != null) { - sb.and("affinityGroupId", sb.entity().getAffinityGroupId(), SearchCriteria.Op.EQ); - } - - if (keyPairName != null) { - sb.and("keyPairName", sb.entity().getKeypairNames(), SearchCriteria.Op.FIND_IN_SET); - } - - if (!isRootAdmin) { - sb.and("displayVm", sb.entity().isDisplayVm(), SearchCriteria.Op.EQ); - } - - if (securityGroupId != null) { - sb.and("securityGroupId", sb.entity().getSecurityGroupId(), SearchCriteria.Op.EQ); - } - - if (autoScaleVmGroupId != null) { - sb.and("autoScaleVmGroupId", sb.entity().getAutoScaleVmGroupId(), SearchCriteria.Op.EQ); - } - - // populate the search criteria with the values passed in - SearchCriteria sc = sb.create(); - - // building ACL condition - _accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); - - if (tags != null && !tags.isEmpty()) { - SearchCriteria tagSc = _userVmJoinDao.createSearchCriteria(); - for (Map.Entry entry : tags.entrySet()) { - SearchCriteria tsc = _userVmJoinDao.createSearchCriteria(); - tsc.addAnd("tagKey", SearchCriteria.Op.EQ, entry.getKey()); - tsc.addAnd("tagValue", SearchCriteria.Op.EQ, entry.getValue()); - tagSc.addOr("tagKey", SearchCriteria.Op.SC, tsc); - } - sc.addAnd("tagKey", SearchCriteria.Op.SC, tagSc); - } - - if (groupId != null && (Long)groupId != -1) { - sc.setParameters("instanceGroupId", groupId); - } - - if (keyword != null) { - SearchCriteria ssc = _userVmJoinDao.createSearchCriteria(); - String likeKeyword = String.format("%%%s%%", keyword); - ssc.addOr("displayName", SearchCriteria.Op.LIKE, likeKeyword); - ssc.addOr("name", SearchCriteria.Op.LIKE, likeKeyword); - if (isRootAdmin) { - ssc.addOr("instanceName", SearchCriteria.Op.LIKE, likeKeyword); - } - ssc.addOr("ipAddress", SearchCriteria.Op.LIKE, likeKeyword); - ssc.addOr("publicIpAddress", SearchCriteria.Op.LIKE, likeKeyword); - ssc.addOr("ip6Address", SearchCriteria.Op.LIKE, likeKeyword); - ssc.addOr("state", SearchCriteria.Op.EQ, keyword); - sc.addAnd("displayName", SearchCriteria.Op.SC, ssc); - } - - if (serviceOffId != null) { - sc.setParameters("serviceOfferingId", serviceOffId); - } - - if (backupOfferingId != null) { - sc.setParameters("backupOfferingId", backupOfferingId); - } - - if (securityGroupId != null) { - sc.setParameters("securityGroupId", securityGroupId); - } - - if (autoScaleVmGroupId != null) { - sc.setParameters("autoScaleVmGroupId", autoScaleVmGroupId); - } - - if (display != null) { - sc.setParameters("display", display); - } - - if (isHaEnabled != null) { - sc.setParameters("haEnabled", isHaEnabled); - } if (ids != null && !ids.isEmpty()) { - sc.setParameters("idIN", ids.toArray()); + userVmSearchBuilder.and("idIN", userVmSearchBuilder.entity().getId(), Op.IN); + } + + userVmSearchBuilder.and("displayName", userVmSearchBuilder.entity().getDisplayName(), Op.LIKE); + userVmSearchBuilder.and("stateEQ", userVmSearchBuilder.entity().getState(), Op.EQ); + userVmSearchBuilder.and("stateNEQ", userVmSearchBuilder.entity().getState(), Op.NEQ); + userVmSearchBuilder.and("stateNIN", userVmSearchBuilder.entity().getState(), Op.NIN); + + if (hostId != null) { + userVmSearchBuilder.and("hostId", userVmSearchBuilder.entity().getHostId(), Op.EQ); + } + + if (zoneId != null) { + userVmSearchBuilder.and("dataCenterId", userVmSearchBuilder.entity().getDataCenterId(), Op.EQ); } if (templateId != null) { - sc.setParameters("templateId", templateId); + userVmSearchBuilder.and("templateId", userVmSearchBuilder.entity().getTemplateId(), Op.EQ); + } + + if (hypervisor != null) { + userVmSearchBuilder.and("hypervisorType", userVmSearchBuilder.entity().getHypervisorType(), Op.EQ); + } + + if (vmHostName != null) { + userVmSearchBuilder.and("name", userVmSearchBuilder.entity().getHostName(), Op.EQ); + } + + if (serviceOfferingId != null) { + userVmSearchBuilder.and("serviceOfferingId", userVmSearchBuilder.entity().getServiceOfferingId(), Op.EQ); + } + if (display != null) { + userVmSearchBuilder.and("display", userVmSearchBuilder.entity().isDisplayVm(), Op.EQ); + } + + if (!isRootAdmin) { + userVmSearchBuilder.and("displayVm", userVmSearchBuilder.entity().isDisplayVm(), Op.EQ); + } + + if (isHaEnabled != null) { + userVmSearchBuilder.and("haEnabled", userVmSearchBuilder.entity().isHaEnabled(), Op.EQ); } if (isoId != null) { - sc.setParameters("isoId", isoId); + userVmSearchBuilder.and("isoId", userVmSearchBuilder.entity().getIsoId(), Op.EQ); } if (userId != null) { - sc.setParameters("userId", userId); + userVmSearchBuilder.and("userId", userVmSearchBuilder.entity().getUserId(), Op.EQ); } - if (networkId != null) { - sc.setParameters("networkId", networkId); + if (podId != null) { + userVmSearchBuilder.and("podId", userVmSearchBuilder.entity().getPodIdToDeployIn(), Op.EQ); } - if (vpcId != null && networkId == null) { - sc.setParameters("vpcId", vpcId); + if (networkId != null || vpcId != null) { + SearchBuilder nicSearch = nicDao.createSearchBuilder(); + nicSearch.and("networkId", nicSearch.entity().getNetworkId(), Op.EQ); + if (vpcId != null) { + SearchBuilder networkSearch = networkDao.createSearchBuilder(); + networkSearch.and("vpcId", networkSearch.entity().getVpcId(), Op.EQ); + nicSearch.join("vpc", networkSearch, networkSearch.entity().getId(), nicSearch.entity().getNetworkId(), JoinBuilder.JoinType.INNER); + } + userVmSearchBuilder.join("nic", nicSearch, nicSearch.entity().getInstanceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); } - if (name != null) { - sc.setParameters("name", name); + if (clusterId != null) { + userVmSearchBuilder.and().op("hostIdIn", userVmSearchBuilder.entity().getHostId(), Op.IN); + userVmSearchBuilder.or().op("lastHostIdIn", userVmSearchBuilder.entity().getLastHostId(), Op.IN); + userVmSearchBuilder.and(userVmSearchBuilder.entity().getState(), Op.EQ).values(VirtualMachine.State.Stopped); + userVmSearchBuilder.cp().cp(); + } + + if (groupId != null && groupId != -1) { + SearchBuilder instanceGroupSearch = instanceGroupVMMapDao.createSearchBuilder(); + instanceGroupSearch.and("groupId", instanceGroupSearch.entity().getGroupId(), Op.EQ); + userVmSearchBuilder.join("instanceGroup", instanceGroupSearch, instanceGroupSearch.entity().getInstanceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); + } + + if (affinityGroupId != null && affinityGroupId != -1) { + SearchBuilder affinityGroupSearch = affinityGroupVMMapDao.createSearchBuilder(); + affinityGroupSearch.and("affinityGroupId", affinityGroupSearch.entity().getAffinityGroupId(), Op.EQ); + userVmSearchBuilder.join("affinityGroup", affinityGroupSearch, affinityGroupSearch.entity().getInstanceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); + } + + if (securityGroupId != null && securityGroupId != -1) { + SearchBuilder securityGroupSearch = securityGroupVMMapDao.createSearchBuilder(); + securityGroupSearch.and("securityGroupId", securityGroupSearch.entity().getSecurityGroupId(), Op.EQ); + userVmSearchBuilder.join("securityGroup", securityGroupSearch, securityGroupSearch.entity().getInstanceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); + } + + if (storageId != null) { + SearchBuilder volumeSearch = volumeDao.createSearchBuilder(); + if (pool.getPoolType().equals(Storage.StoragePoolType.DatastoreCluster)) { + volumeSearch.and("storagePoolId", volumeSearch.entity().getPoolId(), Op.IN); + } else { + volumeSearch.and("storagePoolId", volumeSearch.entity().getPoolId(), Op.EQ); + } + userVmSearchBuilder.join("volume", volumeSearch, volumeSearch.entity().getInstanceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); + } + + if (tags != null && !tags.isEmpty()) { + SearchBuilder resourceTagSearch = resourceTagDao.createSearchBuilder(); + resourceTagSearch.and("resourceType", resourceTagSearch.entity().getResourceType(), Op.EQ); + resourceTagSearch.and().op(); + for (int count = 0; count < tags.size(); count++) { + if (count == 0) { + resourceTagSearch.op("tagKey" + String.valueOf(count), resourceTagSearch.entity().getKey(), Op.EQ); + } else { + resourceTagSearch.or().op("tagKey" + String.valueOf(count), resourceTagSearch.entity().getKey(), Op.EQ); + } + resourceTagSearch.and("tagValue" + String.valueOf(count), resourceTagSearch.entity().getValue(), Op.EQ); + resourceTagSearch.cp(); + } + resourceTagSearch.cp(); + + userVmSearchBuilder.join("tags", resourceTagSearch, resourceTagSearch.entity().getResourceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); + } + + if (keyPairName != null) { + SearchBuilder vmDetailSearchKeys = userVmDetailsDao.createSearchBuilder(); + SearchBuilder vmDetailSearchVmIds = userVmDetailsDao.createSearchBuilder(); + vmDetailSearchKeys.and(vmDetailSearchKeys.entity().getName(), Op.EQ).values(SSH_PUBLIC_KEY); + + SearchBuilder sshKeyPairSearch = sshKeyPairDao.createSearchBuilder(); + sshKeyPairSearch.and("keyPairName", sshKeyPairSearch.entity().getName(), Op.EQ); + + sshKeyPairSearch.join("keyPairToDetailValueJoin", vmDetailSearchKeys, vmDetailSearchKeys.entity().getValue(), sshKeyPairSearch.entity().getPublicKey(), JoinBuilder.JoinType.INNER); + userVmSearchBuilder.join("userVmToDetailJoin", vmDetailSearchVmIds, vmDetailSearchVmIds.entity().getResourceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); + userVmSearchBuilder.join("userVmToKeyPairJoin", sshKeyPairSearch, sshKeyPairSearch.entity().getAccountId(), userVmSearchBuilder.entity().getAccountId(), JoinBuilder.JoinType.INNER); + } + + if (keyword != null) { + userVmSearchBuilder.and().op("keywordDisplayName", userVmSearchBuilder.entity().getDisplayName(), Op.LIKE); + userVmSearchBuilder.or("keywordName", userVmSearchBuilder.entity().getHostName(), Op.LIKE); + userVmSearchBuilder.or("keywordState", userVmSearchBuilder.entity().getState(), Op.EQ); + if (isRootAdmin) { + userVmSearchBuilder.or("keywordInstanceName", userVmSearchBuilder.entity().getInstanceName(), Op.LIKE ); + } + userVmSearchBuilder.cp(); + } + + if (backupOfferingId != null) { + SearchBuilder backupOfferingSearch = backupOfferingDao.createSearchBuilder(); + backupOfferingSearch.and("backupOfferingId", backupOfferingSearch.entity().getId(), Op.EQ); + userVmSearchBuilder.join("backupOffering", backupOfferingSearch, backupOfferingSearch.entity().getId(), userVmSearchBuilder.entity().getBackupOfferingId(), JoinBuilder.JoinType.INNER); + } + + if (autoScaleVmGroupId != null) { + SearchBuilder autoScaleMapSearch = autoScaleVmGroupVmMapDao.createSearchBuilder(); + autoScaleMapSearch.and("autoScaleVmGroupId", autoScaleMapSearch.entity().getVmGroupId(), Op.EQ); + userVmSearchBuilder.join("autoScaleVmGroup", autoScaleMapSearch, autoScaleMapSearch.entity().getInstanceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); + } + + SearchCriteria userVmSearchCriteria = userVmSearchBuilder.create(); + accountMgr.buildACLSearchCriteria(userVmSearchCriteria, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + + if (serviceOfferingId != null) { + userVmSearchCriteria.setParameters("serviceOfferingId", serviceOfferingId); } if (state != null) { if (state.equalsIgnoreCase("present")) { - sc.setParameters("stateNIN", "Destroyed", "Expunging"); + userVmSearchCriteria.setParameters("stateNIN", "Destroyed", "Expunging"); } else { - sc.setParameters("stateEQ", state); + userVmSearchCriteria.setParameters("stateEQ", state); } } if (hypervisor != null) { - sc.setParameters("hypervisorType", hypervisor); + userVmSearchCriteria.setParameters("hypervisorType", hypervisor); } // Don't show Destroyed and Expunging vms to the end user if the AllowUserViewDestroyedVM flag is not set. if (!isAdmin && !AllowUserViewDestroyedVM.valueIn(caller.getAccountId())) { - sc.setParameters("stateNIN", "Destroyed", "Expunging"); + userVmSearchCriteria.setParameters("stateNIN", "Destroyed", "Expunging"); } if (zoneId != null) { - sc.setParameters("dataCenterId", zoneId); + userVmSearchCriteria.setParameters("dataCenterId", zoneId); } - if (affinityGroupId != null) { - sc.setParameters("affinityGroupId", affinityGroupId); + if (templateId != null) { + userVmSearchCriteria.setParameters("templateId", templateId); + } + + if (display != null) { + userVmSearchCriteria.setParameters("display", display); + } + + if (isHaEnabled != null) { + userVmSearchCriteria.setParameters("haEnabled", isHaEnabled); + } + + if (isoId != null) { + userVmSearchCriteria.setParameters("isoId", isoId); + } + + if (ids != null && !ids.isEmpty()) { + userVmSearchCriteria.setParameters("idIN", ids.toArray()); + } + + if (vmHostName != null) { + userVmSearchCriteria.setParameters("name", vmHostName); + } + + if (groupId != null && groupId != -1) { + userVmSearchCriteria.setJoinParameters("instanceGroup","groupId", groupId); + } + + if (affinityGroupId != null && affinityGroupId != -1) { + userVmSearchCriteria.setJoinParameters("affinityGroup", "affinityGroupId", affinityGroupId); + } + + if (securityGroupId != null && securityGroupId != -1) { + userVmSearchCriteria.setJoinParameters("securityGroup","securityGroupId", securityGroupId); + } + + if (keyword != null) { + String keywordMatch = "%" + keyword + "%"; + userVmSearchCriteria.setParameters("keywordDisplayName", keywordMatch); + userVmSearchCriteria.setParameters("keywordName", keywordMatch); + userVmSearchCriteria.setParameters("keywordState", keyword); + if (isRootAdmin) { + userVmSearchCriteria.setParameters("keywordInstanceName", keywordMatch); + } + } + + if (tags != null && !tags.isEmpty()) { + int count = 0; + userVmSearchCriteria.setJoinParameters("tags","resourceType", ResourceObjectType.UserVm); + for (Map.Entry entry : tags.entrySet()) { + userVmSearchCriteria.setJoinParameters("tags", "tagKey" + String.valueOf(count), entry.getKey()); + userVmSearchCriteria.setJoinParameters("tags", "tagValue" + String.valueOf(count), entry.getValue()); + count++; + } } if (keyPairName != null) { - sc.setParameters("keyPairName", keyPairName); + userVmSearchCriteria.setJoinParameters("userVmToKeyPairJoin", "keyPairName", keyPairName); } - if (_accountMgr.isRootAdmin(caller.getId())) { - if (pod != null) { - sc.setParameters("podId", pod); + if (networkId != null) { + userVmSearchCriteria.setJoinParameters("nic", "networkId", networkId); + } + if (vpcId != null) { + userVmSearchCriteria.getJoin("nic").setJoinParameters("vpc", "vpcId", vpcId); + } + + if (userId != null) { + userVmSearchCriteria.setParameters("userId", userId); + } + + if (backupOfferingId != null) { + userVmSearchCriteria.setJoinParameters("backupOffering", "backupOfferingId", backupOfferingId); + } + + if (autoScaleVmGroupId != null) { + userVmSearchCriteria.setJoinParameters("autoScaleVmGroup", "autoScaleVmGroupId", autoScaleVmGroupId); + } + + if (isRootAdmin) { + if (podId != null) { + userVmSearchCriteria.setParameters("podId", podId); if (state == null) { - sc.setParameters("stateNEQ", "Destroyed"); + userVmSearchCriteria.setParameters("stateNEQ", "Destroyed"); } } if (clusterId != null) { - sc.setParameters("clusterId", clusterId); - List hosts = _hostJoinDao.findByClusterId((Long)clusterId, Host.Type.Routing); + List hosts = hostJoinDao.findByClusterId(clusterId, Host.Type.Routing); + if (CollectionUtils.isEmpty(hosts)) { + // cluster has no hosts, so we cannot find VMs, cancel search. + return new Pair<>(new ArrayList<>(), 0); + } List hostIds = hosts.stream().map(HostJoinVO::getId).collect(Collectors.toList()); - sc.setParameters("clusterHostId", hostIds.toArray()); - sc.setParameters("clusterLastHostId", hostIds.toArray()); + userVmSearchCriteria.setParameters("hostIdIn", hostIds.toArray()); + userVmSearchCriteria.setParameters("lastHostIdIn", hostIds.toArray()); } if (hostId != null) { - sc.setParameters("hostIdEQ", hostId); + userVmSearchCriteria.setParameters("hostId", hostId); } - if (storageId != null) { - StoragePoolVO poolVO = _storagePoolDao.findById((Long) storageId); - if (poolVO.getPoolType() == Storage.StoragePoolType.DatastoreCluster) { - List childDatastores = _storagePoolDao.listChildStoragePoolsInDatastoreCluster((Long) storageId); + if (storageId != null && pool != null) { + if (pool.getPoolType().equals(Storage.StoragePoolType.DatastoreCluster)) { + List childDatastores = storagePoolDao.listChildStoragePoolsInDatastoreCluster(storageId); List childDatastoreIds = childDatastores.stream().map(mo -> mo.getId()).collect(Collectors.toList()); - sc.setParameters("poolId", childDatastoreIds.toArray()); + userVmSearchCriteria.setJoinParameters("volume", "storagePoolId", childDatastoreIds.toArray()); } else { - sc.setParameters("poolId", storageId); + userVmSearchCriteria.setJoinParameters("volume", "storagePoolId", storageId); } } + } else { + userVmSearchCriteria.setParameters("displayVm", 1); } - if (!isRootAdmin) { - sc.setParameters("displayVm", 1); - } - // search vm details by ids - Pair, Integer> uniqueVmPair = _userVmJoinDao.searchAndDistinctCount(sc, searchFilter); + Pair, Integer> uniqueVmPair = userVmDao.searchAndDistinctCount(userVmSearchCriteria, searchFilter, new String[]{"vm_instance.id"}); Integer count = uniqueVmPair.second(); - if (count.intValue() == 0) { - // handle empty result cases - return uniqueVmPair; - } - List uniqueVms = uniqueVmPair.first(); - Long[] vmIds = new Long[uniqueVms.size()]; - int i = 0; - for (UserVmJoinVO v : uniqueVms) { - vmIds[i++] = v.getId(); - } - List vms = _userVmJoinDao.searchByIds(vmIds); - return new Pair<>(vms, count); + + List vmIds = uniqueVmPair.first().stream().map(VMInstanceVO::getId).collect(Collectors.toList()); + return new Pair<>(vmIds, count); } @Override @@ -1363,16 +1485,16 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Map tags = cmd.getTags(); if (instanceId != null) { - UserVmVO userVM = _userVmDao.findById(instanceId); + UserVmVO userVM = userVmDao.findById(instanceId); if (userVM == null) { throw new InvalidParameterValueException("Unable to list network groups for virtual machine instance " + instanceId + "; instance not found."); } - _accountMgr.checkAccess(caller, null, true, userVM); + accountMgr.checkAccess(caller, null, true, userVM); return listSecurityGroupRulesByVM(instanceId.longValue(), cmd.getStartIndex(), cmd.getPageSizeVal()); } Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), cmd.isRecursive(), null); - _accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); Long domainId = domainIdRecursiveListProject.first(); Boolean isRecursive = domainIdRecursiveListProject.second(); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); @@ -1381,13 +1503,13 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q SearchBuilder sb = _securityGroupJoinDao.createSearchBuilder(); sb.select(null, Func.DISTINCT, sb.entity().getId()); // select distinct // ids - _accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); SearchCriteria sc = sb.create(); - _accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); if (id != null) { sc.setParameters("id", id); @@ -1435,7 +1557,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private Pair, Integer> listSecurityGroupRulesByVM(long vmId, long pageInd, long pageSize) { Filter sf = new Filter(SecurityGroupVMMapVO.class, null, true, pageInd, pageSize); - Pair, Integer> sgVmMappingPair = _securityGroupVMMapDao.listByInstanceId(vmId, sf); + Pair, Integer> sgVmMappingPair = securityGroupVMMapDao.listByInstanceId(vmId, sf); Integer count = sgVmMappingPair.second(); if (count.intValue() == 0) { // handle empty result cases @@ -1499,7 +1621,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q List permittedAccounts = new ArrayList(); Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), cmd.isRecursive(), null); - _accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); Long domainId = domainIdRecursiveListProject.first(); Boolean isRecursive = domainIdRecursiveListProject.second(); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); @@ -1511,7 +1633,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q // number of // records with // pagination - _accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); sb.and("name", sb.entity().getInstanceName(), SearchCriteria.Op.EQ); sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); @@ -1559,7 +1681,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } SearchCriteria sc = sb.create(); - _accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); if (keyword != null) { SearchCriteria ssc = _routerJoinDao.createSearchCriteria(); @@ -1674,17 +1796,17 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sb.select(null, Func.DISTINCT, sb.entity().getId()); // select distinct // ids - if (_accountMgr.isAdmin(caller.getId())) { + if (accountMgr.isAdmin(caller.getId())) { if (domainId != null) { DomainVO domain = _domainDao.findById(domainId); if (domain == null) { throw new InvalidParameterValueException("Domain id=" + domainId + " doesn't exist in the system"); } - _accountMgr.checkAccess(caller, domain); + accountMgr.checkAccess(caller, domain); if (accountName != null) { - Account owner = _accountMgr.getActiveAccountByName(accountName, domainId); + Account owner = accountMgr.getActiveAccountByName(accountName, domainId); if (owner == null) { throw new InvalidParameterValueException("Unable to find account " + accountName + " in domain " + domainId); } @@ -1725,10 +1847,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q userId = user.getId(); } - if (domainId == null && accountId == null && (_accountMgr.isNormalUser(caller.getId()) || !listAll)) { + if (domainId == null && accountId == null && (accountMgr.isNormalUser(caller.getId()) || !listAll)) { accountId = caller.getId(); userId = user.getId(); - } else if (_accountMgr.isDomainAdmin(caller.getId()) || (isRecursive && !listAll)) { + } else if (accountMgr.isDomainAdmin(caller.getId()) || (isRecursive && !listAll)) { DomainVO domain = _domainDao.findById(caller.getDomainId()); path = domain.getPath(); } @@ -1839,14 +1961,14 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q List permittedAccounts = new ArrayList(); Ternary domainIdRecursiveListProject = new Ternary(domainId, isRecursive, null); - _accountMgr.buildACLSearchParameters(caller, id, accountName, projectId, permittedAccounts, domainIdRecursiveListProject, listAll, true); + accountMgr.buildACLSearchParameters(caller, id, accountName, projectId, permittedAccounts, domainIdRecursiveListProject, listAll, true); domainId = domainIdRecursiveListProject.first(); isRecursive = domainIdRecursiveListProject.second(); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); Filter searchFilter = new Filter(ProjectInvitationJoinVO.class, "id", true, startIndex, pageSizeVal); SearchBuilder sb = _projectInvitationJoinDao.createSearchBuilder(); - _accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); ProjectInvitation invitation = projectInvitationDao.findByUserIdProjectId(callingUser.getId(), callingUser.getAccountId(), projectId == null ? -1 : projectId); sb.and("projectId", sb.entity().getProjectId(), SearchCriteria.Op.EQ); sb.and("state", sb.entity().getState(), SearchCriteria.Op.EQ); @@ -1854,7 +1976,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); SearchCriteria sc = sb.create(); - _accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); if (projectId != null) { sc.setParameters("projectId", projectId); @@ -1917,7 +2039,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q // verify permissions - only accounts belonging to the project can list // project's account - if (!_accountMgr.isAdmin(caller.getId()) && _projectAccountDao.findByProjectIdUserId(projectId, callingUser.getAccountId(), callingUser.getId()) == null && + if (!accountMgr.isAdmin(caller.getId()) && _projectAccountDao.findByProjectIdUserId(projectId, callingUser.getAccountId(), callingUser.getId()) == null && _projectAccountDao.findByProjectIdAccountId(projectId, caller.getAccountId()) == null) { throw new PermissionDeniedException("Account " + caller + " is not authorized to list users of the project id=" + projectId); } @@ -1973,7 +2095,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q public Pair, Integer> searchForServersInternal(ListHostsCmd cmd) { - Long zoneId = _accountMgr.checkAccessAndSpecifyAuthority(CallContext.current().getCallingAccount(), cmd.getZoneId()); + Long zoneId = accountMgr.checkAccessAndSpecifyAuthority(CallContext.current().getCallingAccount(), cmd.getZoneId()); Object name = cmd.getHostName(); Object type = cmd.getType(); Object state = cmd.getState(); @@ -1991,7 +2113,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Filter searchFilter = new Filter(HostJoinVO.class, "id", Boolean.TRUE, startIndex, pageSize); - SearchBuilder sb = _hostJoinDao.createSearchBuilder(); + SearchBuilder sb = hostJoinDao.createSearchBuilder(); sb.select(null, Func.DISTINCT, sb.entity().getId()); // select distinct // ids sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); @@ -2021,7 +2143,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q SearchCriteria sc = sb.create(); if (keyword != null) { - SearchCriteria ssc = _hostJoinDao.createSearchCriteria(); + SearchCriteria ssc = hostJoinDao.createSearchCriteria(); ssc.addOr("name", SearchCriteria.Op.LIKE, "%" + keyword + "%"); ssc.addOr("status", SearchCriteria.Op.LIKE, "%" + keyword + "%"); ssc.addOr("type", SearchCriteria.Op.LIKE, "%" + keyword + "%"); @@ -2072,7 +2194,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sc.setParameters("hypervisor_type", hypervisorType); } // search host details by ids - Pair, Integer> uniqueHostPair = _hostJoinDao.searchAndCount(sc, searchFilter); + Pair, Integer> uniqueHostPair = hostJoinDao.searchAndCount(sc, searchFilter); Integer count = uniqueHostPair.second(); if (count.intValue() == 0) { // handle empty result cases @@ -2084,7 +2206,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q for (HostJoinVO v : uniqueHosts) { hostIds[i++] = v.getId(); } - List hosts = _hostJoinDao.searchByIds(hostIds); + List hosts = hostJoinDao.searchByIds(hostIds); return new Pair, Integer>(hosts, count); } @@ -2101,7 +2223,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q ResponseView respView = cmd.getResponseView(); Account account = CallContext.current().getCallingAccount(); - if (_accountMgr.isRootAdmin(account.getAccountId())) { + if (accountMgr.isRootAdmin(account.getAccountId())) { respView = ResponseView.Full; } @@ -2160,7 +2282,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q List ids = getIdsListFromCmd(cmd.getId(), cmd.getIds()); Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), cmd.isRecursive(), null); - _accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); Long domainId = domainIdRecursiveListProject.first(); Boolean isRecursive = domainIdRecursiveListProject.second(); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); @@ -2175,7 +2297,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q // number of // records with // pagination - _accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); @@ -2186,7 +2308,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sb.and("dataCenterId", sb.entity().getDataCenterId(), SearchCriteria.Op.EQ); sb.and("podId", sb.entity().getPodId(), SearchCriteria.Op.EQ); if (storageId != null) { - StoragePoolVO poolVO = _storagePoolDao.findByUuid(storageId); + StoragePoolVO poolVO = storagePoolDao.findByUuid(storageId); if (poolVO.getPoolType() == Storage.StoragePoolType.DatastoreCluster) { sb.and("storageId", sb.entity().getPoolUuid(), SearchCriteria.Op.IN); } else { @@ -2210,7 +2332,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q // now set the SC criteria... SearchCriteria sc = sb.create(); - _accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + accountMgr.buildACLViewSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); if (keyword != null) { SearchCriteria ssc = _volumeJoinDao.createSearchCriteria(); @@ -2269,9 +2391,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } if (storageId != null) { - StoragePoolVO poolVO = _storagePoolDao.findByUuid(storageId); + StoragePoolVO poolVO = storagePoolDao.findByUuid(storageId); if (poolVO.getPoolType() == Storage.StoragePoolType.DatastoreCluster) { - List childDatastores = _storagePoolDao.listChildStoragePoolsInDatastoreCluster(poolVO.getId()); + List childDatastores = storagePoolDao.listChildStoragePoolsInDatastoreCluster(poolVO.getId()); List childDatastoreIds = childDatastores.stream().map(mo -> mo.getUuid()).collect(Collectors.toList()); sc.setParameters("storageId", childDatastoreIds.toArray()); } else { @@ -2285,7 +2407,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q if (state != null) { sc.setParameters("state", state); - } else if (!_accountMgr.isAdmin(caller.getId())) { + } else if (!accountMgr.isAdmin(caller.getId())) { sc.setParameters("stateNEQ", Volume.State.Expunged); } @@ -2307,7 +2429,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } private boolean shouldListSystemVms(ListVolumesCmd cmd, Long callerId) { - return Boolean.TRUE.equals(cmd.getListSystemVms()) && _accountMgr.isRootAdmin(callerId); + return Boolean.TRUE.equals(cmd.getListSystemVms()) && accountMgr.isRootAdmin(callerId); } @Override @@ -2337,7 +2459,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q if (domain == null) { throw new InvalidParameterValueException("Domain id=" + domainId + " doesn't exist"); } - _accountMgr.checkAccess(caller, domain); + accountMgr.checkAccess(caller, domain); } else { if (caller.getType() != Account.Type.ADMIN) { domainId = caller.getDomainId(); @@ -2414,7 +2536,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q String accountName = cmd.getSearchName(); boolean isRecursive = cmd.isRecursive(); boolean listAll = cmd.listAll(); - boolean callerIsAdmin = _accountMgr.isAdmin(caller.getId()); + boolean callerIsAdmin = accountMgr.isAdmin(caller.getId()); Account account; Domain domain = null; @@ -2426,7 +2548,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q throw new InvalidParameterValueException("Domain id=" + domainId + " doesn't exist"); } // ... and check access rights. - _accountMgr.checkAccess(caller, domain); + accountMgr.checkAccess(caller, domain); } // if no "id" specified... @@ -2449,7 +2571,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q if (account == null || account.getId() == Account.ACCOUNT_ID_SYSTEM) { throw new InvalidParameterValueException("Unable to find account by name " + accountName + " in domain " + domainId); } - _accountMgr.checkAccess(caller, null, true, account); + accountMgr.checkAccess(caller, null, true, account); } else { // if they specified an "id"... if (domainId == null) { @@ -2460,7 +2582,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q if (account == null || account.getId() == Account.ACCOUNT_ID_SYSTEM) { throw new InvalidParameterValueException("Unable to find account by id " + accountId + (domainId == null ? "" : " in domain " + domainId)); } - _accountMgr.checkAccess(caller, null, true, account); + accountMgr.checkAccess(caller, null, true, account); } Filter searchFilter = new Filter(AccountJoinVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); @@ -2556,7 +2678,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q List permittedAccounts = new ArrayList(); Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), cmd.isRecursive(), null); - _accountMgr.buildACLSearchParameters(caller, null, cmd.getAccountName(), null, permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + accountMgr.buildACLSearchParameters(caller, null, cmd.getAccountName(), null, permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); Long domainId = domainIdRecursiveListProject.first(); Boolean isRecursive = domainIdRecursiveListProject.second(); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); @@ -2640,7 +2762,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Map caps = driver.getCapabilities(); if (Storage.StoragePoolType.NetworkFilesystem.toString().equals(poolResponse.getType()) && HypervisorType.VMware.toString().equals(poolResponse.getHypervisor())) { - StoragePoolVO pool = _storagePoolDao.findPoolByUUID(poolResponse.getId()); + StoragePoolVO pool = storagePoolDao.findPoolByUUID(poolResponse.getId()); StoragePoolDetailVO detail = _storagePoolDetailsDao.findDetail(pool.getId(), Storage.Capability.HARDWARE_ACCELERATION.toString()); if (detail != null) { caps.put(Storage.Capability.HARDWARE_ACCELERATION.toString(), detail.getValue()); @@ -2659,7 +2781,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q ScopeType scopeType = ScopeType.validateAndGetScopeType(cmd.getScope()); StoragePoolStatus status = StoragePoolStatus.validateAndGetStatus(cmd.getStatus()); - Long zoneId = _accountMgr.checkAccessAndSpecifyAuthority(CallContext.current().getCallingAccount(), cmd.getZoneId()); + Long zoneId = accountMgr.checkAccessAndSpecifyAuthority(CallContext.current().getCallingAccount(), cmd.getZoneId()); Long id = cmd.getId(); String name = cmd.getStoragePoolName(); String path = cmd.getPath(); @@ -2667,6 +2789,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Long cluster = cmd.getClusterId(); String address = cmd.getIpAddress(); String keyword = cmd.getKeyword(); + Long startIndex = cmd.getStartIndex(); Long pageSize = cmd.getPageSizeVal(); @@ -2786,7 +2909,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private Pair, Integer> searchForImageStoresInternal(ListImageStoresCmd cmd) { - Long zoneId = _accountMgr.checkAccessAndSpecifyAuthority(CallContext.current().getCallingAccount(), cmd.getZoneId()); + Long zoneId = accountMgr.checkAccessAndSpecifyAuthority(CallContext.current().getCallingAccount(), cmd.getZoneId()); Object id = cmd.getId(); Object name = cmd.getStoreName(); String provider = cmd.getProvider(); @@ -2870,7 +2993,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private Pair, Integer> searchForCacheStoresInternal(ListSecondaryStagingStoresCmd cmd) { - Long zoneId = _accountMgr.checkAccessAndSpecifyAuthority(CallContext.current().getCallingAccount(), cmd.getZoneId()); + Long zoneId = accountMgr.checkAccessAndSpecifyAuthority(CallContext.current().getCallingAccount(), cmd.getZoneId()); Object id = cmd.getId(); Object name = cmd.getStoreName(); String provider = cmd.getProvider(); @@ -2967,7 +3090,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Object id = cmd.getId(); Object keyword = cmd.getKeyword(); Long domainId = cmd.getDomainId(); - Boolean isRootAdmin = _accountMgr.isRootAdmin(account.getAccountId()); + Boolean isRootAdmin = accountMgr.isRootAdmin(account.getAccountId()); Boolean isRecursive = cmd.isRecursive(); Long zoneId = cmd.getZoneId(); Long volumeId = cmd.getVolumeId(); @@ -2977,7 +3100,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q // if a domainId is provided, we just return the disk offering // associated with this domain if (domainId != null) { - if (_accountMgr.isRootAdmin(account.getId()) || isPermissible(account.getDomainId(), domainId)) { + if (accountMgr.isRootAdmin(account.getId()) || isPermissible(account.getDomainId(), domainId)) { // check if the user's domain == do's domain || user's domain is // a child of so's domain for non-root users sc.addAnd("domainId", Op.FIND_IN_SET, String.valueOf(domainId)); @@ -2992,7 +3115,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q // For non-root users, only return all offerings for the user's domain, // and everything above till root - if ((_accountMgr.isNormalUser(account.getId()) || _accountMgr.isDomainAdmin(account.getId())) || account.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN) { + if ((accountMgr.isNormalUser(account.getId()) || accountMgr.isDomainAdmin(account.getId())) || account.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN) { if (isRecursive) { // domain + all sub-domains if (account.getType() == Account.Type.NORMAL) { throw new InvalidParameterValueException("Only ROOT admins and Domain admins can list disk offerings with isrecursive=true"); @@ -3154,14 +3277,14 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Boolean encryptRoot = cmd.getEncryptRoot(); SearchCriteria sc = _srvOfferingJoinDao.createSearchCriteria(); - if (!_accountMgr.isRootAdmin(caller.getId()) && isSystem) { + if (!accountMgr.isRootAdmin(caller.getId()) && isSystem) { throw new InvalidParameterValueException("Only ROOT admins can access system's offering"); } // Keeping this logic consistent with domain specific zones // if a domainId is provided, we just return the so associated with this // domain - if (domainId != null && !_accountMgr.isRootAdmin(caller.getId())) { + if (domainId != null && !accountMgr.isRootAdmin(caller.getId())) { // check if the user's domain == so's domain || user's domain is a // child of so's domain if (!isPermissible(caller.getDomainId(), domainId)) { @@ -3177,7 +3300,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q throw ex; } - _accountMgr.checkAccess(caller, null, true, vmInstance); + accountMgr.checkAccess(caller, null, true, vmInstance); currentVmOffering = _srvOfferingDao.findByIdIncludingRemoved(vmInstance.getId(), vmInstance.getServiceOfferingId()); if (! currentVmOffering.isDynamic()) { @@ -3202,8 +3325,8 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Integer vmMemory = currentVmOffering.getRamSize(); Integer vmSpeed = currentVmOffering.getSpeed(); if ((vmCpu == null || vmMemory == null || vmSpeed == null) && VirtualMachine.Type.User.equals(vmInstance.getType())) { - UserVmVO userVmVO = _userVmDao.findById(vmId); - _userVmDao.loadDetails(userVmVO); + UserVmVO userVmVO = userVmDao.findById(vmId); + userVmDao.loadDetails(userVmVO); Map details = userVmVO.getDetails(); vmCpu = NumbersUtil.parseInt(details.get(ApiConstants.CPU_NUMBER), 0); if (vmSpeed == null) { @@ -3225,7 +3348,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } // boolean includePublicOfferings = false; - if ((_accountMgr.isNormalUser(caller.getId()) || _accountMgr.isDomainAdmin(caller.getId())) || caller.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN) { + if ((accountMgr.isNormalUser(caller.getId()) || accountMgr.isDomainAdmin(caller.getId())) || caller.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN) { // For non-root users. if (isSystem) { throw new InvalidParameterValueException("Only root admins can access system's offering"); @@ -3411,7 +3534,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q SearchBuilder sb = _dcJoinDao.createSearchBuilder(); if (resourceTags != null && !resourceTags.isEmpty()) { - SearchBuilder tagSearch = _resourceTagDao.createSearchBuilder(); + SearchBuilder tagSearch = resourceTagDao.createSearchBuilder(); for (int count = 0; count < resourceTags.size(); count++) { tagSearch.or().op("key" + String.valueOf(count), tagSearch.entity().getKey(), SearchCriteria.Op.EQ); tagSearch.and("value" + String.valueOf(count), tagSearch.entity().getValue(), SearchCriteria.Op.EQ); @@ -3455,7 +3578,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q // only list zones associated // with this domain, private zone sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); - if (_accountMgr.isNormalUser(account.getId())) { + if (accountMgr.isNormalUser(account.getId())) { // accountId == null (zones dedicated to a domain) or // accountId = caller SearchCriteria sdc = _dcJoinDao.createSearchCriteria(); @@ -3465,7 +3588,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sc.addAnd("accountId", SearchCriteria.Op.SC, sdc); } - } else if (_accountMgr.isNormalUser(account.getId())) { + } else if (accountMgr.isNormalUser(account.getId())) { // it was decided to return all zones for the user's domain, and // everything above till root // list all zones belonging to this domain, and all of its @@ -3510,7 +3633,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sdc.addAnd("id", SearchCriteria.Op.NIN, dedicatedZoneIds.toArray(new Object[dedicatedZoneIds.size()])); } - } else if (_accountMgr.isDomainAdmin(account.getId()) || account.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN) { + } else if (accountMgr.isDomainAdmin(account.getId()) || account.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN) { // it was decided to return all zones for the domain admin, and // everything above till root, as well as zones till the domain // leaf @@ -3658,11 +3781,11 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q List permittedAccountIds = new ArrayList(); Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), cmd.isRecursive(), null); - _accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccountIds, domainIdRecursiveListProject, listAll, false); + accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccountIds, domainIdRecursiveListProject, listAll, false); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); List permittedAccounts = new ArrayList(); for (Long accountId : permittedAccountIds) { - permittedAccounts.add(_accountMgr.getAccount(accountId)); + permittedAccounts.add(accountMgr.getAccount(accountId)); } boolean showDomr = ((templateFilter != TemplateFilter.selfexecutable) && (templateFilter != TemplateFilter.featured)); @@ -3720,16 +3843,16 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q throw ex; } if (!template.isPublicTemplate() && caller.getType() == Account.Type.DOMAIN_ADMIN) { - Account template_acc = _accountMgr.getAccount(template.getAccountId()); + Account template_acc = accountMgr.getAccount(template.getAccountId()); DomainVO domain = _domainDao.findById(template_acc.getDomainId()); - _accountMgr.checkAccess(caller, domain); + accountMgr.checkAccess(caller, domain); } // if template is not public, perform permission check here else if (!template.isPublicTemplate() && caller.getType() != Account.Type.ADMIN) { - _accountMgr.checkAccess(caller, null, false, template); + accountMgr.checkAccess(caller, null, false, template); } else if (template.isPublicTemplate()) { - _accountMgr.checkAccess(caller, null, false, template); + accountMgr.checkAccess(caller, null, false, template); } // if templateId is specified, then we will just use the id to @@ -3782,7 +3905,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } // get all child domain ID's - if (_accountMgr.isAdmin(account.getId()) || publicTemplates) { + if (accountMgr.isAdmin(account.getId()) || publicTemplates) { List allChildDomains = _domainDao.findAllChildren(domainTreeNode.getPath(), domainTreeNode.getId()); for (DomainVO childDomain : allChildDomains) { relatedDomainIds.add(childDomain.getId()); @@ -4056,13 +4179,14 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q listAll = true; } + List permittedAccountIds = new ArrayList<>(); Ternary domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); - _accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccountIds, domainIdRecursiveListProject, listAll, false); + accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccountIds, domainIdRecursiveListProject, listAll, false); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); List permittedAccounts = new ArrayList<>(); for (Long accountId : permittedAccountIds) { - permittedAccounts.add(_accountMgr.getAccount(accountId)); + permittedAccounts.add(accountMgr.getAccount(accountId)); } HypervisorType hypervisorType = HypervisorType.getType(cmd.getHypervisor()); @@ -4170,18 +4294,18 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Account caller = CallContext.current().getCallingAccount(); if (vmId != null) { - UserVmVO userVM = _userVmDao.findById(vmId); + UserVmVO userVM = userVmDao.findById(vmId); if (userVM == null) { throw new InvalidParameterValueException("Unable to list affinity groups for virtual machine instance " + vmId + "; instance not found."); } - _accountMgr.checkAccess(caller, null, true, userVM); + accountMgr.checkAccess(caller, null, true, userVM); return listAffinityGroupsByVM(vmId.longValue(), startIndex, pageSize); } List permittedAccounts = new ArrayList(); Ternary ternary = new Ternary(domainId, isRecursive, null); - _accountMgr.buildACLSearchParameters(caller, affinityGroupId, accountName, projectId, permittedAccounts, ternary, listAll, false); + accountMgr.buildACLSearchParameters(caller, affinityGroupId, accountName, projectId, permittedAccounts, ternary, listAll, false); domainId = ternary.first(); isRecursive = ternary.second(); @@ -4568,7 +4692,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q List permittedAccountIds = new ArrayList<>(); Ternary domainIdRecursiveListProject = new Ternary(domainId, isRecursive, null); - _accountMgr.buildACLSearchParameters(caller, id, accountName, projectId, permittedAccountIds, domainIdRecursiveListProject, listAll, false); + accountMgr.buildACLSearchParameters(caller, id, accountName, projectId, permittedAccountIds, domainIdRecursiveListProject, listAll, false); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); domainId = domainIdRecursiveListProject.first(); isRecursive = domainIdRecursiveListProject.second(); @@ -4576,7 +4700,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q if (volumeId != null) { VolumeVO volume = volumeDao.findById(volumeId); if (volume != null) { - _accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, volume); + accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, volume); } } @@ -4586,7 +4710,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } else { sb.select(null, Func.DISTINCT, sb.entity().getSnapshotStorePair()); // select distinct (snapshotId, store_role, store_id) key } - _accountMgr.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccountIds, listProjectResourcesCriteria); + accountMgr.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccountIds, listProjectResourcesCriteria); sb.and("statusNEQ", sb.entity().getStatus(), SearchCriteria.Op.NEQ); //exclude those Destroyed snapshot, not showing on UI sb.and("volumeId", sb.entity().getVolumeId(), SearchCriteria.Op.EQ); sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); @@ -4598,7 +4722,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sb.and("locationType", sb.entity().getStoreRole(), SearchCriteria.Op.EQ); if (tags != null && !tags.isEmpty()) { - SearchBuilder tagSearch = _resourceTagDao.createSearchBuilder(); + SearchBuilder tagSearch = resourceTagDao.createSearchBuilder(); for (int count = 0; count < tags.size(); count++) { tagSearch.or().op("key" + String.valueOf(count), tagSearch.entity().getKey(), SearchCriteria.Op.EQ); tagSearch.and("value" + String.valueOf(count), tagSearch.entity().getValue(), SearchCriteria.Op.EQ); @@ -4610,7 +4734,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } SearchCriteria sc = sb.create(); - _accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccountIds, listProjectResourcesCriteria); + accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccountIds, listProjectResourcesCriteria); sc.setParameters("statusNEQ", Snapshot.State.Destroyed); From 6ae3b73ca2d7bf12acdd03333749b7a0a8cc94ee Mon Sep 17 00:00:00 2001 From: slavkap <51903378+slavkap@users.noreply.github.com> Date: Thu, 26 Oct 2023 09:46:14 +0300 Subject: [PATCH 10/13] Create snapshot from VM snapshot without memory for NFS/Local storage (#8117) --- .../main/java/com/cloud/storage/Snapshot.java | 2 +- .../motion/AncientDataMotionStrategy.java | 18 ++++++++- .../kvm/storage/KVMStorageProcessor.java | 40 ++++++++++++------- .../storage/snapshot/SnapshotManagerImpl.java | 40 +++++++++++++++++-- 4 files changed, 78 insertions(+), 22 deletions(-) diff --git a/api/src/main/java/com/cloud/storage/Snapshot.java b/api/src/main/java/com/cloud/storage/Snapshot.java index 5b25843f48b..fc919e442b2 100644 --- a/api/src/main/java/com/cloud/storage/Snapshot.java +++ b/api/src/main/java/com/cloud/storage/Snapshot.java @@ -26,7 +26,7 @@ import java.util.Date; public interface Snapshot extends ControlledEntity, Identity, InternalIdentity, StateObject { public enum Type { - MANUAL, RECURRING, TEMPLATE, HOURLY, DAILY, WEEKLY, MONTHLY, GROUP; + MANUAL, RECURRING, TEMPLATE, HOURLY, DAILY, WEEKLY, MONTHLY, GROUP, FROM_GROUP; private int max = 8; public void setMax(int max) { diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java index 1d463cad7ea..e450addb261 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java @@ -66,10 +66,13 @@ import com.cloud.configuration.Config; import com.cloud.host.Host; import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Snapshot.Type; +import com.cloud.storage.SnapshotVO; import com.cloud.storage.StorageManager; import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StoragePool; import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.utils.NumbersUtil; import com.cloud.utils.db.DB; @@ -97,6 +100,8 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { @Inject StorageManager storageManager; + @Inject + SnapshotDao snapshotDao; @Override public StrategyPriority canHandle(DataObject srcData, DataObject destData) { @@ -583,8 +588,8 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { fullSnapshot = snapshotFullBackup; } Map options = new HashMap(); - options.put("fullSnapshot", fullSnapshot.toString()); - options.put(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.key(), String.valueOf(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value())); + + addCommandOptions(snapshotInfo, fullSnapshot, options); boolean encryptionRequired = anyVolumeRequiresEncryption(srcData, destData); Answer answer = null; @@ -631,6 +636,15 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { } + private void addCommandOptions(SnapshotInfo snapshotInfo, Boolean fullSnapshot, Map options) { + SnapshotVO snap = snapshotDao.findById(snapshotInfo.getSnapshotId()); + if (snap != null && Type.FROM_GROUP.name().equals(snap.getTypeDescription())) { + options.put("typeDescription", snap.getTypeDescription()); + } + options.put("fullSnapshot", fullSnapshot.toString()); + options.put(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.key(), String.valueOf(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value())); + } + @Override public void copyAsync(Map volumeMap, VirtualMachineTO vmTo, Host srcHost, Host destHost, AsyncCompletionCallback callback) { CopyCommandResult result = new CopyCommandResult(null, null); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index ae0fa637bf0..f7ec09ca50f 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -1014,9 +1014,9 @@ public class KVMStorageProcessor implements StorageProcessor { command.add("-b", isCreatedFromVmSnapshot ? snapshotDisk.getPath() : snapshot.getPath()); command.add(NAME_OPTION, snapshotName); command.add("-p", snapshotDestPath); - if (isCreatedFromVmSnapshot) { - descName = UUID.randomUUID().toString(); - } + + descName = UUID.randomUUID().toString(); + command.add("-t", descName); final String result = command.execute(); if (result != null) { @@ -1041,18 +1041,7 @@ public class KVMStorageProcessor implements StorageProcessor { if (isCreatedFromVmSnapshot) { s_logger.debug("Ignoring removal of vm snapshot on primary as this snapshot is created from vm snapshot"); } else if (primaryPool.getType() != StoragePoolType.RBD) { - String snapshotPath = snapshot.getPath(); - String backupSnapshotAfterTakingSnapshot = cmd.getOptions() == null ? null : cmd.getOptions().get(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.key()); - - if (backupSnapshotAfterTakingSnapshot == null || BooleanUtils.toBoolean(backupSnapshotAfterTakingSnapshot)) { - try { - Files.deleteIfExists(Paths.get(snapshotPath)); - } catch (IOException ex) { - s_logger.error(String.format("Failed to delete snapshot [%s] on primary storage [%s].", snapshotPath, primaryPool.getUuid()), ex); - } - } else { - s_logger.debug(String.format("This backup is temporary, not deleting snapshot [%s] on primary storage [%s]", snapshotPath, primaryPool.getUuid())); - } + deleteSnapshotOnPrimary(cmd, snapshot, primaryPool); } try { @@ -1064,6 +1053,27 @@ public class KVMStorageProcessor implements StorageProcessor { } } } + + private void deleteSnapshotOnPrimary(final CopyCommand cmd, final SnapshotObjectTO snapshot, + KVMStoragePool primaryPool) { + String snapshotPath = snapshot.getPath(); + String backupSnapshotAfterTakingSnapshot = null; + boolean deleteSnapshotOnPrimary = true; + if (cmd.getOptions() != null) { + backupSnapshotAfterTakingSnapshot = cmd.getOptions().get(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.key()); + deleteSnapshotOnPrimary = cmd.getOptions().get("typeDescription") == null; + } + + if ((backupSnapshotAfterTakingSnapshot == null || BooleanUtils.toBoolean(backupSnapshotAfterTakingSnapshot)) && deleteSnapshotOnPrimary) { + try { + Files.deleteIfExists(Paths.get(snapshotPath)); + } catch (IOException ex) { + s_logger.error(String.format("Failed to delete snapshot [%s] on primary storage [%s].", snapshotPath, primaryPool.getUuid()), ex); + } + } else { + s_logger.debug(String.format("This backup is temporary, not deleting snapshot [%s] on primary storage [%s]", snapshotPath, primaryPool.getUuid())); + } + } protected synchronized void attachOrDetachISO(final Connect conn, final String vmName, String isoPath, final boolean isAttach, Map params) throws LibvirtException, InternalErrorException { DiskDef iso = new DiskDef(); diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index bd8811b2a15..aeb095b6ecf 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -109,6 +109,7 @@ import com.cloud.storage.SnapshotScheduleVO; import com.cloud.storage.SnapshotVO; import com.cloud.storage.Storage; import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; import com.cloud.storage.VMTemplateVO; @@ -150,8 +151,10 @@ import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.snapshot.VMSnapshot; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; @Component public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implements SnapshotManager, SnapshotApiService, Configurable { @@ -221,6 +224,10 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement protected SnapshotHelper snapshotHelper; @Inject DataCenterDao dataCenterDao; + @Inject + VMSnapshotDetailsDao vmSnapshotDetailsDao; + @Inject + SnapshotDataFactory snapshotDataFactory; private int _totalRetries; private int _pauseInterval; @@ -497,12 +504,12 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement SnapshotInfo snapshotInfo = this.snapshotFactory.getSnapshot(snapshotId, store); snapshotInfo = (SnapshotInfo)store.create(snapshotInfo); SnapshotDataStoreVO snapshotOnPrimaryStore = this._snapshotStoreDao.findByStoreSnapshot(store.getRole(), store.getId(), snapshot.getId()); - snapshotOnPrimaryStore.setState(ObjectInDataStoreStateMachine.State.Ready); - snapshotOnPrimaryStore.setInstallPath(vmSnapshot.getName()); - _snapshotStoreDao.update(snapshotOnPrimaryStore.getId(), snapshotOnPrimaryStore); + + StoragePoolVO storagePool = _storagePoolDao.findById(store.getId()); + updateSnapshotInfo(volumeId, vmSnapshotId, vmSnapshot, snapshot, snapshotOnPrimaryStore, storagePool); + snapshot.setState(Snapshot.State.CreatedOnPrimary); _snapshotDao.update(snapshot.getId(), snapshot); - snapshotInfo = this.snapshotFactory.getSnapshot(snapshotId, store); Long snapshotOwnerId = vm.getAccountId(); @@ -519,10 +526,35 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement _resourceLimitMgr.decrementResourceCount(snapshotOwnerId, ResourceType.snapshot); _resourceLimitMgr.decrementResourceCount(snapshotOwnerId, ResourceType.secondary_storage, new Long(volume.getSize())); throw new CloudRuntimeException("Failed to backup snapshot from vm snapshot", e); + } finally { + if (snapshotOnPrimaryStore != null) { + _snapshotStoreDao.remove(snapshotOnPrimaryStore.getId()); + } } return snapshotInfo; } + private void updateSnapshotInfo(Long volumeId, Long vmSnapshotId, VMSnapshotVO vmSnapshot, SnapshotVO snapshot, + SnapshotDataStoreVO snapshotOnPrimaryStore, StoragePoolVO storagePool) { + if ((storagePool.getPoolType() == StoragePoolType.NetworkFilesystem || storagePool.getPoolType() == StoragePoolType.Filesystem) && vmSnapshot.getType() == VMSnapshot.Type.Disk) { + List vmSnapshotDetails = vmSnapshotDetailsDao.findDetails(vmSnapshotId, "kvmStorageSnapshot"); + for (VMSnapshotDetailsVO vmSnapshotDetailsVO : vmSnapshotDetails) { + SnapshotInfo sInfo = snapshotDataFactory.getSnapshot(Long.parseLong(vmSnapshotDetailsVO.getValue()), DataStoreRole.Primary); + if (sInfo.getVolumeId() == volumeId) { + snapshotOnPrimaryStore.setState(ObjectInDataStoreStateMachine.State.Ready); + snapshotOnPrimaryStore.setInstallPath(sInfo.getPath()); + _snapshotStoreDao.update(snapshotOnPrimaryStore.getId(), snapshotOnPrimaryStore); + snapshot.setTypeDescription(Type.FROM_GROUP.name()); + snapshot.setSnapshotType((short)Type.FROM_GROUP.ordinal()); + } + } + } else { + snapshotOnPrimaryStore.setState(ObjectInDataStoreStateMachine.State.Ready); + snapshotOnPrimaryStore.setInstallPath(vmSnapshot.getName()); + _snapshotStoreDao.update(snapshotOnPrimaryStore.getId(), snapshotOnPrimaryStore); + } + } + @Override public SnapshotVO getParentSnapshot(VolumeInfo volume) { long preId = _snapshotDao.getLastSnapshot(volume.getId(), DataStoreRole.Primary); From a06f8a8763dc0d30b14e41d579ec73d40613e1d9 Mon Sep 17 00:00:00 2001 From: Vishesh Date: Thu, 26 Oct 2023 17:14:11 +0530 Subject: [PATCH 11/13] Fixup updateSnapshotInfo failure due to forward merging (#8150) Fixes build failures --- .../java/com/cloud/storage/snapshot/SnapshotManagerImpl.java | 2 +- .../com/cloud/storage/snapshot/SnapshotManagerImplTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index 7116359f9e4..940860dd04d 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -570,7 +570,7 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement if ((storagePool.getPoolType() == StoragePoolType.NetworkFilesystem || storagePool.getPoolType() == StoragePoolType.Filesystem) && vmSnapshot.getType() == VMSnapshot.Type.Disk) { List vmSnapshotDetails = vmSnapshotDetailsDao.findDetails(vmSnapshotId, "kvmStorageSnapshot"); for (VMSnapshotDetailsVO vmSnapshotDetailsVO : vmSnapshotDetails) { - SnapshotInfo sInfo = snapshotDataFactory.getSnapshot(Long.parseLong(vmSnapshotDetailsVO.getValue()), DataStoreRole.Primary); + SnapshotInfo sInfo = snapshotDataFactory.getSnapshot(Long.parseLong(vmSnapshotDetailsVO.getValue()), storagePool.getId(), DataStoreRole.Primary); if (sInfo.getVolumeId() == volumeId) { snapshotOnPrimaryStore.setState(ObjectInDataStoreStateMachine.State.Ready); snapshotOnPrimaryStore.setInstallPath(sInfo.getPath()); diff --git a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java index 1d7cf0a50d2..e6c2a0d0f3c 100644 --- a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java +++ b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java @@ -74,7 +74,7 @@ public class SnapshotManagerImplTest { @Mock SnapshotService snapshotService; @Mock - SnapshotDataFactory snapshotDataFactory; + SnapshotDataFactory snapshotFactory; @Mock ResourceLimitService resourceLimitService; @Mock @@ -269,7 +269,7 @@ public class SnapshotManagerImplTest { Mockito.when(store.getId()).thenReturn(storeId); Mockito.when(dataStoreManager.getDataStore(storeId, DataStoreRole.Image)).thenReturn(store); Mockito.when(snapshotStoreDao.listReadyBySnapshot(snapshotId, DataStoreRole.Image)).thenReturn(snapshotStoreList); - Mockito.when(snapshotDataFactory.getSnapshot(Mockito.anyLong(), Mockito.any())).thenReturn(Mockito.mock(SnapshotInfo.class)); + Mockito.when(snapshotFactory.getSnapshot(Mockito.anyLong(), Mockito.any())).thenReturn(Mockito.mock(SnapshotInfo.class)); CreateCmdResult result = Mockito.mock(CreateCmdResult.class); Mockito.when(result.isFailed()).thenReturn(false); Mockito.when(result.getPath()).thenReturn("SOMEPATH"); From bd52fa8a12a55d1c2ac484475cb2ed330742bb32 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 27 Oct 2023 10:23:00 +0200 Subject: [PATCH 12/13] New feature: VNF templates and appliances integration (#8022) --- api/src/main/java/com/cloud/network/VNF.java | 112 + .../java/com/cloud/server/ResourceTag.java | 1 + .../main/java/com/cloud/storage/Storage.java | 1 + .../apache/cloudstack/api/ApiConstants.java | 13 +- .../org/apache/cloudstack/api/BaseCmd.java | 3 + .../template/ListVnfTemplatesCmdByAdmin.java | 32 + .../template/RegisterTemplateCmdByAdmin.java | 4 +- .../RegisterVnfTemplateCmdByAdmin.java | 31 + .../template/UpdateTemplateCmdByAdmin.java | 3 +- .../template/UpdateVnfTemplateCmdByAdmin.java | 31 + .../vm/DeployVnfApplianceCmdByAdmin.java | 34 + .../user/template/DeleteVnfTemplateCmd.java | 47 + .../user/template/ListTemplatesCmd.java | 17 + .../user/template/ListVnfTemplatesCmd.java | 33 + .../user/template/RegisterTemplateCmd.java | 12 +- .../user/template/RegisterVnfTemplateCmd.java | 79 + .../user/template/UpdateTemplateCmd.java | 6 +- .../user/template/UpdateVnfTemplateCmd.java | 87 + .../user/vm/DeployVnfApplianceCmd.java | 74 + .../api/command/user/vm/ListVMsCmd.java | 8 + .../api/response/ChildTemplateResponse.java | 2 +- .../cloudstack/api/response/NicResponse.java | 16 + .../api/response/TemplateResponse.java | 2 +- .../api/response/UserVmResponse.java | 60 + .../api/response/VnfNicResponse.java | 119 + .../api/response/VnfTemplateResponse.java | 60 + .../storage/template/VnfTemplateManager.java | 52 + .../storage/template/VnfTemplateUtils.java | 152 + .../test/java/com/cloud/network/VNFTest.java | 58 + .../api/response/VnfNicResponseTest.java | 54 + .../api/response/VnfTemplateResponseTest.java | 47 + .../template/VnfTemplateUtilsTest.java | 261 ++ .../com/cloud/template/TemplateManager.java | 4 + .../cloud/storage/VnfTemplateDetailVO.java | 101 + .../com/cloud/storage/VnfTemplateNicVO.java | 101 + .../storage/dao/VnfTemplateDetailsDao.java | 26 + .../dao/VnfTemplateDetailsDaoImpl.java | 31 + .../cloud/storage/dao/VnfTemplateNicDao.java | 29 + .../storage/dao/VnfTemplateNicDaoImpl.java | 53 + .../src/main/java/com/cloud/vm/NicVO.java | 2 + ...spring-engine-schema-core-daos-context.xml | 2 + .../META-INF/db/schema-41810to41900.sql | 224 ++ .../storage/VnfTemplateDetailVOTest.java | 38 + .../cloud/storage/VnfTemplateNicVOTest.java | 46 + .../dao/VnfTemplateNicDaoImplTest.java | 88 + .../main/java/com/cloud/api/ApiDBUtils.java | 4 + .../com/cloud/api/query/QueryManagerImpl.java | 68 +- .../api/query/dao/TemplateJoinDaoImpl.java | 34 +- .../api/query/dao/UserVmJoinDaoImpl.java | 40 + .../com/cloud/api/query/vo/UserVmJoinVO.java | 8 + .../cloud/template/TemplateAdapterBase.java | 5 +- .../cloud/template/TemplateManagerImpl.java | 81 +- .../java/com/cloud/vm/UserVmManagerImpl.java | 31 +- .../template/VnfTemplateManagerImpl.java | 372 +++ .../spring-server-core-managers-context.xml | 1 + .../cloud/api/query/QueryManagerImplTest.java | 28 + .../query/dao/TemplateJoinDaoImplTest.java | 44 +- .../api/query/dao/UserVmJoinDaoImplTest.java | 102 + .../template/TemplateManagerImplTest.java | 115 + .../com/cloud/vm/UserVmManagerImplTest.java | 71 + .../template/VnfTemplateManagerImplTest.java | 389 +++ test/integration/smoke/test_vnf_templates.py | 341 ++ tools/apidoc/gen_toc.py | 1 + tools/marvin/marvin/lib/base.py | 312 ++ ui/public/locales/en.json | 51 + ui/src/components/view/DetailsTab.vue | 95 +- ui/src/components/view/ListView.vue | 14 +- ui/src/components/widgets/Console.vue | 2 +- ui/src/config/section/compute.js | 3 +- ui/src/config/section/image.js | 9 +- ui/src/config/section/network.js | 406 +++ ui/src/views/AutogenView.vue | 9 +- ui/src/views/compute/DeployVM.vue | 3 +- ui/src/views/compute/DeployVnfAppliance.vue | 2916 +++++++++++++++++ .../compute/wizard/NetworkConfiguration.vue | 7 + .../views/compute/wizard/NetworkSelection.vue | 6 +- .../views/compute/wizard/VnfNicsSelection.vue | 166 + .../views/image/RegisterOrUploadTemplate.vue | 53 +- ui/src/views/image/TemplateVnfSettings.vue | 880 +++++ ui/src/views/image/UpdateTemplate.vue | 2 +- ui/src/views/network/VnfAppliancesTab.vue | 159 + ui/src/views/network/VpcTab.vue | 5 + 82 files changed, 9026 insertions(+), 63 deletions(-) create mode 100644 api/src/main/java/com/cloud/network/VNF.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/template/ListVnfTemplatesCmdByAdmin.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterVnfTemplateCmdByAdmin.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateVnfTemplateCmdByAdmin.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVnfApplianceCmdByAdmin.java create mode 100755 api/src/main/java/org/apache/cloudstack/api/command/user/template/DeleteVnfTemplateCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/template/ListVnfTemplatesCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterVnfTemplateCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateVnfTemplateCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVnfApplianceCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/VnfNicResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/VnfTemplateResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManager.java create mode 100644 api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateUtils.java create mode 100644 api/src/test/java/com/cloud/network/VNFTest.java create mode 100644 api/src/test/java/org/apache/cloudstack/api/response/VnfNicResponseTest.java create mode 100644 api/src/test/java/org/apache/cloudstack/api/response/VnfTemplateResponseTest.java create mode 100644 api/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateUtilsTest.java create mode 100644 engine/schema/src/main/java/com/cloud/storage/VnfTemplateDetailVO.java create mode 100644 engine/schema/src/main/java/com/cloud/storage/VnfTemplateNicVO.java create mode 100644 engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDao.java create mode 100644 engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDaoImpl.java create mode 100644 engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDao.java create mode 100644 engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDaoImpl.java create mode 100755 engine/schema/src/test/java/com/cloud/storage/VnfTemplateDetailVOTest.java create mode 100755 engine/schema/src/test/java/com/cloud/storage/VnfTemplateNicVOTest.java create mode 100644 engine/schema/src/test/java/com/cloud/storage/dao/VnfTemplateNicDaoImplTest.java create mode 100644 server/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImpl.java create mode 100644 server/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImplTest.java create mode 100644 test/integration/smoke/test_vnf_templates.py create mode 100644 ui/src/views/compute/DeployVnfAppliance.vue create mode 100644 ui/src/views/compute/wizard/VnfNicsSelection.vue create mode 100644 ui/src/views/image/TemplateVnfSettings.vue create mode 100644 ui/src/views/network/VnfAppliancesTab.vue diff --git a/api/src/main/java/com/cloud/network/VNF.java b/api/src/main/java/com/cloud/network/VNF.java new file mode 100644 index 00000000000..ebc11c1f39b --- /dev/null +++ b/api/src/main/java/com/cloud/network/VNF.java @@ -0,0 +1,112 @@ +// 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 com.cloud.network; + +import org.apache.commons.lang3.StringUtils; + +public class VNF { + + public enum AccessMethod { + SSH_WITH_PASSWORD("ssh-password"), + SSH_WITH_KEY("ssh-key"), + HTTP("http"), + HTTPS("https"), + CONSOLE("console"); + + String _method; + + AccessMethod(String method) { + _method = method; + } + + @Override + public String toString() { + return _method; + } + + public static AccessMethod fromValue(String method) { + if (StringUtils.isBlank(method)) { + return null; + } else { + for (AccessMethod accessMethod : AccessMethod.values()) { + if (accessMethod.toString().equalsIgnoreCase(method)) { + return accessMethod; + } + } + } + return null; + } + } + + public enum AccessDetail { + ACCESS_METHODS, + USERNAME, + PASSWORD, + SSH_USER, + SSH_PASSWORD, + SSH_PORT, + WEB_USER, + WEB_PASSWORD, + HTTP_PATH, + HTTP_PORT, + HTTPS_PATH, + HTTPS_PORT + } + + public enum VnfDetail { + ICON, + VERSION, + VENDOR, + MAINTAINER + } + + public static class VnfNic { + long deviceId; + String name; + boolean required; + boolean management; + String description; + + public VnfNic(long deviceId, String nicName, boolean required, boolean management, String nicDescription) { + this.deviceId = deviceId; + this.name = nicName; + this.required = required; + this.management = management; + this.description = nicDescription; + } + + public long getDeviceId() { + return deviceId; + } + + public String getName() { + return name; + } + + public boolean isRequired() { + return required; + } + + public boolean isManagement() { + return management; + } + + public String getDescription() { + return description; + } + } +} diff --git a/api/src/main/java/com/cloud/server/ResourceTag.java b/api/src/main/java/com/cloud/server/ResourceTag.java index 6288446cbdd..89ec5b905c9 100644 --- a/api/src/main/java/com/cloud/server/ResourceTag.java +++ b/api/src/main/java/com/cloud/server/ResourceTag.java @@ -30,6 +30,7 @@ public interface ResourceTag extends ControlledEntity, Identity, InternalIdentit public enum ResourceObjectType { UserVm(true, true, true), Template(true, true, true), + VnfTemplate(true, true, true), ISO(true, false, true), Volume(true, true), Snapshot(true, false), diff --git a/api/src/main/java/com/cloud/storage/Storage.java b/api/src/main/java/com/cloud/storage/Storage.java index c6dee56fa22..1ee7200a313 100644 --- a/api/src/main/java/com/cloud/storage/Storage.java +++ b/api/src/main/java/com/cloud/storage/Storage.java @@ -125,6 +125,7 @@ public class Storage { BUILTIN, /* buildin template */ PERHOST, /* every host has this template, don't need to install it in secondary storage */ USER, /* User supplied template/iso */ + VNF, /* VNFs (virtual network functions) template */ DATADISK, /* Template corresponding to a datadisk(non root disk) present in an OVA */ ISODISK /* Template corresponding to a iso (non root disk) present in an OVA */ } 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 3fbd43b143a..c95e5c8bddb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -436,6 +436,7 @@ public class ApiConstants { public static final String TEMPLATE_ID = "templateid"; public static final String TEMPLATE_IDS = "templateids"; public static final String TEMPLATE_NAME = "templatename"; + public static final String TEMPLATE_TYPE = "templatetype"; public static final String TIMEOUT = "timeout"; public static final String TIMEZONE = "timezone"; public static final String TIMEZONEOFFSET = "timezoneoffset"; @@ -1013,7 +1014,6 @@ public class ApiConstants { public static final String DEPLOY_AS_IS = "deployasis"; public static final String DEPLOY_AS_IS_DETAILS = "deployasisdetails"; public static final String CROSS_ZONES = "crossZones"; - public static final String TEMPLATETYPE = "templatetype"; public static final String SOURCETEMPLATEID = "sourcetemplateid"; public static final String DYNAMIC_SCALING_ENABLED = "dynamicscalingenabled"; public static final String IOTHREADS_ENABLED = "iothreadsenabled"; @@ -1047,6 +1047,15 @@ public class ApiConstants { public static final String SOURCE_NAT_IP_ID = "sourcenatipaddressid"; public static final String HAS_RULES = "hasrules"; + public static final String MANAGEMENT = "management"; + public static final String IS_VNF = "isvnf"; + public static final String VNF_NICS = "vnfnics"; + public static final String VNF_DETAILS = "vnfdetails"; + public static final String CLEAN_UP_VNF_DETAILS = "cleanupvnfdetails"; + public static final String CLEAN_UP_VNF_NICS = "cleanupvnfnics"; + public static final String VNF_CONFIGURE_MANAGEMENT = "vnfconfiguremanagement"; + public static final String VNF_CIDR_LIST = "vnfcidrlist"; + /** * This enum specifies IO Drivers, each option controls specific policies on I/O. * Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0). @@ -1092,7 +1101,7 @@ public class ApiConstants { } public enum VMDetails { - all, group, nics, stats, secgrp, tmpl, servoff, diskoff, backoff, iso, volume, min, affgrp; + all, group, nics, stats, secgrp, tmpl, servoff, diskoff, backoff, iso, volume, min, affgrp, vnfnics; } public enum DomainDetails { diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java index 79e103a291d..0b80cfc8229 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java @@ -43,6 +43,7 @@ import org.apache.cloudstack.network.lb.ApplicationLoadBalancerService; import org.apache.cloudstack.network.lb.InternalLoadBalancerVMService; import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.storage.ImageStoreService; +import org.apache.cloudstack.storage.template.VnfTemplateManager; import org.apache.cloudstack.usage.UsageService; import org.apache.commons.collections.MapUtils; import org.apache.log4j.Logger; @@ -213,6 +214,8 @@ public abstract class BaseCmd { public ResourceIconManager resourceIconManager; @Inject public Ipv6Service ipv6Service; + @Inject + public VnfTemplateManager vnfTemplateManager; public abstract void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/ListVnfTemplatesCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/ListVnfTemplatesCmdByAdmin.java new file mode 100644 index 00000000000..f231036bbc6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/ListVnfTemplatesCmdByAdmin.java @@ -0,0 +1,32 @@ +// 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.admin.template; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.template.ListVnfTemplatesCmd; +import org.apache.cloudstack.api.response.TemplateResponse; + +import com.cloud.template.VirtualMachineTemplate; + +@APICommand(name = "listVnfTemplates", description = "List all public, private, and privileged VNF templates.", + responseObject = TemplateResponse.class, entityType = {VirtualMachineTemplate.class}, responseView = ResponseView.Full, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.19.0") +public class ListVnfTemplatesCmdByAdmin extends ListVnfTemplatesCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java index 28593755c11..91c0dd50e8e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java @@ -18,9 +18,11 @@ package org.apache.cloudstack.api.command.admin.template; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.admin.AdminCmd; import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; import org.apache.cloudstack.api.response.TemplateResponse; @APICommand(name = "registerTemplate", description = "Registers an existing template into the CloudStack cloud.", responseObject = TemplateResponse.class, responseView = ResponseView.Full, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class RegisterTemplateCmdByAdmin extends RegisterTemplateCmd {} +public class RegisterTemplateCmdByAdmin extends RegisterTemplateCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterVnfTemplateCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterVnfTemplateCmdByAdmin.java new file mode 100644 index 00000000000..45b40bfebb1 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterVnfTemplateCmdByAdmin.java @@ -0,0 +1,31 @@ +// 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.admin.template; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.response.TemplateResponse; + +@APICommand(name = "registerVnfTemplate", + description = "Registers an existing VNF template into the CloudStack cloud. ", + responseObject = TemplateResponse.class, responseView = ResponseView.Full, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.19.0") +public class RegisterVnfTemplateCmdByAdmin extends RegisterVnfTemplateCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateTemplateCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateTemplateCmdByAdmin.java index 09591c809b1..b1dfae3ed83 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateTemplateCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateTemplateCmdByAdmin.java @@ -24,4 +24,5 @@ import org.apache.cloudstack.api.response.TemplateResponse; @APICommand(name = "updateTemplate", description = "Updates attributes of a template.", responseObject = TemplateResponse.class, responseView = ResponseView.Full, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class UpdateTemplateCmdByAdmin extends UpdateTemplateCmd implements AdminCmd {} +public class UpdateTemplateCmdByAdmin extends UpdateTemplateCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateVnfTemplateCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateVnfTemplateCmdByAdmin.java new file mode 100644 index 00000000000..102a4703d4c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateVnfTemplateCmdByAdmin.java @@ -0,0 +1,31 @@ +// 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.admin.template; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; +import org.apache.cloudstack.api.response.TemplateResponse; + +@APICommand(name = "updateVnfTemplate", + description = "Updates a template to VNF template or attributes of a VNF template.", + responseObject = TemplateResponse.class, responseView = ResponseView.Full, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.19.0") +public class UpdateVnfTemplateCmdByAdmin extends UpdateVnfTemplateCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVnfApplianceCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVnfApplianceCmdByAdmin.java new file mode 100644 index 00000000000..5d2756c82fc --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVnfApplianceCmdByAdmin.java @@ -0,0 +1,34 @@ +// 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.admin.vm; + +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; +import org.apache.cloudstack.api.response.UserVmResponse; + +@APICommand(name = "deployVnfAppliance", + description = "Creates and automatically starts a VNF appliance based on a service offering, disk offering, and template.", + responseObject = UserVmResponse.class, + responseView = ResponseObject.ResponseView.Full, + entityType = {VirtualMachine.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, + since = "4.19.0") +public class DeployVnfApplianceCmdByAdmin extends DeployVnfApplianceCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/DeleteVnfTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/DeleteVnfTemplateCmd.java new file mode 100755 index 00000000000..c2c712cd2bb --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/DeleteVnfTemplateCmd.java @@ -0,0 +1,47 @@ +// 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.user.template; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.response.SuccessResponse; + +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.Account; + +@APICommand(name = "deleteVnfTemplate", + responseObject = SuccessResponse.class, + description = "Deletes a VNF template from the system. All virtual machines using the deleted template will not be affected.", + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.19.0") +public class DeleteVnfTemplateCmd extends DeleteTemplateCmd { + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + VirtualMachineTemplate template = _entityMgr.findById(VirtualMachineTemplate.class, getId()); + if (template != null) { + return template.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java index a64ce195c0f..26d79084531 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java @@ -96,6 +96,15 @@ public class ListTemplatesCmd extends BaseListTaggedResourcesCmd implements User description = "comma separated list of template details requested, value can be a list of [ all, min]") private List viewDetails; + @Parameter(name = ApiConstants.TEMPLATE_TYPE, type = CommandType.STRING, + description = "the type of the template", since = "4.19.0") + private String templateType; + + @Parameter(name = ApiConstants.IS_VNF, type = CommandType.BOOLEAN, + description = "flag to list VNF templates or not; true if need to list VNF templates, false otherwise.", + since = "4.19.0") + private Boolean isVnf; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -151,6 +160,10 @@ public class ListTemplatesCmd extends BaseListTaggedResourcesCmd implements User return parentTemplateId; } + public String getTemplateType() { + return templateType; + } + public boolean listInReadyState() { Account account = CallContext.current().getCallingAccount(); @@ -175,6 +188,10 @@ public class ListTemplatesCmd extends BaseListTaggedResourcesCmd implements User return showIcon != null ? showIcon : false; } + public Boolean getVnf() { + return isVnf; + } + @Override public String getCommandName() { return s_name; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListVnfTemplatesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListVnfTemplatesCmd.java new file mode 100644 index 00000000000..0a98a159527 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListVnfTemplatesCmd.java @@ -0,0 +1,33 @@ +// 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.user.template; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.TemplateResponse; + +import com.cloud.template.VirtualMachineTemplate; + +@APICommand(name = "listVnfTemplates", description = "List all public, private, and privileged VNF templates.", + responseObject = TemplateResponse.class, entityType = {VirtualMachineTemplate.class}, responseView = ResponseView.Restricted, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.19.0") +public class ListVnfTemplatesCmd extends ListTemplatesCmd implements UserCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java index 5709e3ed900..0a087888d52 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java @@ -143,6 +143,7 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { description = "true if template contains XS/VMWare tools inorder to support dynamic scaling of VM cpu/memory") protected Boolean isDynamicallyScalable; + @Deprecated @Parameter(name = ApiConstants.ROUTING, type = CommandType.BOOLEAN, description = "true if the template type is routing i.e., if template is used to deploy router") protected Boolean isRoutingType; @@ -168,6 +169,11 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { description = "(VMware only) true if VM deployments should preserve all the configurations defined for this template", since = "4.15.1") protected Boolean deployAsIs; + @Parameter(name = ApiConstants.TEMPLATE_TYPE, type = CommandType.STRING, + description = "the type of the template. Valid options are: USER/VNF (for all users) and SYSTEM/ROUTING/BUILTIN (for admins only).", + since = "4.19.0") + private String templateType; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -285,6 +291,10 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { Boolean.TRUE.equals(deployAsIs); } + public String getTemplateType() { + return templateType; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -315,7 +325,7 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { VirtualMachineTemplate template = _templateService.registerTemplate(this); if (template != null) { - ListResponse response = new ListResponse(); + ListResponse response = new ListResponse<>(); List templateResponses = _responseGenerator.createTemplateResponses(getResponseView(), template, getZoneIds(), false); response.setResponses(templateResponses); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterVnfTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterVnfTemplateCmd.java new file mode 100644 index 00000000000..c3e99d5791f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterVnfTemplateCmd.java @@ -0,0 +1,79 @@ +// 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.user.template; + +import java.util.List; +import java.util.Map; + +import com.cloud.network.VNF; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.TemplateResponse; +import org.apache.cloudstack.storage.template.VnfTemplateUtils; + +@APICommand(name = "registerVnfTemplate", + description = "Registers an existing VNF template into the CloudStack cloud. ", + responseObject = TemplateResponse.class, responseView = ResponseView.Restricted, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.19.0") +public class RegisterVnfTemplateCmd extends RegisterTemplateCmd implements UserCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VNF_NICS, + type = CommandType.MAP, + description = "VNF nics in key/value pairs using format vnfnics[i].keyname=keyvalue. " + + " Example: vnfnics[0].deviceid=0&&vnfnics[0].name=FirstNIC&&vnfnics[0].required=true" + + "&&vnfnics[1].deviceid=1&&vnfnics[1].name=SecondNIC") + protected Map vnfNics; + + @Parameter(name = ApiConstants.VNF_DETAILS, + type = CommandType.MAP, + description = "VNF details in key/value pairs using format vnfdetails[i].keyname=keyvalue. " + + "Example: vnfdetails[0].vendor=xxx&&vnfdetails[0].version=2.0") + protected Map vnfDetails; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public List getVnfNics() { + return VnfTemplateUtils.getVnfNicsList(this.vnfNics); + } + + public Map getVnfDetails() { + return convertDetailsToMap(vnfDetails); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + protected void validateParameters() { + super.validateParameters(); + + VnfTemplateUtils.validateApiCommandParams(this, null); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateTemplateCmd.java index 28ecd453d26..2afa6a98b13 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateTemplateCmd.java @@ -16,10 +16,11 @@ // under the License. package org.apache.cloudstack.api.command.user.template; -import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.log4j.Logger; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseUpdateTemplateOrIsoCmd; import org.apache.cloudstack.api.Parameter; @@ -41,7 +42,8 @@ public class UpdateTemplateCmd extends BaseUpdateTemplateOrIsoCmd implements Use //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = "templatetype", type = CommandType.STRING, description = "the type of the template") + @Parameter(name = ApiConstants.TEMPLATE_TYPE, type = CommandType.STRING, + description = "the type of the template. Valid options are: USER/VNF (for all users) and SYSTEM/ROUTING/BUILTIN (for admins only).") private String templateType; ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateVnfTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateVnfTemplateCmd.java new file mode 100644 index 00000000000..7479dd3be09 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateVnfTemplateCmd.java @@ -0,0 +1,87 @@ +// 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.user.template; + +import com.cloud.network.VNF; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.TemplateResponse; +import org.apache.cloudstack.storage.template.VnfTemplateUtils; + +import java.util.List; +import java.util.Map; + +@APICommand(name = "updateVnfTemplate", description = "Updates a template to VNF template or attributes of a VNF template.", + responseObject = TemplateResponse.class, responseView = ResponseView.Restricted, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.19.0") +public class UpdateVnfTemplateCmd extends UpdateTemplateCmd implements UserCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VNF_NICS, + type = CommandType.MAP, + description = "VNF nics in key/value pairs using format vnfnics[i].keyname=keyvalue. " + + " Example: vnfnics[0].deviceid=0&&vnfnics[0].name=FirstNIC&&vnfnics[0].required=true" + + "&&vnfnics[1].deviceid=1&&vnfnics[1].name=SecondNIC") + protected Map vnfNics; + + @Parameter(name = ApiConstants.VNF_DETAILS, + type = CommandType.MAP, + description = "VNF details in key/value pairs using format vnfdetails[i].keyname=keyvalue. " + + "Example: vnfdetails[0].vendor=xxx&&vnfdetails[0].version=2.0") + protected Map vnfDetails; + + @Parameter(name = ApiConstants.CLEAN_UP_VNF_DETAILS, + type = CommandType.BOOLEAN, + description = "optional boolean field, which indicates if VNF details will be cleaned up or not") + private Boolean cleanupVnfDetails = null; + + @Parameter(name = ApiConstants.CLEAN_UP_VNF_NICS, + type = CommandType.BOOLEAN, + description = "optional boolean field, which indicates if VNF nics will be cleaned up or not") + private Boolean cleanupVnfNics = null; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + + public List getVnfNics() { + return VnfTemplateUtils.getVnfNicsList(this.vnfNics); + } + + public Map getVnfDetails() { + return convertDetailsToMap(vnfDetails); + } + + public boolean isCleanupVnfDetails(){ + return cleanupVnfDetails == null ? false : cleanupVnfDetails.booleanValue(); + } + + public boolean isCleanupVnfNics(){ + return cleanupVnfNics == null ? false : cleanupVnfNics.booleanValue(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVnfApplianceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVnfApplianceCmd.java new file mode 100644 index 00000000000..4d50dd9c39b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVnfApplianceCmd.java @@ -0,0 +1,74 @@ +// 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.user.vm; + +import com.cloud.exception.ResourceAllocationException; +import com.cloud.utils.net.NetUtils; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.storage.template.VnfTemplateUtils; +import org.apache.commons.collections.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +@APICommand(name = "deployVnfAppliance", + description = "Creates and automatically starts a VNF appliance based on a service offering, disk offering, and template.", + responseObject = UserVmResponse.class, + responseView = ResponseObject.ResponseView.Restricted, + entityType = {VirtualMachine.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.19.0") +public class DeployVnfApplianceCmd extends DeployVMCmd implements UserCmd { + + @Parameter(name = ApiConstants.VNF_CONFIGURE_MANAGEMENT, type = CommandType.BOOLEAN, required = false, + description = "True by default, security group or network rules (source nat and firewall rules) will be configured for VNF management interfaces. False otherwise. " + + "Network rules are configured if management network is an isolated network or shared network with security groups.") + private Boolean vnfConfigureManagement; + + @Parameter(name = ApiConstants.VNF_CIDR_LIST, type = CommandType.LIST, collectionType = CommandType.STRING, + description = "the CIDR list to forward traffic from to the VNF management interface. Multiple entries must be separated by a single comma character (,). The default value is 0.0.0.0/0.") + private List vnfCidrlist; + + public Boolean getVnfConfigureManagement() { + return vnfConfigureManagement != null && vnfConfigureManagement; + } + + public List getVnfCidrlist() { + if (CollectionUtils.isNotEmpty(vnfCidrlist)) { + return vnfCidrlist; + } else { + List defaultCidrList = new ArrayList(); + defaultCidrList.add(NetUtils.ALL_IP4_CIDRS); + return defaultCidrList; + } + } + + @Override + public void create() throws ResourceAllocationException { + VnfTemplateUtils.validateVnfCidrList(this.getVnfCidrlist()); + + super.create(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java index 07d83b47d3c..9ec625cf3cf 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java @@ -189,6 +189,10 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements return zoneId; } + @Parameter(name = ApiConstants.IS_VNF, type = CommandType.BOOLEAN, + description = "flag to list vms created from VNF templates (as known as VNF appliances) or not; true if need to list VNF appliances, false otherwise.", + since = "4.19.0") + private Boolean isVnf; public Long getNetworkId() { return networkId; @@ -266,6 +270,10 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements return accumulate; } + public Boolean getVnf() { + return isVnf; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ChildTemplateResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ChildTemplateResponse.java index b036cd48e87..8f5b5de2919 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ChildTemplateResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ChildTemplateResponse.java @@ -39,7 +39,7 @@ public class ChildTemplateResponse extends BaseResponse { @Param(description = "the size of the template") private Integer size; - @SerializedName("templatetype") + @SerializedName(ApiConstants.TEMPLATE_TYPE) @Param(description = "the type of the template") private String templateType; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java index c267323cb7a..65e126de545 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java @@ -138,6 +138,14 @@ public class NicResponse extends BaseResponse { @Param(description = "MTU configured on the NIC", since="4.18.0") private Integer mtu; + @SerializedName(ApiConstants.PUBLIC_IP_ID) + @Param(description = "public IP address id associated with this nic via Static nat rule") + private String publicIpId; + + @SerializedName(ApiConstants.PUBLIC_IP) + @Param(description = "public IP address associated with this nic via Static nat rule") + private String publicIp; + public void setVmId(String vmId) { this.vmId = vmId; } @@ -400,4 +408,12 @@ public class NicResponse extends BaseResponse { public String getVpcId() { return vpcId; } + + public void setPublicIpId(String publicIpId) { + this.publicIpId = publicIpId; + } + + public void setPublicIp(String publicIp) { + this.publicIp = publicIp; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java index bd09d098708..3abd44941d9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java @@ -123,7 +123,7 @@ public class TemplateResponse extends BaseResponseWithTagInformation implements @Param(description = "the physical size of the template") private Long physicalSize; - @SerializedName(ApiConstants.TEMPLATETYPE) + @SerializedName(ApiConstants.TEMPLATE_TYPE) @Param(description = "the type of the template") private String templateType; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java index 114403da7bc..906529c13ab 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java @@ -16,9 +16,12 @@ // under the License. package org.apache.cloudstack.api.response; +import java.util.ArrayList; import java.util.Comparator; import java.util.Date; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -130,6 +133,10 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "the name of the template for the virtual machine") private String templateName; + @SerializedName(ApiConstants.TEMPLATE_TYPE) + @Param(description = "the type of the template for the virtual machine", since = "4.19.0") + private String templateType; + @SerializedName("templatedisplaytext") @Param(description = " an alternate display text of the template for the virtual machine") private String templateDisplayText; @@ -360,6 +367,14 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @SerializedName(ApiConstants.USER_DATA_DETAILS) @Param(description="list of variables and values for the variables declared in userdata", since = "4.18.0") private String userDataDetails; + @SerializedName(ApiConstants.VNF_NICS) + @Param(description = "NICs of the VNF appliance", since = "4.19.0") + private List vnfNics; + + @SerializedName(ApiConstants.VNF_DETAILS) + @Param(description = "VNF details", since = "4.19.0") + private Map vnfDetails; + public UserVmResponse() { securityGroupList = new LinkedHashSet<>(); nics = new TreeSet<>(Comparator.comparingInt(x -> Integer.parseInt(x.getDeviceId()))); @@ -1045,4 +1060,49 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co this.userDataDetails = userDataDetails; } + public Long getBytesReceived() { + return bytesReceived; + } + + public Long getBytesSent() { + return bytesSent; + } + + public String getTemplateType() { + return templateType; + } + + public void setTemplateType(String templateType) { + this.templateType = templateType; + } + + public List getVnfNics() { + return vnfNics; + } + + public void setVnfNics(List vnfNics) { + this.vnfNics = vnfNics; + } + + public Map getVnfDetails() { + return vnfDetails; + } + + public void setVnfDetails(Map vnfDetails) { + this.vnfDetails = vnfDetails; + } + + public void addVnfNic(VnfNicResponse vnfNic) { + if (this.vnfNics == null) { + this.vnfNics = new ArrayList<>(); + } + this.vnfNics.add(vnfNic); + } + + public void addVnfDetail(String key, String value) { + if (this.vnfDetails == null) { + this.vnfDetails = new LinkedHashMap<>(); + } + this.vnfDetails.put(key,value); + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VnfNicResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VnfNicResponse.java new file mode 100644 index 00000000000..af238c5a330 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/VnfNicResponse.java @@ -0,0 +1,119 @@ +// 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; + +@SuppressWarnings("unused") +public class VnfNicResponse { + @SerializedName(ApiConstants.DEVICE_ID) + @Param(description = "Device id of the NIC") + private long deviceId; + + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the NIC") + private String name; + + @SerializedName(ApiConstants.REQUIRED) + @Param(description = "True if the NIC is required. False if optional") + private Boolean required; + + @SerializedName(ApiConstants.MANAGEMENT) + @Param(description = "True if the NIC is a management interface. False otherwise") + private Boolean management; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "Description of the NIC") + private String description; + + @SerializedName(ApiConstants.NETWORK_ID) + @Param(description = "Network id of the NIC") + private String networkId; + + @SerializedName(ApiConstants.NETWORK_NAME) + @Param(description = "Network name of the NIC") + private String networkName; + + public void setDeviceId(long deviceId) { + this.deviceId = deviceId; + } + + public void setName(String name) { + this.name = name; + } + + public void setRequired(Boolean required) { + this.required = required; + } + + public void setManagement(Boolean management) { + this.management = management; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setNetworkId(String networkId) { + this.networkId = networkId; + } + + public void setNetworkName(String networkName) { + this.networkName = networkName; + } + + public VnfNicResponse() { + } + + public VnfNicResponse(long deviceId, String name, Boolean required, Boolean management, String description) { + this.deviceId = deviceId; + this.name = name; + this.required = required; + this.management = management; + this.description = description; + } + + public long getDeviceId() { + return deviceId; + } + + public String getName() { + return name; + } + + public Boolean isRequired() { + return required; + } + + public Boolean isManagement() { + return management; + } + + public String getDescription() { + return description; + } + + public String getNetworkId() { + return networkId; + } + + public String getNetworkName() { + return networkName; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VnfTemplateResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VnfTemplateResponse.java new file mode 100644 index 00000000000..5fd17efca88 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/VnfTemplateResponse.java @@ -0,0 +1,60 @@ +// 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 java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("unused") +public class VnfTemplateResponse extends TemplateResponse { + + @SerializedName(ApiConstants.VNF_NICS) + @Param(description = "NICs of the VNF template") + private List vnfNics; + + @SerializedName(ApiConstants.VNF_DETAILS) + @Param(description = "VNF details") + private Map vnfDetails; + + public void addVnfNic(VnfNicResponse vnfNic) { + if (this.vnfNics == null) { + this.vnfNics = new ArrayList<>(); + } + this.vnfNics.add(vnfNic); + } + + public void addVnfDetail(String key, String value) { + if (this.vnfDetails == null) { + this.vnfDetails = new LinkedHashMap<>(); + } + this.vnfDetails.put(key,value); + } + + public List getVnfNics() { + return vnfNics; + } + + public Map getVnfDetails() { + return vnfDetails; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManager.java b/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManager.java new file mode 100644 index 00000000000..6571346ad65 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManager.java @@ -0,0 +1,52 @@ +// 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.storage.template; + +import com.cloud.dc.DataCenter; +import com.cloud.exception.InsufficientAddressCapacityException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.security.SecurityGroup; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; +import org.apache.cloudstack.framework.config.ConfigKey; +import java.util.List; + +public interface VnfTemplateManager { + + ConfigKey VnfTemplateAndApplianceEnabled = new ConfigKey("Advanced", Boolean.class, + "vnf.template.appliance.enabled", + "true", + "Indicates whether the creation of VNF templates and VNF appliances is enabled or not.", + false); + + void persistVnfTemplate(long templateId, RegisterVnfTemplateCmd cmd); + + void updateVnfTemplate(long templateId, UpdateVnfTemplateCmd cmd); + + void validateVnfApplianceNics(VirtualMachineTemplate template, List networkIds); + + SecurityGroup createSecurityGroupForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, DeployVnfApplianceCmd cmd); + + void createIsolatedNetworkRulesForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, + UserVm vm, DeployVnfApplianceCmd cmd) + throws InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException; +} diff --git a/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateUtils.java b/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateUtils.java new file mode 100644 index 00000000000..e997a50cec0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateUtils.java @@ -0,0 +1,152 @@ +// 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.storage.template; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.VNF; +import com.cloud.storage.Storage; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.utils.net.NetUtils; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.command.user.template.DeleteVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class VnfTemplateUtils { + private VnfTemplateUtils() { + } + + public static List getVnfNicsList(Map vnfNics) { + List nicsList = new ArrayList<>(); + if (MapUtils.isNotEmpty(vnfNics)) { + Collection nicsCollection = vnfNics.values(); + Iterator iter = nicsCollection.iterator(); + while (iter.hasNext()) { + HashMap nicDetails = (HashMap)iter.next(); + String deviceIdString = nicDetails.get("deviceid"); + String name = nicDetails.get("name"); + String requiredString = nicDetails.get("required"); + String managementString = nicDetails.get("management"); + String description = nicDetails.get("description"); + Integer deviceId = null; + if (StringUtils.isAnyBlank(name, deviceIdString)) { + throw new InvalidParameterValueException("VNF nic name and deviceid cannot be null"); + } + try { + deviceId = Integer.parseInt(deviceIdString); + } catch (NumberFormatException e) { + throw new InvalidParameterValueException("Unable to parse VNF nic deviceId to Integer: " + deviceId); + } + boolean required = StringUtils.isBlank(requiredString) || Boolean.parseBoolean(requiredString); + boolean management = StringUtils.isBlank(managementString) || Boolean.parseBoolean(managementString); + nicsList.add(new VNF.VnfNic(deviceId, name, required, management, description)); + } + Collections.sort(nicsList, Comparator.comparing(VNF.VnfNic::getDeviceId)); + } + return nicsList; + } + + public static void validateApiCommandParams(Map vnfDetails, List vnfNics, String templateType) { + if (templateType != null && !Storage.TemplateType.VNF.name().equals(templateType)) { + throw new InvalidParameterValueException("The template type must be VNF for VNF templates."); + } + + if (vnfDetails != null) { + for (String vnfDetail : vnfDetails.keySet()) { + if (!EnumUtils.isValidEnumIgnoreCase(VNF.VnfDetail.class, vnfDetail) && + !EnumUtils.isValidEnumIgnoreCase(VNF.AccessDetail.class, vnfDetail)) { + throw new InvalidParameterValueException(String.format("Invalid VNF detail found: %s. Valid values are %s and %s", vnfDetail, + Arrays.stream(VNF.AccessDetail.values()).map(method -> method.toString()).collect(Collectors.joining(", ")), + Arrays.stream(VNF.VnfDetail.values()).map(method -> method.toString()).collect(Collectors.joining(", ")))); + } + if (vnfDetails.get(vnfDetail) == null) { + throw new InvalidParameterValueException("Empty value found for VNF detail: " + vnfDetail); + } + if (VNF.AccessDetail.ACCESS_METHODS.name().equalsIgnoreCase(vnfDetail)) { + String[] accessMethods = vnfDetails.get(vnfDetail).split(","); + for (String accessMethod : accessMethods) { + if (VNF.AccessMethod.fromValue(accessMethod.trim()) == null) { + throw new InvalidParameterValueException(String.format("Invalid VNF access method found: %s. Valid values are %s", accessMethod, + Arrays.stream(VNF.AccessMethod.values()).map(method -> method.toString()).sorted().collect(Collectors.joining(", ")))); + } + } + } + } + } + + validateVnfNics(vnfNics); + } + + public static void validateVnfNics(List nicsList) { + long deviceId = 0L; + boolean required = true; + for (VNF.VnfNic nic : nicsList) { + if (nic.getDeviceId() != deviceId) { + throw new InvalidParameterValueException(String.format("deviceid must be consecutive and start from 0. Nic deviceid should be %s but actual is %s.", deviceId, nic.getDeviceId())); + } + if (!required && nic.isRequired()) { + throw new InvalidParameterValueException(String.format("required cannot be true if a preceding nic is optional. Nic with deviceid %s should be required but actual is optional.", deviceId)); + } + deviceId ++; + required = nic.isRequired(); + } + } + + public static void validateApiCommandParams(BaseCmd cmd, VirtualMachineTemplate template) { + if (cmd instanceof RegisterVnfTemplateCmd) { + RegisterVnfTemplateCmd registerCmd = (RegisterVnfTemplateCmd) cmd; + validateApiCommandParams(registerCmd.getVnfDetails(), registerCmd.getVnfNics(), registerCmd.getTemplateType()); + } else if (cmd instanceof UpdateVnfTemplateCmd) { + UpdateVnfTemplateCmd updateCmd = (UpdateVnfTemplateCmd) cmd; + if (!Storage.TemplateType.VNF.equals(template.getTemplateType())) { + throw new InvalidParameterValueException(String.format("Cannot update as template %s is not a VNF template. The template type is %s.", updateCmd.getId(), template.getTemplateType())); + } + validateApiCommandParams(updateCmd.getVnfDetails(), updateCmd.getVnfNics(), updateCmd.getTemplateType()); + } else if (cmd instanceof DeleteVnfTemplateCmd) { + if (!Storage.TemplateType.VNF.equals(template.getTemplateType())) { + DeleteVnfTemplateCmd deleteCmd = (DeleteVnfTemplateCmd) cmd; + throw new InvalidParameterValueException(String.format("Cannot delete as Template %s is not a VNF template. The template type is %s.", deleteCmd.getId(), template.getTemplateType())); + } + } + } + + public static void validateVnfCidrList(List cidrList) { + if (CollectionUtils.isEmpty(cidrList)) { + return; + } + for (String cidr : cidrList) { + if (!NetUtils.isValidIp4Cidr(cidr)) { + throw new InvalidParameterValueException(String.format("Invalid cidr for VNF appliance: %s", cidr)); + } + } + } +} diff --git a/api/src/test/java/com/cloud/network/VNFTest.java b/api/src/test/java/com/cloud/network/VNFTest.java new file mode 100644 index 00000000000..d38e9cc1b93 --- /dev/null +++ b/api/src/test/java/com/cloud/network/VNFTest.java @@ -0,0 +1,58 @@ +// 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 com.cloud.network; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + + +@RunWith(MockitoJUnitRunner.class) +public class VNFTest { + + static long deviceId = 0L; + static String deviceName = "eth0"; + static boolean required = true; + static boolean management = false; + static String description = "description of vnf nic"; + + @Before + public void setUp() { + } + + @Test + public void testAccessMethods() { + Assert.assertEquals(VNF.AccessMethod.CONSOLE, VNF.AccessMethod.fromValue("console")); + Assert.assertEquals(VNF.AccessMethod.HTTP, VNF.AccessMethod.fromValue("http")); + Assert.assertEquals(VNF.AccessMethod.HTTPS, VNF.AccessMethod.fromValue("https")); + Assert.assertEquals(VNF.AccessMethod.SSH_WITH_KEY, VNF.AccessMethod.fromValue("ssh-key")); + Assert.assertEquals(VNF.AccessMethod.SSH_WITH_PASSWORD, VNF.AccessMethod.fromValue("ssh-password")); + } + + @Test + public void testVnfNic() { + VNF.VnfNic vnfNic = new VNF.VnfNic(deviceId, deviceName, required, management, description); + + Assert.assertEquals(deviceId, vnfNic.getDeviceId()); + Assert.assertEquals(deviceName, vnfNic.getName()); + Assert.assertEquals(required, vnfNic.isRequired()); + Assert.assertEquals(management, vnfNic.isManagement()); + Assert.assertEquals(description, vnfNic.getDescription()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/response/VnfNicResponseTest.java b/api/src/test/java/org/apache/cloudstack/api/response/VnfNicResponseTest.java new file mode 100644 index 00000000000..cdfbf451e86 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/response/VnfNicResponseTest.java @@ -0,0 +1,54 @@ +// 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 org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public final class VnfNicResponseTest { + + static long deviceId = 0L; + static String deviceName = "eth0"; + static boolean required = true; + static boolean management = false; + static String description = "description of vnf nic"; + + static String networkUuid = "networkuuid"; + static String networkName = "networkname"; + + @Test + public void testNewVnfNicResponse() { + final VnfNicResponse response = new VnfNicResponse(deviceId, deviceName, required, management, description); + Assert.assertEquals(deviceId, response.getDeviceId()); + Assert.assertEquals(deviceName, response.getName()); + Assert.assertEquals(required, response.isRequired()); + Assert.assertEquals(management, response.isManagement()); + Assert.assertEquals(description, response.getDescription()); + } + + @Test + public void testSetVnfNicResponse() { + final VnfNicResponse response = new VnfNicResponse(); + response.setNetworkId(networkUuid); + response.setNetworkName(networkName); + Assert.assertEquals(networkUuid, response.getNetworkId()); + Assert.assertEquals(networkName, response.getNetworkName()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/response/VnfTemplateResponseTest.java b/api/src/test/java/org/apache/cloudstack/api/response/VnfTemplateResponseTest.java new file mode 100644 index 00000000000..38875c07713 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/response/VnfTemplateResponseTest.java @@ -0,0 +1,47 @@ +// 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 org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public final class VnfTemplateResponseTest { + + @Test + public void testAddVnfNicToResponse() { + final VnfTemplateResponse response = new VnfTemplateResponse(); + + response.addVnfNic(new VnfNicResponse()); + response.addVnfNic(new VnfNicResponse()); + + Assert.assertEquals(2, response.getVnfNics().size()); + } + + @Test + public void testAddVnfDetailToResponse() { + final VnfTemplateResponse response = new VnfTemplateResponse(); + + response.addVnfDetail("key1", "value1"); + response.addVnfDetail("key2", "value2"); + response.addVnfDetail("key3", "value3"); + + Assert.assertEquals(3, response.getVnfDetails().size()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateUtilsTest.java b/api/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateUtilsTest.java new file mode 100644 index 00000000000..7ba8564faf4 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateUtilsTest.java @@ -0,0 +1,261 @@ +// 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.storage.template; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.VNF.VnfNic; +import com.cloud.storage.Storage; +import com.cloud.template.VirtualMachineTemplate; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class VnfTemplateUtilsTest { + + @Test + public void testGetVnfNicsListAllGood() { + final Map vnfNics = new HashMap<>(); + vnfNics.put("0", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "1"), + Map.entry("name", "eth1"), + Map.entry("required", "true"), + Map.entry("description", "The second NIC of VNF appliance") + ))); + vnfNics.put("1", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "2"), + Map.entry("name", "eth2"), + Map.entry("required", "false"), + Map.entry("description", "The third NIC of VNF appliance") + ))); + vnfNics.put("2", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "0"), + Map.entry("name", "eth0"), + Map.entry("description", "The first NIC of VNF appliance") + ))); + + Map vnfNicsMock = Mockito.mock(Map.class); + Mockito.when(vnfNicsMock.values()).thenReturn(vnfNics.values()); + + List nicsList = VnfTemplateUtils.getVnfNicsList(vnfNicsMock); + Mockito.verify(vnfNicsMock).values(); + + Assert.assertEquals(3, nicsList.size()); + Assert.assertEquals(0, nicsList.get(0).getDeviceId()); + Assert.assertEquals("eth0", nicsList.get(0).getName()); + Assert.assertTrue(nicsList.get(0).isRequired()); + Assert.assertEquals(1, nicsList.get(1).getDeviceId()); + Assert.assertEquals("eth1", nicsList.get(1).getName()); + Assert.assertTrue(nicsList.get(1).isRequired()); + Assert.assertEquals(2, nicsList.get(2).getDeviceId()); + Assert.assertEquals("eth2", nicsList.get(2).getName()); + Assert.assertFalse(nicsList.get(2).isRequired()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetVnfNicsListWithEmptyName() { + final Map vnfNics = new HashMap<>(); + vnfNics.put("0", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "1"), + Map.entry("required", "true"), + Map.entry("description", "The second NIC of VNF appliance") + ))); + + Map vnfNicsMock = Mockito.mock(Map.class); + Mockito.when(vnfNicsMock.values()).thenReturn(vnfNics.values()); + + List nicsList = VnfTemplateUtils.getVnfNicsList(vnfNicsMock); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetVnfNicsListWithEmptyDeviceId() { + final Map vnfNics = new HashMap<>(); + vnfNics.put("0", new HashMap<>(Map.ofEntries( + Map.entry("name", "eth1"), + Map.entry("required", "true"), + Map.entry("description", "The second NIC of VNF appliance") + ))); + + Map vnfNicsMock = Mockito.mock(Map.class); + Mockito.when(vnfNicsMock.values()).thenReturn(vnfNics.values()); + + List nicsList = VnfTemplateUtils.getVnfNicsList(vnfNicsMock); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetVnfNicsListWithInvalidDeviceId() { + final Map vnfNics = new HashMap<>(); + vnfNics.put("0", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "invalid"), + Map.entry("name", "eth1"), + Map.entry("required", "true"), + Map.entry("description", "The second NIC of VNF appliance") + ))); + + Map vnfNicsMock = Mockito.mock(Map.class); + Mockito.when(vnfNicsMock.values()).thenReturn(vnfNics.values()); + + List nicsList = VnfTemplateUtils.getVnfNicsList(vnfNicsMock); + } + + @Test + public void testValidateVnfNicsAllGood() { + VnfNic nic1 = Mockito.mock(VnfNic.class); + Mockito.when(nic1.getDeviceId()).thenReturn(0L); + Mockito.when(nic1.isRequired()).thenReturn(true); + + VnfNic nic2 = Mockito.mock(VnfNic.class); + Mockito.when(nic2.getDeviceId()).thenReturn(1L); + Mockito.when(nic2.isRequired()).thenReturn(true); + + VnfNic nic3 = Mockito.mock(VnfNic.class); + Mockito.when(nic3.getDeviceId()).thenReturn(2L); + Mockito.when(nic3.isRequired()).thenReturn(false); + + List nicsList = Arrays.asList(nic1, nic2, nic3); + + VnfTemplateUtils.validateVnfNics(nicsList); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfNicsStartWithNonzero() { + VnfNic nic1 = Mockito.mock(VnfNic.class); + Mockito.when(nic1.getDeviceId()).thenReturn(1L); + + VnfNic nic2 = Mockito.mock(VnfNic.class); + + VnfNic nic3 = Mockito.mock(VnfNic.class); + + List nicsList = Arrays.asList(nic1, nic2, nic3); + + VnfTemplateUtils.validateVnfNics(nicsList); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfNicsWithNonConstantDeviceIds() { + VnfNic nic1 = Mockito.mock(VnfNic.class); + Mockito.when(nic1.getDeviceId()).thenReturn(0L); + Mockito.when(nic1.isRequired()).thenReturn(true); + + VnfNic nic2 = Mockito.mock(VnfNic.class); + Mockito.when(nic2.getDeviceId()).thenReturn(2L); + + VnfNic nic3 = Mockito.mock(VnfNic.class); + + List nicsList = Arrays.asList(nic1, nic2, nic3); + + VnfTemplateUtils.validateVnfNics(nicsList); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfNicsWithInvalidRequired() { + VnfNic nic1 = Mockito.mock(VnfNic.class); + Mockito.when(nic1.getDeviceId()).thenReturn(0L); + Mockito.when(nic1.isRequired()).thenReturn(true); + + VnfNic nic2 = Mockito.mock(VnfNic.class); + Mockito.when(nic2.getDeviceId()).thenReturn(1L); + Mockito.when(nic2.isRequired()).thenReturn(false); + + VnfNic nic3 = Mockito.mock(VnfNic.class); + Mockito.when(nic3.getDeviceId()).thenReturn(2L); + Mockito.when(nic3.isRequired()).thenReturn(true); + + List nicsList = Arrays.asList(nic1, nic2, nic3); + + VnfTemplateUtils.validateVnfNics(nicsList); + } + + @Test + public void testValidateApiCommandParamsAllGood() { + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + RegisterVnfTemplateCmd cmd = Mockito.mock(RegisterVnfTemplateCmd.class); + Map vnfDetails = Mockito.spy(new HashMap<>()); + vnfDetails.put("username", "admin"); + vnfDetails.put("password", "password"); + vnfDetails.put("version", "4.19.0"); + vnfDetails.put("vendor", "cloudstack"); + Mockito.when(cmd.getVnfDetails()).thenReturn(vnfDetails); + + VnfTemplateUtils.validateApiCommandParams(cmd, template); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateApiCommandParamsInvalidAccessMethods() { + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + Mockito.when(template.getTemplateType()).thenReturn(Storage.TemplateType.VNF); + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + Map vnfDetails = Mockito.spy(new HashMap<>()); + vnfDetails.put("access_methods", "invalid"); + Mockito.when(cmd.getVnfDetails()).thenReturn(vnfDetails); + + VnfTemplateUtils.validateApiCommandParams(cmd, template); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateApiCommandParamsInvalidAccessDetails() { + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + Mockito.when(template.getTemplateType()).thenReturn(Storage.TemplateType.VNF); + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + Map vnfDetails = Mockito.spy(new HashMap<>()); + vnfDetails.put("invalid", "value"); + Mockito.when(cmd.getVnfDetails()).thenReturn(vnfDetails); + + VnfTemplateUtils.validateApiCommandParams(cmd, template); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateApiCommandParamsInvalidTemplateType() { + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + Mockito.when(template.getTemplateType()).thenReturn(Storage.TemplateType.USER); + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + + VnfTemplateUtils.validateApiCommandParams(cmd, template); + } + + @Test + public void testValidateVnfCidrList() { + List cidrList = new ArrayList<>(); + cidrList.add("10.10.10.0/24"); + VnfTemplateUtils.validateVnfCidrList(cidrList); + } + + @Test + public void testValidateVnfCidrListWithEmptyList() { + List cidrList = new ArrayList<>(); + VnfTemplateUtils.validateVnfCidrList(cidrList); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfCidrListwithInvalidList() { + List cidrList = new ArrayList<>(); + cidrList.add("10.10.10.0/24"); + cidrList.add("10.10.10.0/33"); + VnfTemplateUtils.validateVnfCidrList(cidrList); + } +} diff --git a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java index a3d9c1b79f1..2c129dfd6a5 100644 --- a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java +++ b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java @@ -18,6 +18,7 @@ package com.cloud.template; import java.util.List; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo; import org.apache.cloudstack.framework.config.ConfigKey; @@ -29,6 +30,7 @@ import com.cloud.deploy.DeployDestination; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.StorageUnavailableException; import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Storage.TemplateType; import com.cloud.storage.StoragePool; import com.cloud.storage.VMTemplateStoragePoolVO; import com.cloud.storage.VMTemplateVO; @@ -133,5 +135,7 @@ public interface TemplateManager { public static final String MESSAGE_REGISTER_PUBLIC_TEMPLATE_EVENT = "Message.RegisterPublicTemplate.Event"; public static final String MESSAGE_RESET_TEMPLATE_PERMISSION_EVENT = "Message.ResetTemplatePermission.Event"; + TemplateType validateTemplateType(BaseCmd cmd, boolean isAdmin, boolean isCrossZones); + List getTemplateDisksOnImageStore(Long templateId, DataStoreRole role, String configurationId); } diff --git a/engine/schema/src/main/java/com/cloud/storage/VnfTemplateDetailVO.java b/engine/schema/src/main/java/com/cloud/storage/VnfTemplateDetailVO.java new file mode 100644 index 00000000000..24d8191fa04 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/VnfTemplateDetailVO.java @@ -0,0 +1,101 @@ +// 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 com.cloud.storage; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; + +import org.apache.cloudstack.api.ResourceDetail; + +@Entity +@Table(name = "vnf_template_details") +public class VnfTemplateDetailVO implements ResourceDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "template_id") + private long resourceId; + + @Column(name = "name") + private String name; + + @Lob + @Column(name = "value", length = 65535) + private String value; + + @Column(name = "display") + private boolean display = true; + + public VnfTemplateDetailVO() { + } + + public VnfTemplateDetailVO(long templateId, String name, String value, boolean display) { + this.resourceId = templateId; + this.name = name; + this.value = value; + this.display = display; + } + + @Override + public long getId() { + return id; + } + + @Override + public long getResourceId() { + return resourceId; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public boolean isDisplay() { + return display; + } + + public void setId(long id) { + this.id = id; + } + + public void setResourceId(long resourceId) { + this.resourceId = resourceId; + } + + public void setName(String name) { + this.name = name; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/engine/schema/src/main/java/com/cloud/storage/VnfTemplateNicVO.java b/engine/schema/src/main/java/com/cloud/storage/VnfTemplateNicVO.java new file mode 100644 index 00000000000..1f5054c0cd8 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/VnfTemplateNicVO.java @@ -0,0 +1,101 @@ +// 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 com.cloud.storage; + +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "vnf_template_nics") +public class VnfTemplateNicVO implements InternalIdentity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "template_id") + private long templateId; + + @Column(name = "device_id") + private long deviceId; + + @Column(name = "device_name") + private String deviceName; + + @Column(name = "required") + private boolean required = true; + + @Column(name = "management") + private boolean management = true; + + @Column(name = "description") + private String description; + + public VnfTemplateNicVO() { + } + + public VnfTemplateNicVO(long templateId, long deviceId, String deviceName, boolean required, boolean management, String description) { + this.templateId = templateId; + this.deviceId = deviceId; + this.deviceName = deviceName; + this.required = required; + this.management = management; + this.description = description; + } + + @Override + public String toString() { + return String.format("Template %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "templateId", "deviceId", "required")); + } + + @Override + public long getId() { + return id; + } + + public long getTemplateId() { + return templateId; + } + + public long getDeviceId() { + return deviceId; + } + + public String getDeviceName() { + return deviceName; + } + + public boolean isRequired() { + return required; + } + + public boolean isManagement() { + return management; + } + + public String getDescription() { + return description; + } +} diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDao.java new file mode 100644 index 00000000000..c492240bf0f --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDao.java @@ -0,0 +1,26 @@ +// 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 com.cloud.storage.dao; + +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.utils.db.GenericDao; + +import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; + +public interface VnfTemplateDetailsDao extends GenericDao, ResourceDetailsDao { + +} diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDaoImpl.java new file mode 100644 index 00000000000..a4cbfa09fe6 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDaoImpl.java @@ -0,0 +1,31 @@ +// 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 com.cloud.storage.dao; + +import com.cloud.storage.VnfTemplateDetailVO; + +import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; +import org.springframework.stereotype.Component; + +@Component +public class VnfTemplateDetailsDaoImpl extends ResourceDetailsDaoBase implements VnfTemplateDetailsDao { + + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + super.addDetail(new VnfTemplateDetailVO(resourceId, key, value, display)); + } +} diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDao.java new file mode 100644 index 00000000000..b076f14abc9 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDao.java @@ -0,0 +1,29 @@ +// 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 com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.utils.db.GenericDao; + +public interface VnfTemplateNicDao extends GenericDao { + + List listByTemplateId(long templateId); + + void deleteByTemplateId(long templateId); +} diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDaoImpl.java new file mode 100644 index 00000000000..990ef446b47 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDaoImpl.java @@ -0,0 +1,53 @@ +// 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 com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.TransactionLegacy; + +public class VnfTemplateNicDaoImpl extends GenericDaoBase implements VnfTemplateNicDao { + + protected SearchBuilder TemplateSearch; + + public VnfTemplateNicDaoImpl() { + TemplateSearch = createSearchBuilder(); + TemplateSearch.and("templateId", TemplateSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateSearch.done(); + } + + @Override + public List listByTemplateId(long templateId) { + SearchCriteria sc = TemplateSearch.create(); + sc.setParameters("templateId", templateId); + return listBy(sc); + } + + @Override + public void deleteByTemplateId(long templateId) { + SearchCriteria sc = TemplateSearch.create(); + sc.setParameters("templateId", templateId); + TransactionLegacy txn = TransactionLegacy.currentTxn(); + txn.start(); + remove(sc); + txn.commit(); + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/NicVO.java b/engine/schema/src/main/java/com/cloud/vm/NicVO.java index fba7c966c44..a32a943ea58 100644 --- a/engine/schema/src/main/java/com/cloud/vm/NicVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/NicVO.java @@ -333,6 +333,8 @@ public class NicVO implements Nic { .append("-") .append(instanceId) .append("-") + .append(deviceId) + .append("-") .append(reservationId) .append("-") .append(iPv4Address) diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 647be81098c..2a0597ce9b5 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -278,6 +278,8 @@ + + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql index c14730adb86..88ff76f0c8c 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql @@ -184,6 +184,230 @@ ALTER TABLE `cloud`.`kubernetes_cluster` MODIFY COLUMN `kubernetes_version_id` b -- Set removed state for all removed accounts UPDATE `cloud`.`account` SET state='removed' WHERE `removed` IS NOT NULL; + +-- New tables for VNF +CREATE TABLE IF NOT EXISTS `cloud`.`vnf_template_nics` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `template_id` bigint unsigned NOT NULL COMMENT 'id of the VNF template', + `device_id` bigint unsigned NOT NULL COMMENT 'Device id of the NIC when plugged into the VNF appliances', + `device_name` varchar(1024) NOT NULL COMMENT 'Name of the NIC', + `required` tinyint NOT NULL DEFAULT '1' COMMENT 'True if the NIC is required. False if optional', + `management` tinyint NOT NULL DEFAULT '1' COMMENT 'True if the NIC is a management interface', + `description` varchar(1024) COMMENT 'Description of the NIC', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_template_id_device_id` (`template_id`, `device_id`), + KEY `fk_vnf_template_nics__template_id` (`template_id`), + CONSTRAINT `fk_vnf_template_nics__template_id` FOREIGN KEY (`template_id`) REFERENCES `vm_template` (`id`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `cloud`.`vnf_template_details` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `template_id` bigint unsigned NOT NULL COMMENT 'id of the VNF template', + `name` varchar(255) NOT NULL, + `value` varchar(1024) NOT NULL, + `display` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'True if the detail can be displayed to the end user', + PRIMARY KEY (`id`), + KEY `fk_vnf_template_details__template_id` (`template_id`), + CONSTRAINT `fk_vnf_template_details__template_id` FOREIGN KEY (`template_id`) REFERENCES `vm_template` (`id`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP VIEW IF EXISTS `cloud`.`user_vm_view`; +CREATE + VIEW `user_vm_view` AS +SELECT + `vm_instance`.`id` AS `id`, + `vm_instance`.`name` AS `name`, + `user_vm`.`display_name` AS `display_name`, + `user_vm`.`user_data` AS `user_data`, + `account`.`id` AS `account_id`, + `account`.`uuid` AS `account_uuid`, + `account`.`account_name` AS `account_name`, + `account`.`type` AS `account_type`, + `domain`.`id` AS `domain_id`, + `domain`.`uuid` AS `domain_uuid`, + `domain`.`name` AS `domain_name`, + `domain`.`path` AS `domain_path`, + `projects`.`id` AS `project_id`, + `projects`.`uuid` AS `project_uuid`, + `projects`.`name` AS `project_name`, + `instance_group`.`id` AS `instance_group_id`, + `instance_group`.`uuid` AS `instance_group_uuid`, + `instance_group`.`name` AS `instance_group_name`, + `vm_instance`.`uuid` AS `uuid`, + `vm_instance`.`user_id` AS `user_id`, + `vm_instance`.`last_host_id` AS `last_host_id`, + `vm_instance`.`vm_type` AS `type`, + `vm_instance`.`limit_cpu_use` AS `limit_cpu_use`, + `vm_instance`.`created` AS `created`, + `vm_instance`.`state` AS `state`, + `vm_instance`.`update_time` AS `update_time`, + `vm_instance`.`removed` AS `removed`, + `vm_instance`.`ha_enabled` AS `ha_enabled`, + `vm_instance`.`hypervisor_type` AS `hypervisor_type`, + `vm_instance`.`instance_name` AS `instance_name`, + `vm_instance`.`guest_os_id` AS `guest_os_id`, + `vm_instance`.`display_vm` AS `display_vm`, + `guest_os`.`uuid` AS `guest_os_uuid`, + `vm_instance`.`pod_id` AS `pod_id`, + `host_pod_ref`.`uuid` AS `pod_uuid`, + `vm_instance`.`private_ip_address` AS `private_ip_address`, + `vm_instance`.`private_mac_address` AS `private_mac_address`, + `vm_instance`.`vm_type` AS `vm_type`, + `data_center`.`id` AS `data_center_id`, + `data_center`.`uuid` AS `data_center_uuid`, + `data_center`.`name` AS `data_center_name`, + `data_center`.`is_security_group_enabled` AS `security_group_enabled`, + `data_center`.`networktype` AS `data_center_network_type`, + `host`.`id` AS `host_id`, + `host`.`uuid` AS `host_uuid`, + `host`.`name` AS `host_name`, + `host`.`cluster_id` AS `cluster_id`, + `host`.`status` AS `host_status`, + `host`.`resource_state` AS `host_resource_state`, + `vm_template`.`id` AS `template_id`, + `vm_template`.`uuid` AS `template_uuid`, + `vm_template`.`name` AS `template_name`, + `vm_template`.`type` AS `template_type`, + `vm_template`.`display_text` AS `template_display_text`, + `vm_template`.`enable_password` AS `password_enabled`, + `iso`.`id` AS `iso_id`, + `iso`.`uuid` AS `iso_uuid`, + `iso`.`name` AS `iso_name`, + `iso`.`display_text` AS `iso_display_text`, + `service_offering`.`id` AS `service_offering_id`, + `service_offering`.`uuid` AS `service_offering_uuid`, + `disk_offering`.`uuid` AS `disk_offering_uuid`, + `disk_offering`.`id` AS `disk_offering_id`, + (CASE + WHEN ISNULL(`service_offering`.`cpu`) THEN `custom_cpu`.`value` + ELSE `service_offering`.`cpu` + END) AS `cpu`, + (CASE + WHEN ISNULL(`service_offering`.`speed`) THEN `custom_speed`.`value` + ELSE `service_offering`.`speed` + END) AS `speed`, + (CASE + WHEN ISNULL(`service_offering`.`ram_size`) THEN `custom_ram_size`.`value` + ELSE `service_offering`.`ram_size` + END) AS `ram_size`, + `backup_offering`.`uuid` AS `backup_offering_uuid`, + `backup_offering`.`id` AS `backup_offering_id`, + `service_offering`.`name` AS `service_offering_name`, + `disk_offering`.`name` AS `disk_offering_name`, + `backup_offering`.`name` AS `backup_offering_name`, + `storage_pool`.`id` AS `pool_id`, + `storage_pool`.`uuid` AS `pool_uuid`, + `storage_pool`.`pool_type` AS `pool_type`, + `volumes`.`id` AS `volume_id`, + `volumes`.`uuid` AS `volume_uuid`, + `volumes`.`device_id` AS `volume_device_id`, + `volumes`.`volume_type` AS `volume_type`, + `security_group`.`id` AS `security_group_id`, + `security_group`.`uuid` AS `security_group_uuid`, + `security_group`.`name` AS `security_group_name`, + `security_group`.`description` AS `security_group_description`, + `nics`.`id` AS `nic_id`, + `nics`.`uuid` AS `nic_uuid`, + `nics`.`device_id` AS `nic_device_id`, + `nics`.`network_id` AS `network_id`, + `nics`.`ip4_address` AS `ip_address`, + `nics`.`ip6_address` AS `ip6_address`, + `nics`.`ip6_gateway` AS `ip6_gateway`, + `nics`.`ip6_cidr` AS `ip6_cidr`, + `nics`.`default_nic` AS `is_default_nic`, + `nics`.`gateway` AS `gateway`, + `nics`.`netmask` AS `netmask`, + `nics`.`mac_address` AS `mac_address`, + `nics`.`broadcast_uri` AS `broadcast_uri`, + `nics`.`isolation_uri` AS `isolation_uri`, + `vpc`.`id` AS `vpc_id`, + `vpc`.`uuid` AS `vpc_uuid`, + `networks`.`uuid` AS `network_uuid`, + `networks`.`name` AS `network_name`, + `networks`.`traffic_type` AS `traffic_type`, + `networks`.`guest_type` AS `guest_type`, + `user_ip_address`.`id` AS `public_ip_id`, + `user_ip_address`.`uuid` AS `public_ip_uuid`, + `user_ip_address`.`public_ip_address` AS `public_ip_address`, + `ssh_details`.`value` AS `keypair_names`, + `resource_tags`.`id` AS `tag_id`, + `resource_tags`.`uuid` AS `tag_uuid`, + `resource_tags`.`key` AS `tag_key`, + `resource_tags`.`value` AS `tag_value`, + `resource_tags`.`domain_id` AS `tag_domain_id`, + `domain`.`uuid` AS `tag_domain_uuid`, + `domain`.`name` AS `tag_domain_name`, + `resource_tags`.`account_id` AS `tag_account_id`, + `account`.`account_name` AS `tag_account_name`, + `resource_tags`.`resource_id` AS `tag_resource_id`, + `resource_tags`.`resource_uuid` AS `tag_resource_uuid`, + `resource_tags`.`resource_type` AS `tag_resource_type`, + `resource_tags`.`customer` AS `tag_customer`, + `async_job`.`id` AS `job_id`, + `async_job`.`uuid` AS `job_uuid`, + `async_job`.`job_status` AS `job_status`, + `async_job`.`account_id` AS `job_account_id`, + `affinity_group`.`id` AS `affinity_group_id`, + `affinity_group`.`uuid` AS `affinity_group_uuid`, + `affinity_group`.`name` AS `affinity_group_name`, + `affinity_group`.`description` AS `affinity_group_description`, + `autoscale_vmgroups`.`id` AS `autoscale_vmgroup_id`, + `autoscale_vmgroups`.`uuid` AS `autoscale_vmgroup_uuid`, + `autoscale_vmgroups`.`name` AS `autoscale_vmgroup_name`, + `vm_instance`.`dynamically_scalable` AS `dynamically_scalable`, + `user_data`.`id` AS `user_data_id`, + `user_data`.`uuid` AS `user_data_uuid`, + `user_data`.`name` AS `user_data_name`, + `user_vm`.`user_data_details` AS `user_data_details`, + `vm_template`.`user_data_link_policy` AS `user_data_policy` +FROM + (((((((((((((((((((((((((((((((((((`user_vm` + JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) + AND ISNULL(`vm_instance`.`removed`)))) + JOIN `account` ON ((`vm_instance`.`account_id` = `account`.`id`))) + JOIN `domain` ON ((`vm_instance`.`domain_id` = `domain`.`id`))) + LEFT JOIN `guest_os` ON ((`vm_instance`.`guest_os_id` = `guest_os`.`id`))) + LEFT JOIN `host_pod_ref` ON ((`vm_instance`.`pod_id` = `host_pod_ref`.`id`))) + LEFT JOIN `projects` ON ((`projects`.`project_account_id` = `account`.`id`))) + LEFT JOIN `instance_group_vm_map` ON ((`vm_instance`.`id` = `instance_group_vm_map`.`instance_id`))) + LEFT JOIN `instance_group` ON ((`instance_group_vm_map`.`group_id` = `instance_group`.`id`))) + LEFT JOIN `data_center` ON ((`vm_instance`.`data_center_id` = `data_center`.`id`))) + LEFT JOIN `host` ON ((`vm_instance`.`host_id` = `host`.`id`))) + LEFT JOIN `vm_template` ON ((`vm_instance`.`vm_template_id` = `vm_template`.`id`))) + LEFT JOIN `vm_template` `iso` ON ((`iso`.`id` = `user_vm`.`iso_id`))) + LEFT JOIN `volumes` ON ((`vm_instance`.`id` = `volumes`.`instance_id`))) + LEFT JOIN `service_offering` ON ((`vm_instance`.`service_offering_id` = `service_offering`.`id`))) + LEFT JOIN `disk_offering` `svc_disk_offering` ON ((`volumes`.`disk_offering_id` = `svc_disk_offering`.`id`))) + LEFT JOIN `disk_offering` ON ((`volumes`.`disk_offering_id` = `disk_offering`.`id`))) + LEFT JOIN `backup_offering` ON ((`vm_instance`.`backup_offering_id` = `backup_offering`.`id`))) + LEFT JOIN `storage_pool` ON ((`volumes`.`pool_id` = `storage_pool`.`id`))) + LEFT JOIN `security_group_vm_map` ON ((`vm_instance`.`id` = `security_group_vm_map`.`instance_id`))) + LEFT JOIN `security_group` ON ((`security_group_vm_map`.`security_group_id` = `security_group`.`id`))) + LEFT JOIN `user_data` ON ((`user_data`.`id` = `user_vm`.`user_data_id`))) + LEFT JOIN `nics` ON (((`vm_instance`.`id` = `nics`.`instance_id`) + AND ISNULL(`nics`.`removed`)))) + LEFT JOIN `networks` ON ((`nics`.`network_id` = `networks`.`id`))) + LEFT JOIN `vpc` ON (((`networks`.`vpc_id` = `vpc`.`id`) + AND ISNULL(`vpc`.`removed`)))) + LEFT JOIN `user_ip_address` ON ((`user_ip_address`.`vm_id` = `vm_instance`.`id`))) + LEFT JOIN `user_vm_details` `ssh_details` ON (((`ssh_details`.`vm_id` = `vm_instance`.`id`) + AND (`ssh_details`.`name` = 'SSH.KeyPairNames')))) + LEFT JOIN `resource_tags` ON (((`resource_tags`.`resource_id` = `vm_instance`.`id`) + AND (`resource_tags`.`resource_type` = 'UserVm')))) + LEFT JOIN `async_job` ON (((`async_job`.`instance_id` = `vm_instance`.`id`) + AND (`async_job`.`instance_type` = 'VirtualMachine') + AND (`async_job`.`job_status` = 0)))) + LEFT JOIN `affinity_group_vm_map` ON ((`vm_instance`.`id` = `affinity_group_vm_map`.`instance_id`))) + LEFT JOIN `affinity_group` ON ((`affinity_group_vm_map`.`affinity_group_id` = `affinity_group`.`id`))) + LEFT JOIN `autoscale_vmgroup_vm_map` ON ((`autoscale_vmgroup_vm_map`.`instance_id` = `vm_instance`.`id`))) + LEFT JOIN `autoscale_vmgroups` ON ((`autoscale_vmgroup_vm_map`.`vmgroup_id` = `autoscale_vmgroups`.`id`))) + LEFT JOIN `user_vm_details` `custom_cpu` ON (((`custom_cpu`.`vm_id` = `vm_instance`.`id`) + AND (`custom_cpu`.`name` = 'CpuNumber')))) + LEFT JOIN `user_vm_details` `custom_speed` ON (((`custom_speed`.`vm_id` = `vm_instance`.`id`) + AND (`custom_speed`.`name` = 'CpuSpeed')))) + LEFT JOIN `user_vm_details` `custom_ram_size` ON (((`custom_ram_size`.`vm_id` = `vm_instance`.`id`) + AND (`custom_ram_size`.`name` = 'memory')))); + -- Add tables for Cluster DRS DROP TABLE IF EXISTS `cloud`.`cluster_drs_plan`; CREATE TABLE `cloud`.`cluster_drs_plan` ( diff --git a/engine/schema/src/test/java/com/cloud/storage/VnfTemplateDetailVOTest.java b/engine/schema/src/test/java/com/cloud/storage/VnfTemplateDetailVOTest.java new file mode 100755 index 00000000000..99edbc46c42 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/storage/VnfTemplateDetailVOTest.java @@ -0,0 +1,38 @@ +// 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 com.cloud.storage; + +import org.junit.Assert; +import org.junit.Test; + +public class VnfTemplateDetailVOTest { + + static long templateId = 100L; + static String name = "key1"; + static String value = "value1"; + static boolean display = true; + + @Test + public void testVnfTemplateNicVOProperties() { + VnfTemplateDetailVO detailVO = new VnfTemplateDetailVO(templateId, name, value, display); + + Assert.assertEquals(templateId, detailVO.getResourceId()); + Assert.assertEquals(name, detailVO.getName()); + Assert.assertEquals(value, detailVO.getValue()); + Assert.assertEquals(display, detailVO.isDisplay()); + } +} diff --git a/engine/schema/src/test/java/com/cloud/storage/VnfTemplateNicVOTest.java b/engine/schema/src/test/java/com/cloud/storage/VnfTemplateNicVOTest.java new file mode 100755 index 00000000000..96ebd4ed171 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/storage/VnfTemplateNicVOTest.java @@ -0,0 +1,46 @@ +// 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 com.cloud.storage; + +import org.junit.Assert; +import org.junit.Test; + +public class VnfTemplateNicVOTest { + + static long templateId = 100L; + static long deviceId = 0L; + static String deviceName = "eth0"; + static boolean required = true; + static boolean management = false; + static String description = "description of vnf nic"; + + + @Test + public void testVnfTemplateNicVOProperties() { + VnfTemplateNicVO nicVO = new VnfTemplateNicVO(templateId, deviceId, deviceName, required, management, description); + + Assert.assertEquals(templateId, nicVO.getTemplateId()); + Assert.assertEquals(deviceId, nicVO.getDeviceId()); + Assert.assertEquals(deviceName, nicVO.getDeviceName()); + Assert.assertEquals(required, nicVO.isRequired()); + Assert.assertEquals(management, nicVO.isManagement()); + Assert.assertEquals(description, nicVO.getDescription()); + + String expected = String.format("Template {\"deviceId\":%d,\"id\":0,\"required\":%s,\"templateId\":%d}", deviceId, required, templateId); + Assert.assertEquals(expected, nicVO.toString()); + } +} diff --git a/engine/schema/src/test/java/com/cloud/storage/dao/VnfTemplateNicDaoImplTest.java b/engine/schema/src/test/java/com/cloud/storage/dao/VnfTemplateNicDaoImplTest.java new file mode 100644 index 00000000000..6a574fc81ee --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/storage/dao/VnfTemplateNicDaoImplTest.java @@ -0,0 +1,88 @@ +// 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 com.cloud.storage.dao; + +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.TransactionLegacy; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; + +@RunWith(MockitoJUnitRunner.class) +public class VnfTemplateNicDaoImplTest { + + @Mock + SearchBuilder searchBuilderVnfTemplateNicVOMock; + + @Mock + SearchCriteria searchCriteriaVnfTemplateNicVOMock; + + @Mock + List listVnfTemplateNicVOMock; + + @Mock + private TransactionLegacy transactionMock; + + @Spy + VnfTemplateNicDaoImpl vnfTemplateNicDaoImplSpy; + + @Before + public void setUp() { + vnfTemplateNicDaoImplSpy.TemplateSearch = searchBuilderVnfTemplateNicVOMock; + Mockito.doReturn(searchCriteriaVnfTemplateNicVOMock).when(searchBuilderVnfTemplateNicVOMock).create(); + Mockito.doNothing().when(searchCriteriaVnfTemplateNicVOMock).setParameters(Mockito.anyString(), Mockito.any()); + } + + @Test + public void testListByTemplateId() { + Mockito.doReturn(listVnfTemplateNicVOMock).when(vnfTemplateNicDaoImplSpy).listBy(Mockito.any(SearchCriteria.class)); + long templateId = 100L; + + List result = vnfTemplateNicDaoImplSpy.listByTemplateId(templateId); + + Assert.assertEquals(listVnfTemplateNicVOMock, result); + Mockito.verify(searchCriteriaVnfTemplateNicVOMock).setParameters("templateId", templateId); + } + + @Test + public void testDeleteByTemplateId() { + Mockito.doReturn(0).when(vnfTemplateNicDaoImplSpy).remove(searchCriteriaVnfTemplateNicVOMock); + long templateId = 100L; + + try (MockedStatic ignore = Mockito.mockStatic(TransactionLegacy.class)) { + Mockito.when(TransactionLegacy.currentTxn()).thenReturn(transactionMock); + Mockito.doNothing().when(transactionMock).start(); + Mockito.doReturn(true).when(transactionMock).commit(); + + vnfTemplateNicDaoImplSpy.deleteByTemplateId(templateId); + + Mockito.verify(transactionMock, Mockito.times(1)).start(); + Mockito.verify(vnfTemplateNicDaoImplSpy, Mockito.times(1)).remove(searchCriteriaVnfTemplateNicVOMock); + Mockito.verify(transactionMock, Mockito.times(1)).commit(); + } + } +} diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index ddfd0671820..5d821d38f2e 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -1533,6 +1533,10 @@ public class ApiDBUtils { return s_ipAddressDao.findByAssociatedVmId(vmId); } + public static IpAddress findIpByAssociatedVmIdAndNetworkId(long vmId, long networkId) { + return s_ipAddressDao.findByVmIdAndNetworkId(networkId, vmId); + } + public static String getHaTag() { return s_haMgr.getHaTag(); } diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 946b48cf480..93b858077f6 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -99,6 +99,7 @@ import org.apache.cloudstack.api.command.user.snapshot.CopySnapshotCmd; import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotsCmd; import org.apache.cloudstack.api.command.user.tag.ListTagsCmd; import org.apache.cloudstack.api.command.user.template.ListTemplatesCmd; +import org.apache.cloudstack.api.command.user.template.ListVnfTemplatesCmd; import org.apache.cloudstack.api.command.user.vm.ListVMsCmd; import org.apache.cloudstack.api.command.user.vmgroup.ListVMGroupsCmd; import org.apache.cloudstack.api.command.user.volume.ListResourceDetailsCmd; @@ -222,6 +223,7 @@ import com.cloud.ha.HighAvailabilityManager; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.RouterHealthCheckResult; +import com.cloud.network.VNF; import com.cloud.network.VpcVirtualNetworkApplianceService; import com.cloud.network.dao.RouterHealthCheckResultDao; import com.cloud.network.dao.RouterHealthCheckResultVO; @@ -1315,6 +1317,15 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q userVmSearchBuilder.join("autoScaleVmGroup", autoScaleMapSearch, autoScaleMapSearch.entity().getInstanceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); } + Boolean isVnf = cmd.getVnf(); + if (isVnf != null) { + SearchBuilder templateSearch = _templateDao.createSearchBuilder(); + templateSearch.and("templateTypeEQ", templateSearch.entity().getTemplateType(), Op.EQ); + templateSearch.and("templateTypeNEQ", templateSearch.entity().getTemplateType(), Op.NEQ); + + userVmSearchBuilder.join("vmTemplate", templateSearch, templateSearch.entity().getId(), userVmSearchBuilder.entity().getTemplateId(), JoinBuilder.JoinType.INNER); + } + SearchCriteria userVmSearchCriteria = userVmSearchBuilder.create(); accountMgr.buildACLSearchCriteria(userVmSearchCriteria, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); @@ -1423,6 +1434,14 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q userVmSearchCriteria.setJoinParameters("autoScaleVmGroup", "autoScaleVmGroupId", autoScaleVmGroupId); } + if (isVnf != null) { + if (isVnf) { + userVmSearchCriteria.setJoinParameters("vmTemplate", "templateTypeEQ", TemplateType.VNF); + } else { + userVmSearchCriteria.setJoinParameters("vmTemplate", "templateTypeNEQ", TemplateType.VNF); + } + } + if (isRootAdmin) { if (podId != null) { userVmSearchCriteria.setParameters("podId", podId); @@ -3791,13 +3810,24 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q boolean showDomr = ((templateFilter != TemplateFilter.selfexecutable) && (templateFilter != TemplateFilter.featured)); HypervisorType hypervisorType = HypervisorType.getType(cmd.getHypervisor()); + String templateType = cmd.getTemplateType(); + if (cmd instanceof ListVnfTemplatesCmd) { + if (templateType == null) { + templateType = TemplateType.VNF.name(); + } else if (!TemplateType.VNF.name().equals(templateType)) { + throw new InvalidParameterValueException("Template type must be VNF when list VNF templates"); + } + } + Boolean isVnf = cmd.getVnf(); + return searchForTemplatesInternal(id, cmd.getTemplateName(), cmd.getKeyword(), templateFilter, false, null, cmd.getPageSizeVal(), cmd.getStartIndex(), cmd.getZoneId(), hypervisorType, - showDomr, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, tags, showRemovedTmpl, cmd.getIds(), parentTemplateId, cmd.getShowUnique()); + showDomr, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, tags, showRemovedTmpl, cmd.getIds(), parentTemplateId, cmd.getShowUnique(), templateType, isVnf); } private Pair, Integer> searchForTemplatesInternal(Long templateId, String name, String keyword, TemplateFilter templateFilter, boolean isIso, Boolean bootable, Long pageSize, Long startIndex, Long zoneId, HypervisorType hyperType, boolean showDomr, boolean onlyReady, List permittedAccounts, Account caller, - ListProjectResourcesCriteria listProjectResourcesCriteria, Map tags, boolean showRemovedTmpl, List ids, Long parentTemplateId, Boolean showUnique) { + ListProjectResourcesCriteria listProjectResourcesCriteria, Map tags, boolean showRemovedTmpl, List ids, Long parentTemplateId, Boolean showUnique, String templateType, + Boolean isVnf) { // check if zone is configured, if not, just return empty list List hypers = null; @@ -3964,7 +3994,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q applyPublicTemplateSharingRestrictions(sc, caller); return templateChecks(isIso, hypers, tags, name, keyword, hyperType, onlyReady, bootable, zoneId, showDomr, caller, - showRemovedTmpl, parentTemplateId, showUnique, searchFilter, sc); + showRemovedTmpl, parentTemplateId, showUnique, templateType, isVnf, searchFilter, sc); } @@ -4019,7 +4049,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private Pair, Integer> templateChecks(boolean isIso, List hypers, Map tags, String name, String keyword, HypervisorType hyperType, boolean onlyReady, Boolean bootable, Long zoneId, boolean showDomr, Account caller, - boolean showRemovedTmpl, Long parentTemplateId, Boolean showUnique, + boolean showRemovedTmpl, Long parentTemplateId, Boolean showUnique, String templateType, Boolean isVnf, Filter searchFilter, SearchCriteria sc) { if (!isIso) { // add hypervisor criteria for template case @@ -4101,6 +4131,18 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sc.addAnd("parentTemplateId", SearchCriteria.Op.EQ, parentTemplateId); } + if (templateType != null) { + sc.addAnd("templateType", SearchCriteria.Op.EQ, templateType); + } + + if (isVnf != null) { + if (isVnf) { + sc.addAnd("templateType", SearchCriteria.Op.EQ, TemplateType.VNF); + } else { + sc.addAnd("templateType", SearchCriteria.Op.NEQ, TemplateType.VNF); + } + } + // don't return removed template, this should not be needed since we // changed annotation for removed field in TemplateJoinVO. // sc.addAnd("removed", SearchCriteria.Op.NULL); @@ -4192,7 +4234,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q HypervisorType hypervisorType = HypervisorType.getType(cmd.getHypervisor()); return searchForTemplatesInternal(cmd.getId(), cmd.getIsoName(), cmd.getKeyword(), isoFilter, true, cmd.isBootable(), cmd.getPageSizeVal(), cmd.getStartIndex(), cmd.getZoneId(), - hypervisorType, true, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, tags, showRemovedISO, null, null, cmd.getShowUnique()); + hypervisorType, true, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, tags, showRemovedISO, null, null, cmd.getShowUnique(), null, null); } @Override @@ -4212,6 +4254,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } fillVMOrTemplateDetailOptions(options, hypervisorType); break; + case VnfTemplate: + fillVnfTemplateDetailOptions(options); + return new DetailOptionsResponse(options); default: throw new CloudRuntimeException("Resource type not supported."); } @@ -4235,6 +4280,19 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q return responses; } + private void fillVnfTemplateDetailOptions(final Map> options) { + for (VNF.AccessDetail detail : VNF.AccessDetail.values()) { + if (VNF.AccessDetail.ACCESS_METHODS.equals(detail)) { + options.put(detail.name().toLowerCase(), Arrays.stream(VNF.AccessMethod.values()).map(method -> method.toString()).sorted().collect(Collectors.toList())); + } else { + options.put(detail.name().toLowerCase(), Collections.emptyList()); + } + } + for (VNF.VnfDetail detail : VNF.VnfDetail.values()) { + options.put(detail.name().toLowerCase(), Collections.emptyList()); + } + } + private void fillVMOrTemplateDetailOptions(final Map> options, final HypervisorType hypervisorType) { if (options == null) { throw new CloudRuntimeException("Invalid/null detail-options response object passed"); diff --git a/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java index 4fe0f200741..b9dcad98f22 100644 --- a/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java @@ -17,6 +17,7 @@ package com.cloud.api.query.dao; import java.util.ArrayList; +import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; @@ -29,10 +30,16 @@ import javax.inject.Inject; import com.cloud.deployasis.DeployAsIsConstants; import com.cloud.deployasis.TemplateDeployAsIsDetailVO; import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao; +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.storage.dao.VnfTemplateDetailsDao; +import com.cloud.storage.dao.VnfTemplateNicDao; import com.cloud.user.dao.UserDataDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.response.VnfNicResponse; +import org.apache.cloudstack.api.response.VnfTemplateResponse; import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.utils.security.DigestHelper; import org.apache.log4j.Logger; @@ -93,6 +100,10 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation tmpltIdPairSearch; @@ -190,7 +201,7 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation nics = vnfTemplateNicDao.listByTemplateId(template.getId()); + for (VnfTemplateNicVO nic : nics) { + vnfTemplateResponse.addVnfNic(new VnfNicResponse(nic.getDeviceId(), nic.getDeviceName(), nic.isRequired(), nic.isManagement(), nic.getDescription())); + } + List details = vnfTemplateDetailsDao.listDetails(template.getId()); + Collections.sort(details, (v1, v2) -> v1.getName().compareToIgnoreCase(v2.getName())); + for (VnfTemplateDetailVO detail : details) { + vnfTemplateResponse.addVnfDetail(detail.getName(), detail.getValue()); + } + templateResponse = vnfTemplateResponse; + } + return templateResponse; + } + private void setDeployAsIsDetails(TemplateJoinVO template, TemplateResponse templateResponse) { if (template.isDeployAsIs()) { List deployAsIsDetails = templateDeployAsIsDetailsDao.listDetails(template.getId()); @@ -326,7 +356,7 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation VmDetailSearch; private final SearchBuilder activeVmByIsoSearch; @@ -183,6 +195,7 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation nicExtraDhcpOptionResponses = _nicExtraDhcpOptionDao.listByNicId(nic_id).stream() @@ -419,9 +438,25 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation vnfNics = vnfTemplateNicDao.listByTemplateId(userVm.getTemplateId()); + for (VnfTemplateNicVO nic : vnfNics) { + userVmResponse.addVnfNic(new VnfNicResponse(nic.getDeviceId(), nic.getDeviceName(), nic.isRequired(), nic.isManagement(), nic.getDescription())); + } + List vnfDetails = vnfTemplateDetailsDao.listDetails(userVm.getTemplateId()); + Collections.sort(vnfDetails, (v1, v2) -> v1.getName().compareToIgnoreCase(v2.getName())); + for (VnfTemplateDetailVO detail : vnfDetails) { + userVmResponse.addVnfDetail(detail.getName(), detail.getValue()); + } + } + private void addVmRxTxDataToResponse(UserVmJoinVO userVm, UserVmResponse userVmResponse) { Long bytesReceived = 0L; Long bytesSent = 0L; @@ -519,6 +554,11 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation zoneId = cmd.getZoneIds(); // ignore passed zoneId if we are using region wide image store @@ -305,7 +306,7 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat } return prepare(false, CallContext.current().getCallingUserId(), cmd.getTemplateName(), cmd.getDisplayText(), cmd.getBits(), cmd.isPasswordEnabled(), cmd.getRequiresHvm(), cmd.getUrl(), cmd.isPublic(), cmd.isFeatured(), cmd.isExtractable(), cmd.getFormat(), cmd.getOsTypeId(), zoneId, hypervisorType, cmd.getChecksum(), true, - cmd.getTemplateTag(), owner, details, cmd.isSshKeyEnabled(), null, cmd.isDynamicallyScalable(), isRouting ? TemplateType.ROUTING : TemplateType.USER, + cmd.getTemplateTag(), owner, details, cmd.isSshKeyEnabled(), null, cmd.isDynamicallyScalable(), templateType, cmd.isDirectDownload(), cmd.isDeployAsIs()); } diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 43a3de2078d..f5d385b4e10 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -36,6 +36,7 @@ import javax.naming.ConfigurationException; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.BaseListTemplateOrIsoPermissionsCmd; import org.apache.cloudstack.api.BaseUpdateTemplateOrIsoCmd; import org.apache.cloudstack.api.BaseUpdateTemplateOrIsoPermissionsCmd; @@ -53,8 +54,10 @@ import org.apache.cloudstack.api.command.user.template.ExtractTemplateCmd; import org.apache.cloudstack.api.command.user.template.GetUploadParamsForTemplateCmd; import org.apache.cloudstack.api.command.user.template.ListTemplatePermissionsCmd; import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; import org.apache.cloudstack.api.command.user.template.UpdateTemplateCmd; import org.apache.cloudstack.api.command.user.template.UpdateTemplatePermissionsCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd; import org.apache.cloudstack.api.response.GetUploadParamsResponse; import org.apache.cloudstack.context.CallContext; @@ -96,6 +99,8 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; +import org.apache.cloudstack.storage.template.VnfTemplateManager; +import org.apache.cloudstack.storage.template.VnfTemplateUtils; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; import org.apache.commons.collections.CollectionUtils; @@ -303,6 +308,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, @Inject protected SnapshotHelper snapshotHelper; + @Inject + VnfTemplateManager vnfTemplateManager; private TemplateAdapter getAdapter(HypervisorType type) { TemplateAdapter adapter = null; @@ -360,6 +367,9 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, if (template != null) { CallContext.current().putContextParameter(VirtualMachineTemplate.class, template.getUuid()); + if (cmd instanceof RegisterVnfTemplateCmd) { + vnfTemplateManager.persistVnfTemplate(template.getId(), (RegisterVnfTemplateCmd) cmd); + } return template; } else { throw new CloudRuntimeException("Failed to create a template"); @@ -1329,6 +1339,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, throw new InvalidParameterValueException("Please specify a valid template."); } + VnfTemplateUtils.validateApiCommandParams(cmd, template); + TemplateAdapter adapter = getAdapter(template.getHypervisorType()); TemplateProfile profile = adapter.prepareDelete(cmd); return adapter.delete(profile); @@ -2113,22 +2125,11 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, // update template type TemplateType templateType = null; if (cmd instanceof UpdateTemplateCmd) { - String newType = ((UpdateTemplateCmd)cmd).getTemplateType(); - if (newType != null) { - if (!_accountService.isRootAdmin(account.getId())) { - throw new PermissionDeniedException("Parameter templatetype can only be specified by a Root Admin, permission denied"); - } - try { - templateType = TemplateType.valueOf(newType.toUpperCase()); - } catch (IllegalArgumentException ex) { - throw new InvalidParameterValueException("Please specify a valid templatetype: ROUTING / SYSTEM / USER / BUILTIN / PERHOST"); - } - } - if (templateType != null && cmd.isRoutingType() != null && (TemplateType.ROUTING.equals(templateType) != cmd.isRoutingType())) { - throw new InvalidParameterValueException("Please specify a valid templatetype (consistent with isrouting parameter)."); - } - if (templateType != null && (templateType == TemplateType.SYSTEM || templateType == TemplateType.BUILTIN) && !template.isCrossZones()) { - throw new InvalidParameterValueException("System and Builtin templates must be cross zone"); + boolean isAdmin = _accountMgr.isAdmin(account.getId()); + templateType = validateTemplateType(cmd, isAdmin, template.isCrossZones()); + if (cmd instanceof UpdateVnfTemplateCmd) { + VnfTemplateUtils.validateApiCommandParams(cmd, template); + vnfTemplateManager.updateVnfTemplate(template.getId(), (UpdateVnfTemplateCmd) cmd); } } @@ -2248,6 +2249,51 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, return _tmpltDao.findById(id); } + @Override + public TemplateType validateTemplateType(BaseCmd cmd, boolean isAdmin, boolean isCrossZones) { + if (!(cmd instanceof UpdateTemplateCmd) && !(cmd instanceof RegisterTemplateCmd)) { + return null; + } + TemplateType templateType = null; + String newType = null; + Boolean isRoutingType = null; + if (cmd instanceof UpdateTemplateCmd) { + newType = ((UpdateTemplateCmd)cmd).getTemplateType(); + isRoutingType = ((UpdateTemplateCmd)cmd).isRoutingType(); + } else if (cmd instanceof RegisterTemplateCmd) { + newType = ((RegisterTemplateCmd)cmd).getTemplateType(); + isRoutingType = ((RegisterTemplateCmd)cmd).isRoutingType(); + } + if (newType != null) { + try { + templateType = TemplateType.valueOf(newType.toUpperCase()); + } catch (IllegalArgumentException ex) { + throw new InvalidParameterValueException(String.format("Please specify a valid templatetype: %s", + org.apache.commons.lang3.StringUtils.join(",", TemplateType.values()))); + } + } + if (templateType != null) { + if (isRoutingType != null && (TemplateType.ROUTING.equals(templateType) != isRoutingType)) { + throw new InvalidParameterValueException("Please specify a valid templatetype (consistent with isrouting parameter)."); + } else if ((templateType == TemplateType.SYSTEM || templateType == TemplateType.BUILTIN) && !isCrossZones) { + throw new InvalidParameterValueException("System and Builtin templates must be cross zone."); + } else if ((cmd instanceof RegisterVnfTemplateCmd || cmd instanceof UpdateVnfTemplateCmd) && !TemplateType.VNF.equals(templateType)) { + throw new InvalidParameterValueException("The template type must be VNF for VNF templates, but the actual type is " + templateType); + } + } else if (cmd instanceof RegisterTemplateCmd) { + boolean isRouting = Boolean.TRUE.equals(isRoutingType); + templateType = (cmd instanceof RegisterVnfTemplateCmd) ? TemplateType.VNF : (isRouting ? TemplateType.ROUTING : TemplateType.USER); + } + if (templateType != null && !isAdmin && !Arrays.asList(TemplateType.USER, TemplateType.VNF).contains(templateType)) { + if (cmd instanceof RegisterTemplateCmd) { + throw new InvalidParameterValueException(String.format("Users can not register template with template type %s.", templateType)); + } else if (cmd instanceof UpdateTemplateCmd) { + throw new InvalidParameterValueException(String.format("Users can not update template to template type %s.", templateType)); + } + } + return templateType; + } + void validateDetails(VMTemplateVO template, Map details) { if (MapUtils.isEmpty(details)) { return; @@ -2270,7 +2316,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, String msg = String.format("Invalid %s: %s specified. Valid values are: %s", ApiConstants.BOOT_MODE, bootMode, Arrays.toString(ApiConstants.BootMode.values())); s_logger.error(msg); - throw new InvalidParameterValueException(msg); } + throw new InvalidParameterValueException(msg); + } } void verifyTemplateId(Long id) { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 90f1dde96e7..1f8332d6f02 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -69,6 +69,7 @@ import org.apache.cloudstack.api.command.admin.vm.DeployVMCmdByAdmin; import org.apache.cloudstack.api.command.admin.vm.RecoverVMCmd; import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd; import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; import org.apache.cloudstack.api.command.user.vm.RebootVMCmd; import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd; @@ -123,6 +124,7 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.cloudstack.storage.template.VnfTemplateManager; import org.apache.cloudstack.userdata.UserDataManager; import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.apache.cloudstack.utils.security.ParserUtils; @@ -620,6 +622,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Inject private UserDataManager userDataManager; + @Inject + VnfTemplateManager vnfTemplateManager; + private static final ConfigKey VmIpFetchWaitInterval = new ConfigKey("Advanced", Integer.class, "externaldhcp.vmip.retrieval.interval", "180", "Wait Interval (in seconds) for shared network vm dhcp ip addr fetch for next iteration ", true); @@ -5901,6 +5906,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if (template == null) { throw new InvalidParameterValueException("Unable to use template " + templateId); } + if (TemplateType.VNF.equals(template.getTemplateType())) { + vnfTemplateManager.validateVnfApplianceNics(template, cmd.getNetworkIds()); + } else if (cmd instanceof DeployVnfApplianceCmd) { + throw new InvalidParameterValueException("Can't deploy VNF appliance from a non-VNF template"); + } ServiceOfferingJoinVO svcOffering = serviceOfferingJoinDao.findById(serviceOfferingId); @@ -5982,14 +5992,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if (networkIds != null) { throw new InvalidParameterValueException("Can't specify network Ids in Basic zone"); } else { - vm = createBasicSecurityGroupVirtualMachine(zone, serviceOffering, template, getSecurityGroupIdList(cmd), owner, name, displayName, diskOfferingId, + vm = createBasicSecurityGroupVirtualMachine(zone, serviceOffering, template, getSecurityGroupIdList(cmd, zone, template, owner), owner, name, displayName, diskOfferingId, size , group , cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm , keyboard , cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, overrideDiskOfferingId); } } else { if (zone.isSecurityGroupEnabled()) { - vm = createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, networkIds, getSecurityGroupIdList(cmd), owner, name, + vm = createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, networkIds, getSecurityGroupIdList(cmd, zone, template, owner), owner, name, displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, overrideDiskOfferingId, null); @@ -6001,6 +6011,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir vm = createAdvancedVirtualMachine(zone, serviceOffering, template, networkIds, owner, name, displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, null, overrideDiskOfferingId); + if (cmd instanceof DeployVnfApplianceCmd) { + vnfTemplateManager.createIsolatedNetworkRulesForVnfAppliance(zone, template, owner, vm, (DeployVnfApplianceCmd) cmd); + } } } @@ -6267,6 +6280,20 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } + protected List getSecurityGroupIdList(SecurityGroupAction cmd, DataCenter zone, VirtualMachineTemplate template, Account owner) { + List securityGroupIdList = getSecurityGroupIdList(cmd); + if (cmd instanceof DeployVnfApplianceCmd) { + SecurityGroup securityGroup = vnfTemplateManager.createSecurityGroupForVnfAppliance(zone, template, owner, (DeployVnfApplianceCmd) cmd); + if (securityGroup != null) { + if (securityGroupIdList == null) { + securityGroupIdList = new ArrayList<>(); + } + securityGroupIdList.add(securityGroup.getId()); + } + } + return securityGroupIdList; + } + // this is an opportunity to verify that parameters that came in via the Details Map are OK // for example, minIops and maxIops should either both be specified or neither be specified and, // if specified, minIops should be <= maxIops diff --git a/server/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImpl.java b/server/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImpl.java new file mode 100644 index 00000000000..0371be86448 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImpl.java @@ -0,0 +1,372 @@ +// 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.storage.template; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import com.cloud.dc.DataCenter; +import com.cloud.exception.InsufficientAddressCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.IpAddress; +import com.cloud.network.IpAddressManager; +import com.cloud.network.Network; +import com.cloud.network.NetworkModel; +import com.cloud.network.NetworkService; +import com.cloud.network.VNF; +import com.cloud.network.dao.FirewallRulesDao; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.firewall.FirewallService; +import com.cloud.network.rules.FirewallRule; +import com.cloud.network.rules.FirewallRuleVO; +import com.cloud.network.rules.RulesService; +import com.cloud.network.security.SecurityGroup; +import com.cloud.network.security.SecurityGroupManager; +import com.cloud.network.security.SecurityGroupService; +import com.cloud.network.security.SecurityGroupVO; +import com.cloud.network.security.SecurityRule; +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.storage.dao.VnfTemplateDetailsDao; +import com.cloud.storage.dao.VnfTemplateNicDao; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallbackWithExceptionNoReturn; +import com.cloud.utils.db.TransactionStatus; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; +import com.cloud.vm.NicVO; +import com.cloud.vm.dao.NicDao; +import org.apache.cloudstack.api.command.admin.template.ListVnfTemplatesCmdByAdmin; +import org.apache.cloudstack.api.command.admin.template.RegisterVnfTemplateCmdByAdmin; +import org.apache.cloudstack.api.command.admin.template.UpdateVnfTemplateCmdByAdmin; +import org.apache.cloudstack.api.command.admin.vm.DeployVnfApplianceCmdByAdmin; +import org.apache.cloudstack.api.command.user.template.DeleteVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.ListVnfTemplatesCmd; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.log4j.Logger; + + +public class VnfTemplateManagerImpl extends ManagerBase implements VnfTemplateManager, PluggableService, Configurable { + + static final Logger LOGGER = Logger.getLogger(VnfTemplateManagerImpl.class); + + public static final String VNF_SECURITY_GROUP_NAME = "VNF_SecurityGroup_"; + public static final String ACCESS_METHOD_SEPARATOR = ","; + public static final Integer ACCESS_DEFAULT_SSH_PORT = 22; + public static final Integer ACCESS_DEFAULT_HTTP_PORT = 80; + public static final Integer ACCESS_DEFAULT_HTTPS_PORT = 443; + + @Inject + VnfTemplateDetailsDao vnfTemplateDetailsDao; + @Inject + VnfTemplateNicDao vnfTemplateNicDao; + @Inject + SecurityGroupManager securityGroupManager; + @Inject + SecurityGroupService securityGroupService; + @Inject + NetworkModel networkModel; + @Inject + IpAddressManager ipAddressManager; + @Inject + NicDao nicDao; + @Inject + NetworkDao networkDao; + @Inject + NetworkService networkService; + @Inject + RulesService rulesService; + @Inject + FirewallRulesDao firewallRulesDao; + @Inject + FirewallService firewallService; + + @Override + public List> getCommands() { + final List> cmdList = new ArrayList<>(); + if (!VnfTemplateAndApplianceEnabled.value()) { + return cmdList; + } + cmdList.add(RegisterVnfTemplateCmd.class); + cmdList.add(RegisterVnfTemplateCmdByAdmin.class); + cmdList.add(ListVnfTemplatesCmd.class); + cmdList.add(ListVnfTemplatesCmdByAdmin.class); + cmdList.add(UpdateVnfTemplateCmd.class); + cmdList.add(UpdateVnfTemplateCmdByAdmin.class); + cmdList.add(DeleteVnfTemplateCmd.class); + cmdList.add(DeployVnfApplianceCmd.class); + cmdList.add(DeployVnfApplianceCmdByAdmin.class); + return cmdList; + } + + @Override + public void persistVnfTemplate(long templateId, RegisterVnfTemplateCmd cmd) { + persistVnfTemplateNics(templateId, cmd.getVnfNics()); + persistVnfTemplateDetails(templateId, cmd); + } + + private void persistVnfTemplateNics(long templateId, List nics) { + for (VNF.VnfNic nic : nics) { + VnfTemplateNicVO vnfTemplateNicVO = new VnfTemplateNicVO(templateId, nic.getDeviceId(), nic.getName(), nic.isRequired(), nic.isManagement(), nic.getDescription()); + vnfTemplateNicDao.persist(vnfTemplateNicVO); + } + } + + private void persistVnfTemplateDetails(long templateId, RegisterVnfTemplateCmd cmd) { + persistVnfTemplateDetails(templateId, cmd.getVnfDetails()); + } + + private void persistVnfTemplateDetails(long templateId, Map vnfDetails) { + for (Map.Entry entry: vnfDetails.entrySet()) { + String value = entry.getValue(); + if (VNF.AccessDetail.ACCESS_METHODS.name().equalsIgnoreCase(entry.getKey())) { + value = Arrays.stream(value.split(ACCESS_METHOD_SEPARATOR)).sorted().collect(Collectors.joining(ACCESS_METHOD_SEPARATOR)); + } + vnfTemplateDetailsDao.addDetail(templateId, entry.getKey().toLowerCase(), value, true); + } + } + + @Override + public void updateVnfTemplate(long templateId, UpdateVnfTemplateCmd cmd) { + updateVnfTemplateDetails(templateId, cmd); + updateVnfTemplateNics(templateId, cmd); + } + + private void updateVnfTemplateDetails(long templateId, UpdateVnfTemplateCmd cmd) { + boolean cleanupVnfDetails = cmd.isCleanupVnfDetails(); + if (cleanupVnfDetails) { + vnfTemplateDetailsDao.removeDetails(templateId); + } else if (MapUtils.isNotEmpty(cmd.getVnfDetails())) { + vnfTemplateDetailsDao.removeDetails(templateId); + persistVnfTemplateDetails(templateId, cmd.getVnfDetails()); + } + } + + private void updateVnfTemplateNics(long templateId, UpdateVnfTemplateCmd cmd) { + boolean cleanupVnfNics = cmd.isCleanupVnfNics(); + if (cleanupVnfNics) { + vnfTemplateNicDao.deleteByTemplateId(templateId); + } else if (CollectionUtils.isNotEmpty(cmd.getVnfNics())) { + vnfTemplateNicDao.deleteByTemplateId(templateId); + persistVnfTemplateNics(templateId, cmd.getVnfNics()); + } + } + + @Override + public String getConfigComponentName() { + return VnfTemplateManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] { VnfTemplateAndApplianceEnabled }; + } + + @Override + public void validateVnfApplianceNics(VirtualMachineTemplate template, List networkIds) { + if (CollectionUtils.isEmpty(networkIds)) { + throw new InvalidParameterValueException("VNF nics list is empty"); + } + List vnfNics = vnfTemplateNicDao.listByTemplateId(template.getId()); + for (VnfTemplateNicVO vnfNic : vnfNics) { + if (vnfNic.isRequired() && networkIds.size() <= vnfNic.getDeviceId()) { + throw new InvalidParameterValueException("VNF nic is required but not found: " + vnfNic); + } + } + } + + protected Set getOpenPortsForVnfAppliance(VirtualMachineTemplate template) { + Set ports = new HashSet<>(); + VnfTemplateDetailVO accessMethodsDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.ACCESS_METHODS.name().toLowerCase()); + if (accessMethodsDetail == null || accessMethodsDetail.getValue() == null) { + return ports; + } + String[] accessMethods = accessMethodsDetail.getValue().split(ACCESS_METHOD_SEPARATOR); + for (String accessMethod : accessMethods) { + if (VNF.AccessMethod.SSH_WITH_KEY.toString().equalsIgnoreCase(accessMethod) + || VNF.AccessMethod.SSH_WITH_PASSWORD.toString().equalsIgnoreCase(accessMethod)) { + VnfTemplateDetailVO accessDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.SSH_PORT.name().toLowerCase()); + if (accessDetail == null) { + ports.add(ACCESS_DEFAULT_SSH_PORT); + } else { + ports.add(NumbersUtil.parseInt(accessDetail.getValue(), ACCESS_DEFAULT_SSH_PORT)); + } + } else if (VNF.AccessMethod.HTTP.toString().equalsIgnoreCase(accessMethod)) { + VnfTemplateDetailVO accessDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.HTTP_PORT.name().toLowerCase()); + if (accessDetail == null) { + ports.add(ACCESS_DEFAULT_HTTP_PORT); + } else { + ports.add(NumbersUtil.parseInt(accessDetail.getValue(), ACCESS_DEFAULT_HTTP_PORT)); + } + } else if (VNF.AccessMethod.HTTPS.toString().equalsIgnoreCase(accessMethod)) { + VnfTemplateDetailVO accessDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.HTTPS_PORT.name().toLowerCase()); + if (accessDetail == null) { + ports.add(ACCESS_DEFAULT_HTTPS_PORT); + } else { + ports.add(NumbersUtil.parseInt(accessDetail.getValue(), ACCESS_DEFAULT_HTTPS_PORT)); + } + } + } + return ports; + } + + private Set getDeviceIdsOfVnfManagementNics(VirtualMachineTemplate template) { + Set deviceIds = new HashSet<>(); + for (VnfTemplateNicVO nic : vnfTemplateNicDao.listByTemplateId(template.getId())) { + if (nic.isManagement()) { + deviceIds.add(nic.getDeviceId()); + } + } + return deviceIds; + } + + protected Map getManagementNetworkAndIp(VirtualMachineTemplate template, UserVm vm) { + Map networkAndIpMap = new HashMap<>(); + Set managementDeviceIds = getDeviceIdsOfVnfManagementNics(template); + for (NicVO nic : nicDao.listByVmId(vm.getId())) { + if (managementDeviceIds.contains((long) nic.getDeviceId()) && nic.getIPv4Address() != null) { + Network network = networkDao.findById(nic.getNetworkId()); + if (network == null || !Network.GuestType.Isolated.equals(network.getGuestType())) { + continue; + } + if (!networkModel.areServicesSupportedInNetwork(network.getId(), Network.Service.StaticNat)) { + LOGGER.info(String.format("Network ID: %s does not support static nat, " + + "skipping this network configuration for VNF appliance", network.getUuid())); + continue; + } + if (network.getVpcId() != null) { + LOGGER.info(String.format("Network ID: %s is a VPC tier, " + + "skipping this network configuration for VNF appliance", network.getUuid())); + continue; + } + if (!networkModel.areServicesSupportedInNetwork(network.getId(), Network.Service.Firewall)) { + LOGGER.info(String.format("Network ID: %s does not support firewall, " + + "skipping this network configuration for VNF appliance", network.getUuid())); + continue; + } + networkAndIpMap.put(network, nic.getIPv4Address()); + } + } + return networkAndIpMap; + } + + @Override + public SecurityGroup createSecurityGroupForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, + DeployVnfApplianceCmd cmd) { + if (zone == null || !zone.isSecurityGroupEnabled()) { + return null; + } + if (!cmd.getVnfConfigureManagement()) { + return null; + } + LOGGER.debug("Creating security group and rules for VNF appliance"); + Set ports = getOpenPortsForVnfAppliance(template); + if (ports.size() == 0) { + LOGGER.debug("No need to create security group and rules for VNF appliance as there is no ports to be open"); + return null; + } + String securityGroupName = VNF_SECURITY_GROUP_NAME.concat(Long.toHexString(System.currentTimeMillis())); + SecurityGroupVO securityGroupVO = securityGroupManager.createSecurityGroup(securityGroupName, + "Security group for VNF appliance", owner.getDomainId(), owner.getId(), owner.getAccountName()); + if (securityGroupVO == null) { + throw new CloudRuntimeException(String.format("Failed to create security group: %s", securityGroupName)); + } + List cidrList = cmd.getVnfCidrlist(); + for (Integer port : ports) { + securityGroupService.authorizeSecurityGroupRule(securityGroupVO.getId(), NetUtils.TCP_PROTO, port, port, + null, null, cidrList, null, SecurityRule.SecurityRuleType.IngressRule); + } + return securityGroupVO; + } + + @Override + public void createIsolatedNetworkRulesForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, + UserVm vm, DeployVnfApplianceCmd cmd) + throws InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException { + + Map networkAndIpMap = getManagementNetworkAndIp(template, vm); + Set ports = getOpenPortsForVnfAppliance(template); + for (Map.Entry entry : networkAndIpMap.entrySet()) { + Network network = entry.getKey(); + LOGGER.debug("Creating network rules for VNF appliance on isolated network " + network.getUuid()); + String ip = entry.getValue(); + IpAddress publicIp = networkService.allocateIP(owner, zone.getId(), network.getId(), null, null); + if (publicIp == null) { + continue; + } + publicIp = ipAddressManager.associateIPToGuestNetwork(publicIp.getId(), network.getId(), false); + if (publicIp.isSourceNat()) { + // If isolated network is not implemented, the first acquired Public IP will be Source NAT IP + publicIp = networkService.allocateIP(owner, zone.getId(), network.getId(), null, null); + if (publicIp == null) { + continue; + } + publicIp = ipAddressManager.associateIPToGuestNetwork(publicIp.getId(), network.getId(), false); + } + final IpAddress publicIpFinal = publicIp; + final List cidrList = cmd.getVnfCidrlist(); + try { + boolean result = rulesService.enableStaticNat(publicIp.getId(), vm.getId(), network.getId(), ip); + if (!result) { + throw new CloudRuntimeException(String.format("Failed to create static nat for vm: %s", vm.getUuid())); + } + } catch (NetworkRuleConflictException e) { + throw new CloudRuntimeException(String.format("Failed to create static nat for vm %s due to %s", vm.getUuid(), e.getMessage())); + } + if (network.getVpcId() == null) { + Transaction.execute(new TransactionCallbackWithExceptionNoReturn<>() { + @Override + public void doInTransactionWithoutResult(final TransactionStatus status) throws CloudRuntimeException { + for (Integer port : ports) { + FirewallRuleVO newFirewallRule = new FirewallRuleVO(null, publicIpFinal.getId(), port, port, NetUtils.TCP_PROTO, + network.getId(), owner.getAccountId(), owner.getDomainId(), FirewallRule.Purpose.Firewall, + cidrList, null, null, null, FirewallRule.TrafficType.Ingress); + newFirewallRule.setDisplay(true); + newFirewallRule.setState(FirewallRule.State.Add); + firewallRulesDao.persist(newFirewallRule); + } + } + }); + firewallService.applyIngressFwRules(publicIp.getId(), owner); + } + LOGGER.debug("Created network rules for VNF appliance on isolated network " + network.getUuid()); + } + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index b976ed6a329..3443e2403dd 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -356,4 +356,5 @@ + diff --git a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java index 29361bfeeae..08ba0955f49 100644 --- a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java +++ b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java @@ -24,7 +24,9 @@ import com.cloud.event.dao.EventJoinDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; import com.cloud.network.Network; +import com.cloud.network.VNF; import com.cloud.network.dao.NetworkVO; +import com.cloud.server.ResourceTag; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.AccountVO; @@ -39,6 +41,8 @@ import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.command.user.event.ListEventsCmd; +import org.apache.cloudstack.api.command.user.resource.ListDetailOptionsCmd; +import org.apache.cloudstack.api.response.DetailOptionsResponse; import org.apache.cloudstack.api.response.EventResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.context.CallContext; @@ -54,10 +58,13 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import static org.mockito.Mockito.when; @@ -197,6 +204,27 @@ public class QueryManagerImplTest { queryManager.searchForEvents(cmd); } + @Test + public void listVnfDetailOptionsCmd() { + ListDetailOptionsCmd cmd = Mockito.mock(ListDetailOptionsCmd.class); + when(cmd.getResourceType()).thenReturn(ResourceTag.ResourceObjectType.VnfTemplate); + + DetailOptionsResponse response = queryManager.listDetailOptions(cmd); + Map> options = response.getDetails(); + + int expectedLength = VNF.AccessDetail.values().length + VNF.VnfDetail.values().length; + Assert.assertEquals(expectedLength, options.size()); + Set keys = options.keySet(); + for (VNF.AccessDetail detail : VNF.AccessDetail.values()) { + Assert.assertTrue(keys.contains(detail.name().toLowerCase())); + } + for (VNF.VnfDetail detail : VNF.VnfDetail.values()) { + Assert.assertTrue(keys.contains(detail.name().toLowerCase())); + } + List expectedAccessMethods = Arrays.stream(VNF.AccessMethod.values()).map(method -> method.toString()).sorted().collect(Collectors.toList()); + Assert.assertEquals(expectedAccessMethods, options.get(VNF.AccessDetail.ACCESS_METHODS.name().toLowerCase())); + + } @Test public void applyPublicTemplateRestrictionsTestDoesNotApplyRestrictionsWhenCallerIsRootAdmin() { Mockito.when(accountMock.getType()).thenReturn(Account.Type.ADMIN); diff --git a/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java b/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java index df25eb88332..94d6722a7c5 100755 --- a/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java +++ b/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java @@ -19,17 +19,25 @@ package com.cloud.api.query.dao; import com.cloud.api.query.vo.TemplateJoinVO; import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.Storage; +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.storage.dao.VnfTemplateDetailsDao; +import com.cloud.storage.dao.VnfTemplateNicDao; import com.cloud.template.TemplateManager; import com.cloud.user.Account; import org.apache.cloudstack.api.response.TemplateResponse; +import org.apache.cloudstack.api.response.VnfTemplateResponse; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.Arrays; import java.util.Date; import java.util.Map; @@ -39,6 +47,12 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas @InjectMocks private TemplateJoinDaoImpl _templateJoinDaoImpl; + @Mock + private VnfTemplateNicDao vnfTemplateNicDao; + + @Mock + private VnfTemplateDetailsDao vnfTemplateDetailsDao; + private TemplateJoinVO template = new TemplateJoinVO(); private TemplateResponse templateResponse = new TemplateResponse(); @@ -60,6 +74,8 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas private String domainName = "ROOT"; private String detailName = "detail_name1"; private String detailValue = "detail_val"; + private Storage.TemplateType templateType = Storage.TemplateType.VNF; + private Long templateId = 101L; @Before public void setup() { @@ -68,7 +84,7 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas } @Test - public void testUpdateTemplateTagInfo(){ + public void testUpdateTemplateTagInfo() { testUpdateTagInformation(_templateJoinDaoImpl, template, templateResponse); } @@ -89,8 +105,8 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas Assert.assertEquals(accountName, ReflectionTestUtils.getField(response, "account")); Assert.assertEquals(domainUuid, ReflectionTestUtils.getField(response, "domainId")); Assert.assertEquals(domainName, ReflectionTestUtils.getField(response, "domainName")); - Assert.assertTrue(((Map)ReflectionTestUtils.getField(response, "details")).containsKey(detailName)); - Assert.assertEquals(detailValue, ((Map)ReflectionTestUtils.getField(response, "details")).get(detailName)); + Assert.assertTrue(((Map) ReflectionTestUtils.getField(response, "details")).containsKey(detailName)); + Assert.assertEquals(detailValue, ((Map) ReflectionTestUtils.getField(response, "details")).get(detailName)); } private void populateTemplateJoinVO() { @@ -111,5 +127,27 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas ReflectionTestUtils.setField(template, "domainName", domainName); ReflectionTestUtils.setField(template, "detailName", detailName); ReflectionTestUtils.setField(template, "detailValue", detailValue); + ReflectionTestUtils.setField(template, "templateType", templateType); + } + + @Test + public void testNewUpdateResponseForVnf() { + ReflectionTestUtils.setField(template, "id", templateId); + ReflectionTestUtils.setField(template, "templateType", templateType); + + VnfTemplateNicVO vnfNic1 = new VnfTemplateNicVO(templateId, 0L, "eth0", true, true, "first"); + VnfTemplateNicVO vnfNic2 = new VnfTemplateNicVO(templateId, 1L, "eth1", true, true, "second"); + Mockito.doReturn(Arrays.asList(vnfNic1, vnfNic2)).when(vnfTemplateNicDao).listByTemplateId(templateId); + + VnfTemplateDetailVO detail1 = new VnfTemplateDetailVO(templateId, "name1", "value1", true); + VnfTemplateDetailVO detail2 = new VnfTemplateDetailVO(templateId, "name2", "value2", true); + VnfTemplateDetailVO detail3 = new VnfTemplateDetailVO(templateId, "name3", "value3", true); + Mockito.doReturn(Arrays.asList(detail1, detail2, detail3)).when(vnfTemplateDetailsDao).listDetails(templateId); + + final TemplateResponse response = _templateJoinDaoImpl.newUpdateResponse(template); + Assert.assertTrue(response instanceof VnfTemplateResponse); + Assert.assertEquals(2, ((VnfTemplateResponse)response).getVnfNics().size()); + Assert.assertEquals(3, ((VnfTemplateResponse)response).getVnfDetails().size()); + } } diff --git a/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java b/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java index a4f28c9dc7e..320c556fc51 100755 --- a/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java +++ b/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java @@ -17,23 +17,78 @@ package com.cloud.api.query.dao; import com.cloud.api.query.vo.UserVmJoinVO; +import com.cloud.storage.Storage; +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.storage.dao.VnfTemplateDetailsDao; +import com.cloud.storage.dao.VnfTemplateNicDao; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.UserStatisticsVO; +import com.cloud.user.dao.UserDao; +import com.cloud.user.dao.UserStatisticsDao; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.dao.UserVmDetailsDao; +import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.response.UserVmResponse; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import java.util.Arrays; +import java.util.EnumSet; + +import static org.mockito.ArgumentMatchers.nullable; + @RunWith(MockitoJUnitRunner.class) public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseTest { @InjectMocks private UserVmJoinDaoImpl _userVmJoinDaoImpl; + @Mock + private UserDao userDao; + + @Mock + private AnnotationDao annotationDao; + + @Mock + private AccountManager accountMgr; + + @Mock + private UserVmDetailsDao _userVmDetailsDao; + + @Mock + private UserStatisticsDao userStatsDao; + + @Mock + private VnfTemplateNicDao vnfTemplateNicDao; + + @Mock + private VnfTemplateDetailsDao vnfTemplateDetailsDao; + private UserVmJoinVO userVm = new UserVmJoinVO(); private UserVmResponse userVmResponse = new UserVmResponse(); + @Mock + Account caller; + + @Mock + UserVmJoinVO userVmMock; + + private Long vmId = 100L; + + private Long templateId = 101L; + @Before public void setup() { prepareSetup(); @@ -50,4 +105,51 @@ public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseT testUpdateTagInformation(_userVmJoinDaoImpl, userVm, userVmResponse); } + private void prepareNewUserVmResponseForVnfAppliance() { + Mockito.when(userVmMock.getId()).thenReturn(vmId); + Mockito.when(userVmMock.getTemplateId()).thenReturn(templateId); + Mockito.when(userVmMock.getTemplateType()).thenReturn(Storage.TemplateType.VNF); + + Mockito.when(caller.getId()).thenReturn(2L); + Mockito.when(accountMgr.isRootAdmin(nullable(Long.class))).thenReturn(true); + + SearchBuilder searchBuilderMock = Mockito.mock(SearchBuilder.class); + Mockito.doReturn(searchBuilderMock).when(userStatsDao).createSearchBuilder(); + UserStatisticsVO userStatisticsVOMock = Mockito.mock(UserStatisticsVO.class); + Mockito.when(searchBuilderMock.entity()).thenReturn(userStatisticsVOMock); + SearchCriteria searchCriteriaMock = Mockito.mock(SearchCriteria.class); + Mockito.doReturn(searchCriteriaMock).when(searchBuilderMock).create(); + Mockito.doReturn(Arrays.asList()).when(userStatsDao).search(searchCriteriaMock, null); + + VnfTemplateNicVO vnfNic1 = new VnfTemplateNicVO(templateId, 0L, "eth0", true, true, "first"); + VnfTemplateNicVO vnfNic2 = new VnfTemplateNicVO(templateId, 1L, "eth1", true, true, "second"); + Mockito.doReturn(Arrays.asList(vnfNic1, vnfNic2)).when(vnfTemplateNicDao).listByTemplateId(templateId); + + VnfTemplateDetailVO detail1 = new VnfTemplateDetailVO(templateId, "name1", "value1", true); + VnfTemplateDetailVO detail2 = new VnfTemplateDetailVO(templateId, "name2", "value2", true); + VnfTemplateDetailVO detail3 = new VnfTemplateDetailVO(templateId, "name3", "value3", true); + Mockito.doReturn(Arrays.asList(detail1, detail2, detail3)).when(vnfTemplateDetailsDao).listDetails(templateId); + } + + @Test + public void testNewUserVmResponseForVnfAppliance() { + prepareNewUserVmResponseForVnfAppliance(); + + UserVmResponse response = _userVmJoinDaoImpl.newUserVmResponse(ResponseObject.ResponseView.Full, "virtualmachine", userVmMock, + EnumSet.of(ApiConstants.VMDetails.all), null, null, caller); + + Assert.assertEquals(2, response.getVnfNics().size()); + Assert.assertEquals(3, response.getVnfDetails().size()); + } + + @Test + public void testNewUserVmResponseForVnfApplianceVnfNics() { + prepareNewUserVmResponseForVnfAppliance(); + + UserVmResponse response = _userVmJoinDaoImpl.newUserVmResponse(ResponseObject.ResponseView.Full, "virtualmachine", userVmMock, + EnumSet.of(ApiConstants.VMDetails.vnfnics), null, null, caller); + + Assert.assertEquals(2, response.getVnfNics().size()); + Assert.assertEquals(3, response.getVnfDetails().size()); + } } diff --git a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java index 43c3b3f25c0..cb4d701d8a5 100755 --- a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java +++ b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java @@ -45,6 +45,10 @@ import javax.inject.Inject; import org.apache.cloudstack.api.command.user.template.CreateTemplateCmd; import org.apache.cloudstack.api.command.user.template.DeleteTemplateCmd; +import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; @@ -68,6 +72,7 @@ import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.cloudstack.storage.template.VnfTemplateManager; import org.apache.cloudstack.test.utils.SpringUtils; import org.junit.After; import org.junit.Assert; @@ -193,6 +198,8 @@ public class TemplateManagerImplTest { @Inject AccountManager _accountMgr; + @Inject + VnfTemplateManager vnfTemplateManager; public class CustomThreadPoolExecutor extends ThreadPoolExecutor { AtomicInteger ai = new AtomicInteger(0); @@ -589,6 +596,109 @@ public class TemplateManagerImplTest { Assert.assertEquals(template, resultTemplate); } + @Test + public void testRegisterTemplateWithTemplateType() { + RegisterTemplateCmd cmd = Mockito.mock(RegisterTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + Assert.assertEquals(Storage.TemplateType.SYSTEM, type); + } + + @Test + public void testRegisterTemplateWithoutTemplateType() { + RegisterTemplateCmd cmd = Mockito.mock(RegisterTemplateCmd.class); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + Assert.assertEquals(Storage.TemplateType.USER, type); + } + + @Test(expected = InvalidParameterValueException.class) + public void testRegisterTemplateWithSystemTemplateTypeByUser() { + RegisterVnfTemplateCmd cmd = Mockito.mock(RegisterVnfTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + } + + @Test(expected = InvalidParameterValueException.class) + public void testRegisterVnfTemplateWithTemplateType() { + RegisterVnfTemplateCmd cmd = Mockito.mock(RegisterVnfTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + Assert.assertEquals(Storage.TemplateType.VNF, type); + } + + @Test + public void testRegisterVnfTemplateWithoutTemplateType() { + RegisterVnfTemplateCmd cmd = Mockito.mock(RegisterVnfTemplateCmd.class); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + Assert.assertEquals(Storage.TemplateType.VNF, type); + } + + @Test + public void testUpdateTemplateWithTemplateType() { + UpdateTemplateCmd cmd = Mockito.mock(UpdateTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + Assert.assertEquals(Storage.TemplateType.SYSTEM, type); + } + + @Test + public void testUpdateTemplateWithoutTemplateType() { + UpdateTemplateCmd cmd = Mockito.mock(UpdateTemplateCmd.class); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + Assert.assertNull(type); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateTemplateWithInvalidTemplateType() { + UpdateTemplateCmd cmd = Mockito.mock(UpdateTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn("invalidtype"); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateTemplateWithInvalidTemplateTypeForRouting() { + UpdateTemplateCmd cmd = Mockito.mock(UpdateTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.USER.toString()); + when(cmd.isRoutingType()).thenReturn(true); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateTemplateWithInvalidCrossZonesForSystem() { + UpdateTemplateCmd cmd = Mockito.mock(UpdateTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, false); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateTemplateWithSystemTemplateTypeByUser() { + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateVnfTemplateWithTemplateType() { + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + Assert.assertEquals(Storage.TemplateType.VNF, type); + } + + @Test + public void testUpdateVnfTemplateWithoutTemplateType() { + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + Assert.assertNull(type); + } + + @Test + public void testDeleteTemplateWithTemplateType() { + DeleteTemplateCmd cmd = new DeleteTemplateCmd(); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + Assert.assertNull(type); + } + @Configuration @ComponentScan(basePackageClasses = {TemplateManagerImpl.class}, includeFilters = {@ComponentScan.Filter(value = TestConfiguration.Library.class, type = FilterType.CUSTOM)}, @@ -790,6 +900,11 @@ public class TemplateManagerImplTest { return Mockito.mock(HypervisorGuruManager.class); } + @Bean + public VnfTemplateManager vnfTemplateManager() { + return Mockito.mock(VnfTemplateManager.class); + } + @Bean public SnapshotHelper snapshotHelper() { return Mockito.mock(SnapshotHelper.class); diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index 7886e70920c..7acd347d7fe 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.vm; +import com.cloud.api.query.dao.ServiceOfferingJoinDao; +import com.cloud.api.query.vo.ServiceOfferingJoinVO; import com.cloud.configuration.Resource; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; @@ -37,6 +39,7 @@ import com.cloud.hypervisor.Hypervisor; import com.cloud.network.NetworkModel; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; +import com.cloud.network.security.SecurityGroupVO; import com.cloud.offering.ServiceOffering; import com.cloud.server.ManagementService; import com.cloud.service.ServiceOfferingVO; @@ -44,6 +47,7 @@ import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.GuestOSVO; import com.cloud.storage.ScopeType; +import com.cloud.storage.Storage; import com.cloud.storage.VMTemplateVO; import com.cloud.storage.Volume; import com.cloud.storage.VolumeApiService; @@ -72,6 +76,7 @@ import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.UserVmDetailsDao; import org.apache.cloudstack.api.BaseCmd.HTTPMethod; import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; @@ -79,6 +84,7 @@ import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.template.VnfTemplateManager; import org.apache.cloudstack.userdata.UserDataManager; import org.junit.After; import org.junit.Assert; @@ -229,10 +235,20 @@ public class UserVmManagerImplTest { @Mock VirtualMachineProfile virtualMachineProfile; + @Mock + VirtualMachineTemplate templateMock; + + @Mock + VnfTemplateManager vnfTemplateManager; + + @Mock + ServiceOfferingJoinDao serviceOfferingJoinDao; + private static final long vmId = 1l; private static final long zoneId = 2L; private static final long accountId = 3L; private static final long serviceOfferingId = 10L; + private static final long templateId = 11L; private static final long GiB_TO_BYTES = 1024 * 1024 * 1024; @@ -941,6 +957,43 @@ public class UserVmManagerImplTest { userVmManagerImpl.createVirtualMachine(deployVMCmd); } + @Test + public void createVirtualMachine() throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { + DeployVMCmd deployVMCmd = new DeployVMCmd(); + ReflectionTestUtils.setField(deployVMCmd, "zoneId", zoneId); + ReflectionTestUtils.setField(deployVMCmd, "templateId", templateId); + ReflectionTestUtils.setField(deployVMCmd, "serviceOfferingId", serviceOfferingId); + deployVMCmd._accountService = accountService; + + when(accountService.finalyzeAccountId(nullable(String.class), nullable(Long.class), nullable(Long.class), eq(true))).thenReturn(accountId); + when(accountService.getActiveAccountById(accountId)).thenReturn(account); + when(entityManager.findById(DataCenter.class, zoneId)).thenReturn(_dcMock); + when(entityManager.findById(ServiceOffering.class, serviceOfferingId)).thenReturn(serviceOffering); + when(serviceOffering.getState()).thenReturn(ServiceOffering.State.Active); + + when(entityManager.findById(VirtualMachineTemplate.class, templateId)).thenReturn(templateMock); + when(templateMock.getTemplateType()).thenReturn(Storage.TemplateType.VNF); + when(templateMock.isDeployAsIs()).thenReturn(false); + when(templateMock.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); + when(templateMock.getUserDataId()).thenReturn(null); + Mockito.doNothing().when(vnfTemplateManager).validateVnfApplianceNics(any(), nullable(List.class)); + + ServiceOfferingJoinVO svcOfferingMock = Mockito.mock(ServiceOfferingJoinVO.class); + when(serviceOfferingJoinDao.findById(anyLong())).thenReturn(svcOfferingMock); + when(_dcMock.isLocalStorageEnabled()).thenReturn(true); + when(_dcMock.getNetworkType()).thenReturn(DataCenter.NetworkType.Basic); + Mockito.doReturn(userVmVoMock).when(userVmManagerImpl).createBasicSecurityGroupVirtualMachine(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), nullable(Boolean.class), any(), any(), any(), + any(), any(), any(), any(), eq(true), any()); + + UserVm result = userVmManagerImpl.createVirtualMachine(deployVMCmd); + assertEquals(userVmVoMock, result); + Mockito.verify(vnfTemplateManager).validateVnfApplianceNics(templateMock, null); + Mockito.verify(userVmManagerImpl).createBasicSecurityGroupVirtualMachine(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), nullable(Boolean.class), any(), any(), any(), + any(), any(), any(), any(), eq(true), any()); + } + private List mockVolumesForIsAnyVmVolumeUsingLocalStorageTest(int localVolumes, int nonLocalVolumes) { List volumes = new ArrayList<>(); for (int i=0; i< localVolumes + nonLocalVolumes; ++i) { @@ -1076,6 +1129,24 @@ public class UserVmManagerImplTest { Mockito.verify(userVmDao).update(vmId, userVmVoMock); } + @Test + public void testGetSecurityGroupIdList() { + DeployVnfApplianceCmd cmd = Mockito.mock(DeployVnfApplianceCmd.class); + Mockito.doReturn(new ArrayList()).when(userVmManagerImpl).getSecurityGroupIdList(cmd); + SecurityGroupVO securityGroupVO = Mockito.mock(SecurityGroupVO.class); + long securityGroupId = 100L; + when(securityGroupVO.getId()).thenReturn(securityGroupId); + Mockito.doReturn(securityGroupVO).when(vnfTemplateManager).createSecurityGroupForVnfAppliance(any(), any(), any(), any(DeployVnfApplianceCmd.class)); + + List securityGroupIds = userVmManagerImpl.getSecurityGroupIdList(cmd, null, null, null); + + Assert.assertEquals(1, securityGroupIds.size()); + Assert.assertEquals(securityGroupId, securityGroupIds.get(0).longValue()); + + Mockito.verify(userVmManagerImpl).getSecurityGroupIdList(cmd); + Mockito.verify(vnfTemplateManager).createSecurityGroupForVnfAppliance(any(), any(), any(), any(DeployVnfApplianceCmd.class)); + } + @Test public void getCurrentVmPasswordOrDefineNewPasswordTestTemplateIsNotPasswordEnabledReturnPreDefinedString() { String expected = "saved_password"; diff --git a/server/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImplTest.java new file mode 100644 index 00000000000..c3fa0d62604 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImplTest.java @@ -0,0 +1,389 @@ +// 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.storage.template; + +import com.cloud.dc.DataCenter; +import com.cloud.exception.InsufficientAddressCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.IpAddressManager; +import com.cloud.network.Network; +import com.cloud.network.NetworkModel; +import com.cloud.network.NetworkService; +import com.cloud.network.VNF; +import com.cloud.network.dao.FirewallRulesDao; +import com.cloud.network.dao.IPAddressVO; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.firewall.FirewallService; +import com.cloud.network.rules.FirewallRuleVO; +import com.cloud.network.rules.RulesService; +import com.cloud.network.security.SecurityGroup; +import com.cloud.network.security.SecurityGroupManager; +import com.cloud.network.security.SecurityGroupRuleVO; +import com.cloud.network.security.SecurityGroupService; +import com.cloud.network.security.SecurityGroupVO; +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.storage.dao.VnfTemplateDetailsDao; +import com.cloud.storage.dao.VnfTemplateNicDao; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.vm.NicVO; +import com.cloud.vm.dao.NicDao; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; + +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class VnfTemplateManagerImplTest { + + @Spy + @InjectMocks + VnfTemplateManagerImpl vnfTemplateManagerImpl; + + @Mock + VnfTemplateDetailsDao vnfTemplateDetailsDao; + @Mock + VnfTemplateNicDao vnfTemplateNicDao; + + @Mock + VirtualMachineTemplate template; + + @Mock + NicDao nicDao; + + @Mock + NetworkDao networkDao; + + @Mock + NetworkModel networkModel; + + @Mock + SecurityGroupManager securityGroupManager; + + @Mock + SecurityGroupService securityGroupService; + + @Mock + NetworkService networkService; + + @Mock + IpAddressManager ipAddressManager; + + @Mock + RulesService rulesService; + + @Mock + FirewallRulesDao firewallRulesDao; + + @Mock + FirewallService firewallService; + + final static long templateId = 100L; + final static long vmId = 101L; + final static long networkId = 101L; + final static long securityGroupId = 102L; + final static long zoneId = 103L; + final static long publicIpId = 104L; + final static String ipAddress = "10.10.10.10"; + final static Integer sshPort = 2222; + final static Integer httpPort = 8080; + final static Integer httpsPort = 8443; + final Map vnfNics = new HashMap<>(); + final Map vnfDetails = new HashMap<>(); + + @Before + public void setUp() { + vnfNics.put("0", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "1"), + Map.entry("name", "eth1"), + Map.entry("required", "true"), + Map.entry("description", "The second NIC of VNF appliance") + ))); + vnfNics.put("1", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "2"), + Map.entry("name", "eth2"), + Map.entry("required", "false"), + Map.entry("description", "The third NIC of VNF appliance") + ))); + vnfNics.put("2", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "0"), + Map.entry("name", "eth0"), + Map.entry("description", "The first NIC of VNF appliance") + ))); + + vnfDetails.put("0", new HashMap<>(Map.ofEntries( + Map.entry("accessMethods", "console,http,https"), + Map.entry("username", "admin"), + Map.entry("password", "password"), + Map.entry("version", "4.19.0"), + Map.entry("vendor", "cloudstack") + ))); + + VnfTemplateNicVO vnfNic1 = new VnfTemplateNicVO(templateId, 0L, "eth0", true, true, "first"); + VnfTemplateNicVO vnfNic2 = new VnfTemplateNicVO(templateId, 1L, "eth1", true, true, "second"); + VnfTemplateNicVO vnfNic3 = new VnfTemplateNicVO(templateId, 2L, "eth2", false, true, "third"); + Mockito.doReturn(Arrays.asList(vnfNic1, vnfNic2, vnfNic3)).when(vnfTemplateNicDao).listByTemplateId(templateId); + + when(template.getId()).thenReturn(templateId); + } + + @Test + public void testPersistVnfTemplateRegister() { + RegisterVnfTemplateCmd cmd = new RegisterVnfTemplateCmd(); + ReflectionTestUtils.setField(cmd,"vnfNics", vnfNics); + ReflectionTestUtils.setField(cmd,"vnfDetails", vnfDetails); + + vnfTemplateManagerImpl.persistVnfTemplate(templateId, cmd); + + Mockito.verify(vnfTemplateNicDao, Mockito.times(vnfNics.size())).persist(any(VnfTemplateNicVO.class)); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(0)).removeDetails(templateId); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(5)).addDetail(eq(templateId), anyString(), anyString(), eq(true)); + } + + @Test + public void testPersistVnfTemplateUpdate() { + UpdateVnfTemplateCmd cmd = new UpdateVnfTemplateCmd(); + ReflectionTestUtils.setField(cmd,"vnfNics", vnfNics); + ReflectionTestUtils.setField(cmd,"vnfDetails", vnfDetails); + + vnfTemplateManagerImpl.updateVnfTemplate(templateId, cmd); + + Mockito.verify(vnfTemplateNicDao, Mockito.times(vnfNics.size())).persist(any(VnfTemplateNicVO.class)); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(1)).removeDetails(templateId); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(5)).addDetail(eq(templateId), anyString(), anyString(), eq(true)); + } + + @Test + public void testPersistVnfTemplateUpdateWithoutNics() { + UpdateVnfTemplateCmd cmd = new UpdateVnfTemplateCmd(); + ReflectionTestUtils.setField(cmd,"vnfDetails", vnfDetails); + ReflectionTestUtils.setField(cmd,"cleanupVnfNics", true); + + vnfTemplateManagerImpl.updateVnfTemplate(templateId, cmd); + + Mockito.verify(vnfTemplateNicDao, Mockito.times(1)).deleteByTemplateId(templateId); + Mockito.verify(vnfTemplateNicDao, Mockito.times(0)).persist(any(VnfTemplateNicVO.class)); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(1)).removeDetails(templateId); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(5)).addDetail(eq(templateId), anyString(), anyString(), eq(true)); + } + + @Test + public void testPersistVnfTemplateUpdateWithoutDetails() { + UpdateVnfTemplateCmd cmd = new UpdateVnfTemplateCmd(); + ReflectionTestUtils.setField(cmd,"vnfNics", vnfNics); + ReflectionTestUtils.setField(cmd,"cleanupVnfDetails", true); + + vnfTemplateManagerImpl.updateVnfTemplate(templateId, cmd); + + Mockito.verify(vnfTemplateNicDao, Mockito.times(vnfNics.size())).persist(any(VnfTemplateNicVO.class)); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(1)).removeDetails(templateId); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(0)).addDetail(eq(templateId), anyString(), anyString(), eq(true)); + } + + @Test + public void testValidateVnfApplianceNicsWithRequiredNics() { + List networkIds = Arrays.asList(200L, 201L); + vnfTemplateManagerImpl.validateVnfApplianceNics(template, networkIds); + } + + @Test + public void testValidateVnfApplianceNicsWithAllNics() { + List networkIds = Arrays.asList(200L, 201L, 202L); + vnfTemplateManagerImpl.validateVnfApplianceNics(template, networkIds); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfApplianceNicsWithEmptyList() { + List networkIds = new ArrayList<>(); + vnfTemplateManagerImpl.validateVnfApplianceNics(template, networkIds); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfApplianceNicsWithMissingNetworkId() { + List networkIds = Arrays.asList(200L); + vnfTemplateManagerImpl.validateVnfApplianceNics(template, networkIds); + } + + @Test + public void testGetManagementNetworkAndIp() { + when(template.getId()).thenReturn(templateId); + VnfTemplateNicVO vnfNic1 = new VnfTemplateNicVO(templateId, 0L, "eth0", true, true, "first"); + VnfTemplateNicVO vnfNic2 = new VnfTemplateNicVO(templateId, 1L, "eth1", true, false, "second"); + VnfTemplateNicVO vnfNic3 = new VnfTemplateNicVO(templateId, 2L, "eth2", false, false, "third"); + Mockito.doReturn(Arrays.asList(vnfNic1, vnfNic2, vnfNic3)).when(vnfTemplateNicDao).listByTemplateId(templateId); + + UserVm vm = Mockito.mock(UserVm.class); + when(vm.getId()).thenReturn(vmId); + NicVO nic1 = Mockito.mock(NicVO.class); + NicVO nic2 = Mockito.mock(NicVO.class); + NicVO nic3 = Mockito.mock(NicVO.class); + when(nic1.getDeviceId()).thenReturn(0); + when(nic1.getIPv4Address()).thenReturn(ipAddress); + when(nic1.getNetworkId()).thenReturn(networkId); + when(nic2.getDeviceId()).thenReturn(1); + when(nic3.getDeviceId()).thenReturn(2); + Mockito.doReturn(Arrays.asList(nic1, nic2, nic3)).when(nicDao).listByVmId(vmId); + + NetworkVO network = Mockito.mock(NetworkVO.class); + when(network.getId()).thenReturn(networkId); + when(network.getGuestType()).thenReturn(Network.GuestType.Isolated); + when(network.getVpcId()).thenReturn(null); + Mockito.doReturn(network).when(networkDao).findById(networkId); + when(networkModel.areServicesSupportedInNetwork(networkId, Network.Service.StaticNat)).thenReturn(true); + when(networkModel.areServicesSupportedInNetwork(networkId, Network.Service.Firewall)).thenReturn(true); + + Map networkAndIpMap = vnfTemplateManagerImpl.getManagementNetworkAndIp(template, vm); + + Assert.assertEquals(1, networkAndIpMap.size()); + Assert.assertTrue(networkAndIpMap.containsKey(network)); + Assert.assertTrue(networkAndIpMap.containsValue(ipAddress)); + } + + @Test + public void testGetOpenPortsForVnfAppliance() { + when(template.getId()).thenReturn(templateId); + VnfTemplateDetailVO accessMethodsDetail = Mockito.mock(VnfTemplateDetailVO.class); + when(accessMethodsDetail.getValue()).thenReturn("console,ssh-password,http,https"); + when(vnfTemplateDetailsDao.findDetail(templateId, VNF.AccessDetail.ACCESS_METHODS.name().toLowerCase())).thenReturn(accessMethodsDetail); + + VnfTemplateDetailVO sshPortDetail = Mockito.mock(VnfTemplateDetailVO.class); + when(sshPortDetail.getValue()).thenReturn(String.valueOf(sshPort)); + when(vnfTemplateDetailsDao.findDetail(templateId, VNF.AccessDetail.SSH_PORT.name().toLowerCase())).thenReturn(sshPortDetail); + + VnfTemplateDetailVO httpPortDetail = Mockito.mock(VnfTemplateDetailVO.class); + when(httpPortDetail.getValue()).thenReturn(String.valueOf(httpPort)); + when(vnfTemplateDetailsDao.findDetail(templateId, VNF.AccessDetail.HTTP_PORT.name().toLowerCase())).thenReturn(httpPortDetail); + + VnfTemplateDetailVO httpsPortDetail = Mockito.mock(VnfTemplateDetailVO.class); + when(httpsPortDetail.getValue()).thenReturn(String.valueOf(httpsPort)); + when(vnfTemplateDetailsDao.findDetail(templateId, VNF.AccessDetail.HTTPS_PORT.name().toLowerCase())).thenReturn(httpsPortDetail); + + Set ports = vnfTemplateManagerImpl.getOpenPortsForVnfAppliance(template); + + Assert.assertEquals(3, ports.size()); + Assert.assertTrue(ports.contains(sshPort)); + Assert.assertTrue(ports.contains(httpPort)); + Assert.assertTrue(ports.contains(httpsPort)); + } + + @Test + public void testCreateSecurityGroupForVnfAppliance() { + DataCenter zone = Mockito.mock(DataCenter.class); + when(zone.isSecurityGroupEnabled()).thenReturn(true); + + DeployVnfApplianceCmd cmd = Mockito.mock(DeployVnfApplianceCmd.class); + when(cmd.getVnfConfigureManagement()).thenReturn(true); + when(cmd.getVnfCidrlist()).thenReturn(Arrays.asList("0.0.0.0/0")); + + Set ports = new HashSet<>(); + ports.add(sshPort); + ports.add(httpPort); + ports.add(httpsPort); + Mockito.doReturn(ports).when(vnfTemplateManagerImpl).getOpenPortsForVnfAppliance(template); + + Account owner = Mockito.mock(Account.class); + when(owner.getDomainId()).thenReturn(1L); + when(owner.getAccountName()).thenReturn("admin"); + + SecurityGroupVO securityGroupVO = Mockito.mock(SecurityGroupVO.class); + when(securityGroupVO.getId()).thenReturn(securityGroupId); + Mockito.doReturn(securityGroupVO).when(securityGroupManager).createSecurityGroup(anyString(), anyString(), anyLong(), anyLong(), anyString()); + SecurityGroupRuleVO securityGroupRuleVO = Mockito.mock(SecurityGroupRuleVO.class); + Mockito.doReturn(Arrays.asList(securityGroupRuleVO)).when(securityGroupService).authorizeSecurityGroupRule(anyLong(), anyString(), anyInt(), anyInt(), + any(), any(), any(), any(), any()); + + SecurityGroup result = vnfTemplateManagerImpl.createSecurityGroupForVnfAppliance(zone, template, owner, cmd); + + Assert.assertEquals(result, securityGroupVO); + Mockito.verify(securityGroupService, Mockito.times(3)).authorizeSecurityGroupRule(anyLong(), anyString(), anyInt(), anyInt(), + any(), any(), any(), any(), any()); + } + + @Test + public void testCreateIsolatedNetworkRulesForVnfAppliance() throws InsufficientAddressCapacityException, ResourceUnavailableException, + ResourceAllocationException, NetworkRuleConflictException { + DataCenter zone = Mockito.mock(DataCenter.class); + when(zone.getId()).thenReturn(zoneId); + Account owner = Mockito.mock(Account.class); + UserVm vm = Mockito.mock(UserVm.class); + when(vm.getId()).thenReturn(vmId); + DeployVnfApplianceCmd cmd = Mockito.mock(DeployVnfApplianceCmd.class); + + Map networkAndIpMap = new HashMap<>(); + NetworkVO network = Mockito.mock(NetworkVO.class); + when(network.getId()).thenReturn(networkId); + when(network.getVpcId()).thenReturn(null); + networkAndIpMap.put(network, ipAddress); + Mockito.doReturn(networkAndIpMap).when(vnfTemplateManagerImpl).getManagementNetworkAndIp(template, vm); + + Set ports = new HashSet<>(); + ports.add(sshPort); + ports.add(httpPort); + ports.add(httpsPort); + Mockito.doReturn(ports).when(vnfTemplateManagerImpl).getOpenPortsForVnfAppliance(template); + + FirewallRuleVO firewallRuleVO = Mockito.mock(FirewallRuleVO.class); + + IPAddressVO publicIp = Mockito.mock(IPAddressVO.class); + when(publicIp.getId()).thenReturn(publicIpId); + when(publicIp.isSourceNat()).thenReturn(true).thenReturn(false); + Mockito.doReturn(publicIp).when(networkService).allocateIP(owner, zoneId, networkId, null, null); + Mockito.doReturn(publicIp).when(ipAddressManager).associateIPToGuestNetwork(publicIpId, networkId, false); + Mockito.doReturn(true).when(rulesService).enableStaticNat(publicIpId, vmId, networkId, ipAddress); + when(firewallRulesDao.persist(any())).thenReturn(firewallRuleVO); + Mockito.doReturn(true).when(firewallService).applyIngressFwRules(publicIpId, owner); + + vnfTemplateManagerImpl.createIsolatedNetworkRulesForVnfAppliance(zone, template, owner, vm, cmd); + + Mockito.verify(networkService, Mockito.times(2)).allocateIP(owner, zoneId, networkId, null, null); + Mockito.verify(ipAddressManager, Mockito.times(2)).associateIPToGuestNetwork(publicIpId, networkId, false); + Mockito.verify(rulesService, Mockito.times(1)).enableStaticNat(publicIpId, vmId, networkId, ipAddress); + Mockito.verify(firewallRulesDao, Mockito.times(3)).persist(any()); + Mockito.verify(firewallService, Mockito.times(1)).applyIngressFwRules(publicIpId, owner); + } +} diff --git a/test/integration/smoke/test_vnf_templates.py b/test/integration/smoke/test_vnf_templates.py new file mode 100644 index 00000000000..f963ce41b38 --- /dev/null +++ b/test/integration/smoke/test_vnf_templates.py @@ -0,0 +1,341 @@ +# 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. + +""" Smoke tests for VNF templates/appliances +""" +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.base import (Account, + Domain, + Configurations, + ServiceOffering, + VirtualMachine, + Network, + NetworkOffering, + VnfAppliance, + VnfTemplate, + Zone) +from marvin.lib.common import get_zone, get_template +from nose.plugins.attrib import attr + +import time + +VNF_NICS = [{"deviceid": "0", "name": "WAN", "required": "true", "description": "Public WAN"}, + {"deviceid": "1", "name": "LAN-1", "required": "true", "description": "Private LAN-1"}] +NEW_VNF_NICS = [{"deviceid": "0", "name": "WAN", "required": "true", "description": "Public WAN"}, + {"deviceid": "1", "name": "LAN-1", "required": "true", "description": "Private LAN-1"}, + {"deviceid": "2", "name": "LAN-2", "required": "false", "description": "Private LAN-2"}] +VNF_DETAILS = [{"access_methods": "console,https,http", "username": "root"}] +NEW_VNF_DETAILS = [{"access_methods": "console,https,http", "username": "root", "password": "cloudstack"}] + +class TestVnfTemplates(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + + testClient = super(TestVnfTemplates, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls._cleanup = [] + cls.services = testClient.getParsedTestDataConfig() + + # Get Zone, Domain and templates + cls.hypervisor = cls.testClient.getHypervisorInfo() + zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.zone = Zone(zone.__dict__) + cls.template = get_template(cls.apiclient, cls.zone.id) + + cls.domain = Domain.create( + cls.apiclient, + cls.services["domain"] + ) + cls._cleanup.append(cls.domain) + + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + admin=True, + domainid=cls.domain.id + ) + cls._cleanup.append(cls.account) + + cls.user = cls.account.user[0] + cls.user_apiclient = cls.testClient.getUserApiClient( + cls.user.username, cls.domain.name + ) + + cls.service_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["big"] + ) + cls._cleanup.append(cls.service_offering) + + cls.vnf_template_config = { + "name": "pfsense", + "displaytext": "pfsense", + "format": cls.template.format, + "url": cls.template.url, + "requireshvm": "True", + "ispublic": "True", + "isextractable": "True", + "hypervisor": cls.hypervisor, + "zoneid": cls.zone.id, + "ostype": "FreeBSD 12 (64-bit)", + "directdownload": False + } + + cls.initial_setting = Configurations.list( + cls.apiclient, + name="vnf.template.appliance.enabled")[0].value + + Configurations.update(cls.apiclient, "vnf.template.appliance.enabled", "true") + + cls.vnf_templates = [] + + @classmethod + def tearDownClass(cls): + Configurations.update(cls.apiclient, "vnf.template.appliance.enabled", cls.initial_setting) + if len(cls.vnf_templates) > 0: + for vnf_template in cls.vnf_templates: + vnf_template.delete(cls.user_apiclient) + super(TestVnfTemplates, cls).tearDownClass() + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.cleanup = [] + + def tearDown(self): + super(TestVnfTemplates, self).tearDown() + + def ensureVnfTemplateExists(self): + if len(self.vnf_templates) == 0: + self.vnf_template = VnfTemplate.register(self.user_apiclient, + self.vnf_template_config, + zoneid=self.zone.id, + hypervisor=self.hypervisor, + vnfnics=VNF_NICS, + vnfdetails=VNF_DETAILS) + self.vnf_templates.append(self.vnf_template) + else: + self.vnf_template = self.vnf_templates[0] + + def ensureVnfTemplateDownloaded(self): + """Check if template download will finish in 5 minutes""" + retries = 30 + interval = 10 + while retries > -1: + time.sleep(interval) + templates_response = VnfTemplate.list( + self.user_apiclient, + id=self.vnf_template.id, + zoneid=self.zone.id, + templatefilter='self' + ) + template = templates_response[0] + + if not hasattr(template, 'status') or not template or not template.status: + retries = retries - 1 + continue + + if 'Failed' in template.status: + raise Exception( + "Failed to download template: status - %s" % + template.status) + elif template.status == 'Download Complete' and template.isready: + return + elif 'Downloaded' in template.status: + retries = retries - 1 + continue + elif 'Installing' not in template.status: + if retries >= 0: + retries = retries - 1 + continue + raise Exception( + "Error in downloading template: status - %s" % + template.status) + else: + retries = retries - 1 + raise Exception("Template download failed exception.") + + @attr(tags=["advanced"], required_hardware="false") + def test_01_register_vnf_template(self): + """Test register VNF template + """ + self.ensureVnfTemplateExists() + + @attr(tags=["advanced"], required_hardware="false") + def test_02_list_vnf_template(self): + """Test list VNF template + """ + self.ensureVnfTemplateExists() + + templates_response = VnfTemplate.list( + self.user_apiclient, + id=self.vnf_template.id, + zoneid=self.zone.id, + templatefilter='self' + ) + + if isinstance(templates_response, list) and len(templates_response) > 0: + template = templates_response[0] + self.assertEqual("VNF", template.templatetype, + "The template type of VNF template should be VNF but actually it is %s" % template.templatetype) + self.assertTrue(isinstance(template.vnfnics, list), "The template vnfnics must be a list") + self.assertEqual(2, len(template.vnfnics), "The VNF template should have 2 VNF nics") + self.assertEqual(2, len(template.vnfdetails.__dict__), "The VNF template should have 2 VNF details") + else: + self.fail("Failed to get VNF templates by listVnfTemplates API") + + @attr(tags=["advanced"], required_hardware="false") + def test_03_edit_vnf_template(self): + """Test edit VNF template + """ + self.ensureVnfTemplateExists() + + self.vnf_template.update( + self.user_apiclient, + id=self.vnf_template.id, + vnfnics=NEW_VNF_NICS, + vnfdetails=NEW_VNF_DETAILS + ) + + templates_response = VnfTemplate.list( + self.user_apiclient, + id=self.vnf_template.id, + zoneid=self.zone.id, + templatefilter='self' + ) + + if isinstance(templates_response, list) and len(templates_response) > 0: + template = templates_response[0] + self.assertEqual("VNF", template.templatetype, + "The template type of VNF template should be VNF but actually it is %s" % template.templatetype) + self.assertEqual(3, len(template.vnfnics), "The VNF template should have 2 VNF nics") + self.assertEqual(3, len(template.vnfdetails.__dict__), "The VNF template should have 3 VNF details") + else: + self.fail("Failed to get VNF templates by listVnfTemplates API") + + @attr(tags=["advanced"], required_hardware="false") + def test_04_deploy_vnf_appliance(self): + """Test deploy VNF appliance + """ + self.ensureVnfTemplateExists() + self.ensureVnfTemplateDownloaded() + + templates_response = VnfTemplate.list( + self.user_apiclient, + id=self.vnf_template.id, + zoneid=self.zone.id, + templatefilter='self' + ) + + if isinstance(templates_response, list) and len(templates_response) > 0: + template = templates_response[0] + if not template.isready: + self.fail("VNF template is not Ready") + else: + self.fail("Failed to find VNF template") + + # Create network offerings + self.isolated_network_offering = NetworkOffering.create( + self.apiclient, + self.services["isolated_network_offering"]) + self.cleanup.append(self.isolated_network_offering) + self.isolated_network_offering.update( + self.apiclient, + state='Enabled') + + self.l2_network_offering = NetworkOffering.create( + self.apiclient, + self.services["l2-network_offering"]) + self.cleanup.append(self.l2_network_offering) + self.l2_network_offering.update( + self.apiclient, + state='Enabled') + + # Create networks + isolated_network = Network.create( + self.user_apiclient, + self.services["network"], + networkofferingid=self.isolated_network_offering.id, + zoneid=self.zone.id + ) + self.cleanup.append(isolated_network) + + l2_network_1 = Network.create( + self.user_apiclient, + self.services["l2-network"], + networkofferingid=self.l2_network_offering.id, + zoneid=self.zone.id + ) + self.cleanup.append(l2_network_1) + + l2_network_2 = Network.create( + self.user_apiclient, + self.services["l2-network"], + networkofferingid=self.l2_network_offering.id, + zoneid=self.zone.id + ) + self.cleanup.append(l2_network_2) + + # failed deployment + try: + self.virtual_machine = VirtualMachine.create( + self.user_apiclient, + self.services["virtual_machine"], + zoneid=self.zone.id, + templateid=self.vnf_template.id, + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.service_offering.id, + networkids=[isolated_network.id] + ) + self.cleanup.append(self.virtual_machine) + self.fail("The deployment should fail") + except Exception as e: + pass + + # success deployment + self.vnf_appliance = VnfAppliance.create( + self.user_apiclient, + self.services["virtual_machine"], + zoneid=self.zone.id, + templateid=self.vnf_template.id, + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.service_offering.id, + networkids=[isolated_network.id, l2_network_1.id, l2_network_2.id], + vnfconfiguremanagement='true' + ) + self.cleanup.append(self.vnf_appliance) + + @attr(tags=["advanced"], required_hardware="false") + def test_05_delete_vnf_template(self): + """Test delete VNF template + """ + self.ensureVnfTemplateExists() + + self.vnf_template.delete(self.user_apiclient) + + templates_response = VnfTemplate.list( + self.user_apiclient, + id=self.vnf_template.id, + zoneid=self.zone.id, + templatefilter='self' + ) + self.assertIsNone(templates_response, "The VNF template should be removed") + + self.vnf_templates.remove(self.vnf_template) diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index bd102e9c7cc..75ff7e1d29a 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -50,6 +50,7 @@ known_categories = { 'SystemVm': 'System VM', 'VirtualMachine': 'Virtual Machine', 'VM': 'Virtual Machine', + 'Vnf': 'Virtual Network Functions', 'Domain': 'Domain', 'Template': 'Template', 'Iso': 'ISO', diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index d57f1a7e552..fd4b39a0ee9 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -6731,3 +6731,315 @@ class VMSchedule: cmd.id = self.id cmd.virtualmachineid = self.virtualmachineid return (apiclient.deleteVMSchedule(cmd)) + +class VnfTemplate: + """Manage VNF template life cycle""" + + def __init__(self, items): + self.__dict__.update(items) + + @classmethod + def register(cls, apiclient, services, zoneid=None, + account=None, domainid=None, hypervisor=None, + projectid=None, details=None, randomize_name=True, + vnfnics=None, vnfdetails=None): + """Create VNF template from URL""" + + # Create template from Virtual machine and Volume ID + cmd = registerVnfTemplate.registerVnfTemplateCmd() + cmd.displaytext = services["displaytext"] + if randomize_name: + cmd.name = "-".join([services["name"], random_gen()]) + else: + cmd.name = services["name"] + cmd.format = services["format"] + if hypervisor: + cmd.hypervisor = hypervisor + elif "hypervisor" in services: + cmd.hypervisor = services["hypervisor"] + + if "ostypeid" in services: + cmd.ostypeid = services["ostypeid"] + elif "ostype" in services: + # Find OSTypeId from Os type + sub_cmd = listOsTypes.listOsTypesCmd() + sub_cmd.description = services["ostype"] + ostypes = apiclient.listOsTypes(sub_cmd) + + if not isinstance(ostypes, list): + raise Exception( + "Unable to find Ostype id with desc: %s" % + services["ostype"]) + cmd.ostypeid = ostypes[0].id + else: + raise Exception( + "Unable to find Ostype is required for registering template") + + cmd.url = services["url"] + + if zoneid: + cmd.zoneid = zoneid + else: + cmd.zoneid = services["zoneid"] + + cmd.isfeatured = services[ + "isfeatured"] if "isfeatured" in services else False + cmd.ispublic = services[ + "ispublic"] if "ispublic" in services else False + cmd.isextractable = services[ + "isextractable"] if "isextractable" in services else False + cmd.isdynamicallyscalable = services["isdynamicallyscalable"] if "isdynamicallyscalable" in services else False + cmd.passwordenabled = services[ + "passwordenabled"] if "passwordenabled" in services else False + cmd.deployasis = services["deployasis"] if "deployasis" in services else False + + if account: + cmd.account = account + + if domainid: + cmd.domainid = domainid + + if projectid: + cmd.projectid = projectid + elif "projectid" in services: + cmd.projectid = services["projectid"] + + if details: + cmd.details = details + + if "directdownload" in services: + cmd.directdownload = services["directdownload"] + + if vnfnics: + cmd.vnfnics = vnfnics + + if vnfdetails: + cmd.vnfdetails = vnfdetails + + # Register Template + template = apiclient.registerVnfTemplate(cmd) + + if isinstance(template, list): + return VnfTemplate(template[0].__dict__) + + def delete(self, apiclient, zoneid=None): + """Delete VNF Template""" + + cmd = deleteVnfTemplate.deleteVnfTemplateCmd() + cmd.id = self.id + if zoneid: + cmd.zoneid = zoneid + apiclient.deleteVnfTemplate(cmd) + + def update(self, apiclient, **kwargs): + """Updates the template details""" + + cmd = updateVnfTemplate.updateVnfTemplateCmd() + cmd.id = self.id + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + return (apiclient.updateVnfTemplate(cmd)) + + @classmethod + def list(cls, apiclient, **kwargs): + """List all templates matching criteria""" + + cmd = listVnfTemplates.listVnfTemplatesCmd() + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + if 'account' in list(kwargs.keys()) and 'domainid' in list(kwargs.keys()): + cmd.listall = True + return (apiclient.listVnfTemplates(cmd)) + +class VnfAppliance: + """Manage VNF Appliance life cycle""" + + def __init__(self, items): + self.__dict__.update(items) + + @classmethod + def create(cls, apiclient, services, templateid=None, accountid=None, + domainid=None, zoneid=None, networkids=None, + serviceofferingid=None, securitygroupids=None, + projectid=None, startvm=None, diskofferingid=None, + affinitygroupnames=None, affinitygroupids=None, group=None, + hostid=None, clusterid=None, keypair=None, ipaddress=None, mode='default', + method='GET', hypervisor=None, customcpunumber=None, + customcpuspeed=None, custommemory=None, rootdisksize=None, + rootdiskcontroller=None, vpcid=None, macaddress=None, datadisktemplate_diskoffering_list={}, + properties=None, nicnetworklist=None, bootmode=None, boottype=None, dynamicscalingenabled=None, + userdataid=None, userdatadetails=None, extraconfig=None, + vnfconfiguremanagement=None, vnfcidrlist=None): + """Create the VNF appliance""" + + cmd = deployVnfAppliance.deployVnfApplianceCmd() + + if serviceofferingid: + cmd.serviceofferingid = serviceofferingid + elif "serviceoffering" in services: + cmd.serviceofferingid = services["serviceoffering"] + + if zoneid: + cmd.zoneid = zoneid + elif "zoneid" in services: + cmd.zoneid = services["zoneid"] + + if hypervisor: + cmd.hypervisor = hypervisor + + if "displayname" in services: + cmd.displayname = services["displayname"] + + if "name" in services: + cmd.name = services["name"] + + if accountid: + cmd.account = accountid + elif "account" in services: + cmd.account = services["account"] + + if domainid: + cmd.domainid = domainid + elif "domainid" in services: + cmd.domainid = services["domainid"] + + if networkids: + cmd.networkids = networkids + allow_egress = False + elif "networkids" in services: + cmd.networkids = services["networkids"] + allow_egress = False + else: + # When no networkids are passed, network + # is created using the "defaultOfferingWithSourceNAT" + # which has an egress policy of DENY. But guests in tests + # need access to test network connectivity + allow_egress = True + + if templateid: + cmd.templateid = templateid + elif "template" in services: + cmd.templateid = services["template"] + + if diskofferingid: + cmd.diskofferingid = diskofferingid + elif "diskoffering" in services: + cmd.diskofferingid = services["diskoffering"] + + if keypair: + cmd.keypair = keypair + elif "keypair" in services: + cmd.keypair = services["keypair"] + + if ipaddress: + cmd.ipaddress = ipaddress + elif "ipaddress" in services: + cmd.ipaddress = services["ipaddress"] + + if securitygroupids: + cmd.securitygroupids = [str(sg_id) for sg_id in securitygroupids] + + if "affinitygroupnames" in services: + cmd.affinitygroupnames = services["affinitygroupnames"] + elif affinitygroupnames: + cmd.affinitygroupnames = affinitygroupnames + + if affinitygroupids: + cmd.affinitygroupids = affinitygroupids + + if projectid: + cmd.projectid = projectid + + if startvm is not None: + cmd.startvm = startvm + + if hostid: + cmd.hostid = hostid + + if clusterid: + cmd.clusterid = clusterid + + if "userdata" in services: + cmd.userdata = base64.urlsafe_b64encode(services["userdata"].encode()).decode() + + if userdataid is not None: + cmd.userdataid = userdataid + + if userdatadetails is not None: + cmd.userdatadetails = userdatadetails + + if "dhcpoptionsnetworklist" in services: + cmd.dhcpoptionsnetworklist = services["dhcpoptionsnetworklist"] + + if dynamicscalingenabled is not None: + cmd.dynamicscalingenabled = dynamicscalingenabled + + cmd.details = [{}] + + if customcpunumber: + cmd.details[0]["cpuNumber"] = customcpunumber + + if customcpuspeed: + cmd.details[0]["cpuSpeed"] = customcpuspeed + + if custommemory: + cmd.details[0]["memory"] = custommemory + + if not rootdisksize is None and rootdisksize >= 0: + cmd.details[0]["rootdisksize"] = rootdisksize + + if rootdiskcontroller: + cmd.details[0]["rootDiskController"] = rootdiskcontroller + + if "size" in services: + cmd.size = services["size"] + + if group: + cmd.group = group + + cmd.datadisktemplatetodiskofferinglist = [] + for datadisktemplate, diskoffering in list(datadisktemplate_diskoffering_list.items()): + cmd.datadisktemplatetodiskofferinglist.append({ + 'datadisktemplateid': datadisktemplate, + 'diskofferingid': diskoffering + }) + + # program default access to ssh + if mode.lower() == 'basic': + cls.ssh_access_group(apiclient, cmd) + + if macaddress: + cmd.macaddress = macaddress + elif macaddress in services: + cmd.macaddress = services["macaddress"] + + if properties: + cmd.properties = properties + + if nicnetworklist: + cmd.nicnetworklist = nicnetworklist + + if bootmode: + cmd.bootmode = bootmode + + if boottype: + cmd.boottype = boottype + + if extraconfig: + cmd.extraconfig = extraconfig + + if vnfconfiguremanagement: + cmd.vnfconfiguremanagement = vnfconfiguremanagement + + if vnfcidrlist: + cmd.vnfcidrlist = vnfcidrlist + + vnf_app = apiclient.deployVnfAppliance(cmd, method=method) + + return VnfAppliance(vnf_app.__dict__) + + def delete(self, apiclient, expunge=True, **kwargs): + """Destroy an VNF appliance""" + cmd = destroyVirtualMachine.destroyVirtualMachineCmd() + cmd.id = self.id + cmd.expunge = expunge + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + apiclient.destroyVirtualMachine(cmd) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index a10ed69320a..2118fa2e525 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -372,6 +372,7 @@ "label.back": "Back", "label.backup": "Backups", "label.backup.attach.restore": "Restore and attach backup volume", +"label.backup.configure.schedule": "Configure Backup Schedule", "label.backup.offering.assign": "Assign VM to backup offering", "label.backup.offering.remove": "Remove VM from backup offering", "label.backup.offerings": "Backup offerings", @@ -1151,6 +1152,8 @@ "label.launch": "Launch", "label.launch.vm": "Launch instance", "label.launch.vm.and.stay": "Launch instance & stay on this page", +"label.launch.vnf.appliance": "Launch VNF appliance", +"label.launch.vnf.appliance.and.stay": "Launch VNF appliance & stay on this page", "label.launch.zone": "Launch zone", "label.lb.algorithm.leastconn": "Least connections", "label.lb.algorithm.roundrobin": "Round-robin", @@ -1460,6 +1463,7 @@ "label.parentname": "Parent", "label.passive": "Passive", "label.password": "Password", +"label.password.default": "Default Password", "label.password.reset.confirm": "Password has been reset to ", "label.passwordenabled": "Password enabled", "label.path": "Path", @@ -2191,6 +2195,42 @@ "label.vmwaredcname": "VMware datacenter name", "label.vmwaredcvcenter": "VMware datacenter vCenter", "label.vmwarenetworklabel": "VMware traffic label", +"label.vnf.appliance": "VNF Appliance", +"label.vnf.appliances": "VNF appliances", +"label.vnf.appliance.add": "Add VNF Appliance", +"label.vnf.appliance.access.methods": "Management access information of this VNF appliance", +"label.vnf.app.action.destroy": "Destroy VNF appliance", +"label.vnf.app.action.edit": "Edit VNF appliance", +"label.vnf.app.action.expunge": "Expunge VNF appliance", +"label.vnf.app.action.migrate.to.host": "Migrate VNF appliance to another host", +"label.vnf.app.action.migrate.to.ps": "Migrate VNF appliance to another primary storage", +"label.vnf.app.action.recover": "Recover VNF appliance", +"label.vnf.app.action.scale": "Scale VNF appliance", +"label.vnf.app.action.start": "Start VNF appliance", +"label.vnf.app.action.stop": "Stop VNF appliance", +"label.vnf.app.action.reboot": "Reboot VNF appliance", +"label.vnf.app.action.reinstall": "Reinstall VNF appliance", +"label.vnf.cidr.list": "Source cidr list of rules", +"label.vnf.cidr.list.tooltip": "the CIDR list to forward traffic from to the VNF management interface. Multiple entries must be separated by a single comma character (,). The default value is 0.0.0.0/0.", +"label.vnf.configure.management": "Configure rules for VNF management interfaces", +"label.vnf.configure.management.tooltip": "True by default, security group or network rules (source nat and firewall rules) will be configured for VNF management interfaces. False otherwise.", +"label.vnf.detail.add": "Add VNF detail", +"label.vnf.detail.remove": "Remove VNF detail", +"label.vnf.details": "VNF details", +"label.vnf.nic.add": "Add VNF nic", +"label.vnf.nic.delete": "Delete VNF nic", +"label.vnf.nic.description": "Description of VNF nic", +"label.vnf.nic.deviceid": "Device ID of VNF nic. It starts with 0", +"label.vnf.nic.edit": "Edit VNF nic", +"label.vnf.nic.management": "Management NIC", +"label.vnf.nic.management.description": "True if the VNF nic is a management interface. False otherwise", +"label.vnf.nic.name": "Name of VNF nic", +"label.vnf.nic.remove": "Remove VNF nic", +"label.vnf.nic.required": "True if VNF nic is required. Otherwise optional", +"label.vnf.nics": "VNF nics", +"label.vnf.settings": "VNF settings", +"label.vnf.templates": "VNF templates", +"label.vnf.template.register": "Register VNF template", "label.vnmc": "VNMC", "label.volgroup": "Volume group", "label.volume": "Volume", @@ -3145,6 +3185,17 @@ "message.vm.state.stopped": "VM is stopped.", "message.vm.state.stopping": "VM is being stopped.", "message.vm.state.unknown": "VM state is unknown.", +"message.vnf.appliance.networks": "Please select networks for the new VNF appliance.", +"message.vnf.credentials.change": "Please change the password(s) of the VNF appliance immediately.", +"message.vnf.credentials.default": "The default credentials(s) of the VNF appliance", +"message.vnf.credentials.in.template.vnf.details": "Please find the default credentials for this VNF in the details of the VNF template.", +"message.vnf.error.deviceid.should.be.continuous": "The deviceid of selected VNF nics should be continuous.", +"message.vnf.error.network.is.already.used": "Network has been used by multiple nics of the new VNF appliance.", +"message.vnf.error.no.networks": "Please select networks for nics of the new VNF appliance.", +"message.vnf.error.no.network.for.required.deviceid": "Please select a network for required nic of the new VNF appliance.", +"message.vnf.nic.move.up.fail": "Failed to move up this NIC", +"message.vnf.nic.move.down.fail": "Failed to move down this NIC", +"message.vnf.select.networks": "Please select a network for each VNF nic. ", "message.volume.state.allocated": "The volume is allocated but has not been created yet.", "message.volume.state.attaching": "The volume is attaching to a volume from Ready state.", "message.volume.state.copying": "The volume is being copied from the image store to primary storage, in case it's an uploaded volume.", diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 301686c3433..a6f77eacdd0 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -28,6 +28,11 @@

+ + + @@ -159,11 +164,99 @@ export default { customDisplayItems () { return ['ip6routes', 'privatemtu', 'publicmtu'] }, + vnfAccessMethods () { + if (this.resource.templatetype === 'VNF' && ['vm', 'vnfapp'].includes(this.$route.meta.name)) { + const accessMethodsDescription = [] + const accessMethods = this.resource.vnfdetails?.access_methods || null + const username = this.resource.vnfdetails?.username || null + const password = this.resource.vnfdetails?.password || null + const sshPort = this.resource.vnfdetails?.ssh_port || 22 + const sshUsername = this.resource.vnfdetails?.ssh_user || null + const sshPassword = this.resource.vnfdetails?.ssh_password || null + let httpPath = this.resource.vnfdetails?.http_path || '' + if (!httpPath.startsWith('/')) { + httpPath = '/' + httpPath + } + const httpPort = this.resource.vnfdetails?.http_port || null + let httpsPath = this.resource.vnfdetails?.https_path || '' + if (!httpsPath.startsWith('/')) { + httpsPath = '/' + httpsPath + } + const httpsPort = this.resource.vnfdetails?.https_port || null + const webUsername = this.resource.vnfdetails?.web_user || null + const webPassword = this.resource.vnfdetails?.web_password || null + + const credentials = [] + if (username) { + credentials.push(this.$t('label.username') + ' : ' + username) + } + if (password) { + credentials.push(this.$t('label.password.default') + ' : ' + password) + } + if (webUsername) { + credentials.push('Web ' + this.$t('label.username') + ' : ' + webUsername) + } + if (webPassword) { + credentials.push('Web ' + this.$t('label.password.default') + ' : ' + webPassword) + } + if (sshUsername) { + credentials.push('SSH ' + this.$t('label.username') + ' : ' + sshUsername) + } + if (sshPassword) { + credentials.push('SSH ' + this.$t('label.password.default') + ' : ' + sshPassword) + } + + const managementDeviceIds = [] + for (const vnfnic of this.resource.vnfnics) { + if (vnfnic.management) { + managementDeviceIds.push(vnfnic.deviceid) + } + } + const managementIps = [] + for (const nic of this.resource.nic) { + if (managementDeviceIds.includes(parseInt(nic.deviceid)) && nic.ipaddress) { + managementIps.push(nic.ipaddress) + if (nic.publicip) { + managementIps.push(nic.publicip) + } + } + } + + if (accessMethods) { + const accessMethodsArray = accessMethods.split(',') + for (const accessMethod of accessMethodsArray) { + if (accessMethod === 'console') { + accessMethodsDescription.push('- VM Console.') + } else if (accessMethod === 'ssh-password') { + accessMethodsDescription.push('- SSH with password' + (sshPort ? ' (SSH port is ' + sshPort + ').' : '.')) + } else if (accessMethod === 'ssh-key') { + accessMethodsDescription.push('- SSH with key' + (sshPort ? ' (SSH port is ' + sshPort + ').' : '.')) + } else if (accessMethod === 'http') { + for (const managementIp of managementIps) { + const url = 'http://' + managementIp + (httpPort ? ':' + httpPort : '') + httpPath + accessMethodsDescription.push('- Webpage: ' + url + '') + } + } else if (accessMethod === 'https') { + for (const managementIp of managementIps) { + const url = 'https://' + managementIp + (httpsPort ? ':' + httpsPort : '') + httpsPath + accessMethodsDescription.push('- Webpage: ' + url + '') + } + } + } + } else { + accessMethodsDescription.push('- VM Console.') + } + if (credentials) { + accessMethodsDescription.push('
' + this.$t('message.vnf.credentials.in.template.vnf.details')) + } + return accessMethodsDescription.join('
') + } + return null + }, ipV6Address () { if (this.dataResource.nic && this.dataResource.nic.length > 0) { return this.dataResource.nic.filter(e => { return e.ip6address }).map(e => { return e.ip6address }).join(', ') } - return null }, ip6routes () { diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index b3405d8eda1..6ab6967cc6f 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -39,7 +39,7 @@