├── .codeclimate.yml ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .rspec ├── .rspec_ci ├── .rubocop.yml ├── .rubocop_cc.yml ├── .rubocop_local.yml ├── .whitesource ├── .yamllint ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app ├── helpers │ └── manageiq │ │ └── showback │ │ ├── time_converter_helper.rb │ │ ├── units_converter_helper.rb │ │ └── utils_helper.rb └── models │ └── manageiq │ └── showback │ ├── column_units.yml │ ├── data_rollup.rb │ ├── data_rollup │ ├── cpu.rb │ ├── disk.rb │ ├── fixed.rb │ ├── flavor.rb │ ├── mem.rb │ ├── metering.rb │ ├── net.rb │ └── storage.rb │ ├── data_view.rb │ ├── envelope.rb │ ├── input_measure.rb │ ├── manager.rb │ ├── price_plan.rb │ ├── rate.rb │ └── tier.rb ├── bin ├── before_install ├── console ├── rails ├── setup └── update ├── bundler.d └── .keep ├── config ├── initializers │ └── money.rb └── settings.yml ├── db └── fixtures │ ├── input_measures.yml │ └── price_plans.yml ├── docs ├── README.md ├── benchmarks │ ├── README.md │ └── benchmark_events.rb └── demo │ ├── README.md │ ├── seed_clean_db.rb │ └── seed_play_data.rb ├── lib ├── manageiq │ ├── consumption.rb │ └── showback │ │ ├── engine.rb │ │ └── version.rb ├── tasks │ └── README.md └── tasks_private │ └── spec.rake ├── locale └── .keep ├── manageiq-consumption.gemspec ├── renovate.json └── spec ├── factories ├── data_rollup.rb ├── data_view.rb ├── envelope.rb ├── input_measure.rb ├── price_plan.rb ├── rate.rb └── tier.rb ├── helpers ├── time_converter_helper_spec.rb ├── units_converter_helper_spec.rb └── utils_helper_spec.rb ├── models ├── data_rollup │ ├── cpu_spec.rb │ ├── flavor_spec.rb │ └── mem_spec.rb ├── data_rollup_spec.rb ├── data_view_spec.rb ├── envelope_spec.rb ├── input_measure_spec.rb ├── manageiq │ └── showback │ │ └── version_spec.rb ├── manager_spec.rb ├── price_plan_spec.rb ├── rate_spec.rb └── tier_spec.rb ├── rails_helper.rb ├── showback_spec.rb └── spec_helper.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | prepare: 3 | fetch: 4 | - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_base.yml 5 | path: ".rubocop_base.yml" 6 | - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_cc_base.yml 7 | path: ".rubocop_cc_base.yml" 8 | - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/base.yml 9 | path: styles/base.yml 10 | - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/cc_base.yml 11 | path: styles/cc_base.yml 12 | checks: 13 | argument-count: 14 | enabled: false 15 | complex-logic: 16 | enabled: false 17 | file-lines: 18 | enabled: false 19 | method-complexity: 20 | config: 21 | threshold: 11 22 | method-count: 23 | enabled: false 24 | method-lines: 25 | enabled: false 26 | nested-control-flow: 27 | enabled: false 28 | return-statements: 29 | enabled: false 30 | plugins: 31 | rubocop: 32 | enabled: true 33 | config: ".rubocop_cc.yml" 34 | channel: rubocop-1-56-3 35 | exclude_patterns: 36 | - node_modules/ 37 | - spec/ 38 | - test/ 39 | - tmp/ 40 | - vendor/ 41 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: 0 0 * * * 9 | jobs: 10 | ci: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | ruby-version: 15 | - '3.1' 16 | - '3.3' 17 | services: 18 | postgres: 19 | image: manageiq/postgresql:13 20 | env: 21 | POSTGRESQL_USER: root 22 | POSTGRESQL_PASSWORD: smartvm 23 | POSTGRESQL_DATABASE: vmdb_test 24 | options: "--health-cmd pg_isready --health-interval 2s --health-timeout 5s 25 | --health-retries 5" 26 | ports: 27 | - 5432:5432 28 | env: 29 | PGHOST: localhost 30 | PGPASSWORD: smartvm 31 | CC_TEST_REPORTER_ID: "${{ secrets.CC_TEST_REPORTER_ID }}" 32 | steps: 33 | - uses: actions/checkout@v6 34 | - name: Set up system 35 | run: bin/before_install 36 | - name: Set up Ruby 37 | uses: ruby/setup-ruby@v1 38 | with: 39 | ruby-version: "${{ matrix.ruby-version }}" 40 | bundler-cache: true 41 | timeout-minutes: 30 42 | - name: Prepare tests 43 | run: bin/setup 44 | - name: Run tests 45 | run: bundle exec rake 46 | - name: Report code coverage 47 | if: "${{ github.ref == 'refs/heads/master' && matrix.ruby-version == '3.3' }}" 48 | continue-on-error: true 49 | uses: paambaati/codeclimate-action@v9 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | .rubocop-* 12 | /bundler.d/* 13 | !/bundler.d/.keep 14 | /config/settings.local.yml 15 | /config/settings/*.local.yml 16 | /config/environments/*.local.yml 17 | /spec/manageiq 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require manageiq/spec/spec_helper 2 | --require spec_helper 3 | --color 4 | --order random 5 | --exclude-pattern "spec/manageiq/**/*_spec.rb" 6 | -------------------------------------------------------------------------------- /.rspec_ci: -------------------------------------------------------------------------------- 1 | --require manageiq/spec/spec_helper 2 | --require spec_helper 3 | --color 4 | --order random 5 | --profile 25 6 | --exclude-pattern "spec/manageiq/**/*_spec.rb" 7 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - ".rubocop_local.yml" 3 | inherit_gem: 4 | manageiq-style: ".rubocop_base.yml" 5 | -------------------------------------------------------------------------------- /.rubocop_cc.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - ".rubocop_base.yml" 3 | - ".rubocop_cc_base.yml" 4 | - ".rubocop_local.yml" 5 | -------------------------------------------------------------------------------- /.rubocop_local.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-consumption/15df18d6945094f22a4e4848a535bd2d3517112c/.rubocop_local.yml -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "settingsInheritedFrom": "ManageIQ/whitesource-config@master" 3 | } 4 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | ignore: | 3 | /vendor/** 4 | /spec/manageiq/** 5 | 6 | extends: relaxed 7 | 8 | rules: 9 | indentation: 10 | indent-sequences: false 11 | line-length: 12 | max: 120 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | 7 | 8 | ## Hammer-1 9 | 10 | ### Added 11 | - Update i18n catalog for hammer [(#141)](https://github.com/ManageIQ/manageiq-consumption/pull/141) 12 | 13 | ## Gaprindashvili-1 - Released 2018-01-31 14 | 15 | ### Added 16 | - Add translations [(#129)](https://github.com/ManageIQ/manageiq-consumption/pull/129) 17 | - Add missing usage types for integration [(#110)](https://github.com/ManageIQ/manageiq-consumption/pull/110) 18 | - Now rate create a zero tier after create [(#105)](https://github.com/ManageIQ/manageiq-consumption/pull/105) 19 | - Add US8 currency [(#104)](https://github.com/ManageIQ/manageiq-consumption/pull/104) 20 | - Allows the new chargeback to be plugged in and substitute the old rating [(#95)](https://github.com/ManageIQ/manageiq-consumption/pull/95) 21 | 22 | ## Added initial changelog 23 | 24 | - Initial version of manageiq-consumption 25 | - Functional parity with existing rate management in the old chargeback. It is possible to generate the same information using the old and the new chargeback. 26 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # Declare your gem's dependencies in manageiq-consumption.gemspec. 2 | # Bundler will treat runtime dependencies like base dependencies, and 3 | # development dependencies will be added by default to the :development group. 4 | gemspec 5 | 6 | # Declare any dependencies that are still in development here instead of in 7 | # your gemspec. These might include edge Rails or gems from your path or 8 | # Git. Remember to move these dependencies to your gemspec before releasing 9 | # your gem to rubygems.org. 10 | 11 | # Load Gemfile with dependencies from manageiq 12 | eval_gemfile(File.expand_path("spec/manageiq/Gemfile", __dir__)) 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ManageIQ::Consumption 2 | 3 | [![CI](https://github.com/ManageIQ/manageiq-consumption/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/ManageIQ/manageiq-consumption/actions/workflows/ci.yaml) 4 | [![Maintainability](https://api.codeclimate.com/v1/badges/7ddcfc7e53574d375f43/maintainability)](https://codeclimate.com/github/ManageIQ/manageiq-consumption/maintainability) 5 | [![Test Coverage](https://api.codeclimate.com/v1/badges/7ddcfc7e53574d375f43/test_coverage)](https://codeclimate.com/github/ManageIQ/manageiq-consumption/test_coverage) 6 | 7 | [![Chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ManageIQ/manageiq/chargeback?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | Consumption plugin for ManageIQ. 10 | 11 | ## Introduction 12 | Manageiq-consumption is a gem replacement for chargeback/showback on manageiq. 13 | 14 | It is being designed as a complete rewrite of [ManageIQ](https://www.mananageiq.org) chargeback code in [https://github.com/ManageIQ](https://github.com/ManageIQ). 15 | 16 | The main reason for the effort is to make sure that ManageIQ is capable of understanding and replicating any price plan for the cloud available for customers. 17 | 18 | #### Design principles: 19 | - **Flexibility:** capable of supporting telco chargeback(simplified). It is based on TMForum concepts adapted to cloud 20 | - **Performance:** reduce the time needed to generate an invoice 21 | - **Automation:** generation of invoices should be automated, for new users, groups, or resources 22 | - **Easy integration** with 3rd party billing and ERP systems to provide full billing and payments 23 | - **API oriented:** Every function should be available through an API. Parts of the system will be suceptible of being substitued by an external billing system via API. 24 | 25 | #### Concepts 26 | Please see the [wiki](https://github.com/ManageIQ/manageiq-consumption/wiki) for updated documentation on concepts, architecture and configuration. 27 | 28 | #### Overall project status 29 | All the project status can be followed in: 30 | 31 | Pivotal tracker: 32 | [https://www.pivotaltracker.com/n/projects/1958459](https://www.pivotaltracker.com/n/projects/1958459) 33 | Github issues: 34 | [https://github.com/ManageIQ/manageiq-consumption/issues](https://github.com/ManageIQ/manageiq-consumption/issues) 35 | 36 | #### Development Phases 37 | 38 | Development has been divided in phases: 39 | 40 | - Phase 1 (current): We use the old reporting data that is fed into the new chargeback. Old chargeback rating can be deleted. Price plans are migrated into the new chargeback. It should have functional parity with the old chargeback. 41 | - Phase 2: Reporting is changed into the new showback_event mechanism, to increase the flexibility and speed of the system 42 | - Phase 3: Extend it into a financial management system 43 | 44 | ## Demo documentation 45 | 46 | There are instruction to perform a demo of the new system inside the code, if you want to have a look it simply: 47 | 48 | Go to [demo section](/docs/demo/README.md) 49 | 50 | ## Development 51 | 52 | See the section on plugins in the [ManageIQ Developer Setup](http://manageiq.org/docs/guides/developer_setup/plugins) 53 | 54 | For quick local setup run `bin/setup`, which will clone the core ManageIQ repository under the *spec* directory and setup necessary config files. If you have already cloned it, you can run `bin/update` to bring the core ManageIQ code up to date. 55 | 56 | ## License 57 | 58 | The gem is available as open source under the terms of the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0). 59 | 60 | ## Contributing 61 | 62 | 1. Fork it 63 | 2. Create your feature branch (`git checkout -b my-new-feature`) 64 | 3. Commit your changes (`git commit -am 'Add some feature'`) 65 | 4. Push to the branch (`git push origin my-new-feature`) 66 | 5. Create new Pull Request 67 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | 3 | begin 4 | require 'rspec/core/rake_task' 5 | 6 | APP_RAKEFILE = File.expand_path("spec/manageiq/Rakefile", __dir__) 7 | load 'rails/tasks/engine.rake' 8 | load 'rails/tasks/statistics.rake' 9 | rescue LoadError 10 | end 11 | 12 | require 'bundler/gem_tasks' 13 | 14 | FileList['lib/tasks_private/**/*.rake'].each { |r| load r } 15 | 16 | task :default => :spec 17 | -------------------------------------------------------------------------------- /app/helpers/manageiq/showback/time_converter_helper.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Helper for unit converters 3 | # 4 | # Allows the user to find distance between prefixes, and also to convert between units. 5 | # It also allows the user to extract the prefix from a unit 'Mb' -> 'M' 6 | # 7 | # 8 | 9 | module ManageIQ::Showback 10 | module TimeConverterHelper 11 | VALID_INTERVAL_UNITS = %w(hourly daily weekly monthly yearly).freeze 12 | 13 | def self.number_of_intervals(period:, interval:, calculation_date: Time.current, days_in_month: nil, days_in_year: nil) 14 | # Period: time period as input (end_time - start_time) 15 | # interval: base interval to calculate against (i.e 'daily', 'monthly', default: 'monthly') 16 | # Calculation_date: used to calculate taking into account the #days in month 17 | # It always return at least 1 as the event exists 18 | return 1 if period.zero? 19 | time_span = case interval 20 | when 'minutely' then 1.minute.seconds 21 | when 'hourly' then 1.hour.seconds 22 | when 'daily' then 1.day.seconds 23 | when 'weekly' then 1.week.seconds 24 | when 'monthly' then (days_in_month || Time.days_in_month(calculation_date.month)) * 1.day.seconds 25 | when 'yearly' then (days_in_year || Time.days_in_year(calculation_date.year)) * 1.day.seconds 26 | end 27 | period.div(time_span) + (period.modulo(time_span).zero? ? 0 : 1) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/helpers/manageiq/showback/units_converter_helper.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Helper for unit converters 3 | # 4 | # Allows the user to find distance between prefixes, and also to convert between units. 5 | # It also allows the user to extract the prefix from a unit 'Mb' -> 'M' 6 | # 7 | # 8 | 9 | module ManageIQ::Showback 10 | module UnitsConverterHelper 11 | SYMBOLS = %w(b B Hz bps Bps).freeze # What symbols are going to be searched for 12 | 13 | SI_PREFIX = { '' => { :name => '', 14 | :value => 1 }, 15 | 'K' => { :name => 'kilo', 16 | :value => 1000 }, 17 | 'M' => { :name => 'mega', 18 | :value => 1_000_000 }, 19 | 'G' => { :name => 'giga', 20 | :value => 1_000_000_000 }, 21 | 'T' => { :name => 'tera', 22 | :value => 1_000_000_000_000 }, 23 | 'P' => { :name => 'peta', 24 | :value => 1_000_000_000_000_000 }, 25 | 'E' => { :name => 'exa', 26 | :value => 1_000_000_000_000_000_000 }, 27 | 'd' => { :name => 'deci', 28 | :value => 1 / 10.to_r }, # Rational 29 | 'c' => { :name => 'centi', 30 | :value => 1 / 100.to_r }, 31 | 'm' => { :name => 'milli', 32 | :value => 1 / 1_000.to_r }, 33 | 'µ' => { :name => 'micro', 34 | :value => 1 / 1_000_000.to_r}, 35 | 'n' => { :name => 'nano', 36 | :value => 1 / 1_000_000_000.to_r}, 37 | 'p' => { :name => 'pico', 38 | :value => 1 / 1_000_000_000_000.to_r} }.freeze 39 | BINARY_PREFIX = { '' => { :name => '', 40 | :value => 1}, 41 | 'Ki' => { :name => 'kibi', 42 | :value => 1024}, 43 | 'Mi' => { :name => 'mebi', 44 | :value => 1_048_576}, 45 | 'Gi' => { :name => 'gibi', 46 | :value => 1_073_741_824}, 47 | 'Ti' => { :name => 'tebi', 48 | :value => 1_099_511_627_776}, 49 | 'Pi' => { :name => 'pebi', 50 | :value => 1_125_899_906_842_624}, 51 | 'Ei' => { :name => 'exbi', 52 | :value => 1_152_921_504_606_846_976} }.freeze 53 | 54 | ALL_PREFIXES = SI_PREFIX.merge(BINARY_PREFIX).freeze 55 | 56 | def self.to_unit(value, unit = '', destination_unit = '', prefix_type = 'ALL_PREFIXES') 57 | # It returns the value converted to the new unit 58 | prefix = extract_prefix(unit) 59 | destination_prefix = extract_prefix(destination_unit) 60 | prefix_distance = distance(prefix, destination_prefix, prefix_type) 61 | return nil if prefix_distance.nil? 62 | (value * prefix_distance).to_f 63 | end 64 | 65 | def self.distance(prefix, other_prefix = '', prefix_type = 'ALL_PREFIXES') 66 | # Returns the distance and whether you need to divide or multiply 67 | # Check that the list of conversions exists or use the International Sistem SI 68 | list = (const_get(prefix_type.upcase) if const_defined?(prefix_type.upcase)) || ALL_PREFIXES 69 | 70 | # Find the prefix name, value pair in the list 71 | orig = list[prefix] 72 | dest = list[other_prefix] 73 | # If I can't find the prefixes in the list: 74 | # If they are the same, return 1 75 | # If they are different (i.e. "cores" vs "none", return nil) 76 | return 1 if prefix == other_prefix 77 | return nil if orig.nil? || dest.nil? 78 | orig[:value].to_r / dest[:value] 79 | end 80 | 81 | def self.extract_prefix(unit) 82 | prefix = nil 83 | SYMBOLS.each do |x| 84 | prefix ||= /(.*)#{x}\z/.match(unit)&.captures 85 | end 86 | (prefix[0] unless prefix.nil?) || unit || '' 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /app/helpers/manageiq/showback/utils_helper.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback 2 | module UtilsHelper 3 | def self.included_in?(context, test) 4 | # Validating that one JSON is completely included in the other 5 | # Only to be called with JSON! 6 | return false if test.nil? || context.nil? 7 | result = true 8 | test = {} if test.empty? 9 | Hashdiff.diff(context, test).each do |x| 10 | result = false if x[0] == '+' || x[0] == '~' 11 | end 12 | result 13 | end 14 | 15 | # 16 | # Function for get the parent of a resource in MiQ 17 | # 18 | def self.get_parent(resource) 19 | parent_type = get_type_hierarchy_next(resource) 20 | # Check if the class has the method of the type of resource of the parent 21 | nil unless parent_type.present? 22 | nil if resource.methods.include?(parent_type.tableize.singularize.to_sym) 23 | begin 24 | # I get the resource or returns nil 25 | resource.send(parent_type.tableize.singularize) 26 | rescue 27 | nil 28 | end 29 | end 30 | 31 | HARDWARE_RESOURCE = %w(Vm Host EmsCluster ExtManagementSystem Provider MiqEnterprise).freeze 32 | CONTAINER_RESOURCE = %w(Container ContainerNode ContainerReplicator ContainerProject ExtManagementSystem Provider MiqEnterprise).freeze 33 | 34 | # 35 | # MiQ need to be implement ancestry for all kind of resources so we make our function to get the type of the parent 36 | # 37 | def self.get_type_hierarchy_next(resource) 38 | resource_type = resource.type.split("::")[-1] unless resource.type.nil? 39 | # I get the next type of resource parent 40 | return HARDWARE_RESOURCE[HARDWARE_RESOURCE.index(resource_type) + 1] || "" if HARDWARE_RESOURCE.include?(resource_type) 41 | return CONTAINER_RESOURCE[CONTAINER_RESOURCE.index(resource_type) + 1] || "" if CONTAINER_RESOURCE.include?(resource_type) 42 | "" 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/column_units.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :cpu_usage_rate_average: "Vm" 3 | :numvcpus: "vcpus" 4 | :cpu_total_cores: "cores" 5 | :total_mem: "Mb" 6 | :ram_size: "Mb" 7 | :max_number_of_cpu: "cores" 8 | :average: "percent" 9 | :number: "cores" 10 | :max_mem: "Mb" 11 | :memory_reserved: "Mb" 12 | :cpu_reserved: "cores" 13 | # for old chargeback integration 14 | :cpu_usagemhz_rate_average: "Mhz" 15 | :v_derived_cpu_total_cores_used: "vcpus" 16 | :derived_vm_numvcpus: "vcpus" 17 | :derived_memory_used: "Mb" 18 | :derived_memory_available: "Mb" 19 | :net_usage_rate_average: "kbps" 20 | :disk_usage_rate_average: "kbps" 21 | :fixed_compute_1: "" 22 | :fixed_compute_2: "" 23 | :fixed_storage_1: "" 24 | :fixed_storage_2: "" 25 | :metering_used_hours: "" 26 | :derived_vm_allocated_disk_storage: "Gi" 27 | :derived_vm_used_disk_storage: "Gi" 28 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/data_rollup.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback 2 | class DataRollup < ApplicationRecord 3 | belongs_to :resource, :polymorphic => true 4 | 5 | has_many :data_views, 6 | :dependent => :destroy, 7 | :inverse_of => :data_rollup, 8 | :foreign_key => :showback_data_rollup_id 9 | has_many :envelopes, 10 | :through => :data_views, 11 | :inverse_of => :data_rollups, 12 | :foreign_key => :showback_data_rollup_id 13 | 14 | validates :start_time, :end_time, :resource, :presence => true 15 | validate :start_time_before_end_time 16 | validates :resource, :presence => true 17 | 18 | default_value_for :data, {} 19 | default_value_for :context, {} 20 | 21 | after_create :generate_data 22 | 23 | extend ActiveSupport::Concern 24 | include Cpu 25 | include Disk 26 | include Fixed 27 | include Flavor 28 | include Mem 29 | include Metering 30 | include Net 31 | include Storage 32 | 33 | self.table_name = 'showback_data_rollups' 34 | 35 | def start_time_before_end_time 36 | errors.add(:start_time, "Start time should be before end time") unless end_time.to_i >= start_time.to_i 37 | end 38 | 39 | # return the parsing error message if not valid JSON; otherwise nil 40 | def validate_format 41 | data = JSON.decode(data) if data.class == Hash 42 | JSON.parse(data) && nil if data.present? 43 | rescue JSON::ParserError 44 | nil 45 | end 46 | 47 | def clean_data 48 | self.data = {} 49 | end 50 | 51 | def generate_data(data_units = ManageIQ::Showback::Manager.load_column_units) 52 | clean_data 53 | ManageIQ::Showback::InputMeasure.all.each do |group_type| 54 | next unless resource_type.include?(group_type.entity) 55 | data[group_type.group] = {} 56 | group_type.fields.each do |dim| 57 | data[group_type.group][dim] = [0, data_units[dim.to_sym] || ""] unless group_type.group == "FLAVOR" 58 | end 59 | end 60 | end 61 | 62 | def self.data_rollups_between_month(start_of_month, end_of_month) 63 | ManageIQ::Showback::DataRollup.where("start_time >= ? AND end_time <= ?", 64 | DateTime.now.utc.beginning_of_month.change(:month => start_of_month), 65 | DateTime.now.utc.end_of_month.change(:month => end_of_month)) 66 | end 67 | 68 | def self.data_rollups_actual_month 69 | ManageIQ::Showback::DataRollup.where("start_time >= ? AND end_time <= ?", 70 | DateTime.now.utc.beginning_of_month, 71 | DateTime.now.utc.end_of_month) 72 | end 73 | 74 | def self.data_rollups_past_month 75 | ManageIQ::Showback::DataRollup.where("start_time >= ? AND end_time <= ?", 76 | DateTime.now.utc.beginning_of_month - 1.month, 77 | DateTime.now.utc.end_of_month - 1.month) 78 | end 79 | 80 | def get_group(entity, field) 81 | data[entity][field] if data && data[entity] 82 | end 83 | 84 | def get_group_unit(entity, field) 85 | get_group(entity, field).last 86 | end 87 | 88 | def get_group_value(entity, field) 89 | get_group(entity, field).first 90 | end 91 | 92 | def last_flavor 93 | data["FLAVOR"][data["FLAVOR"].keys.max] 94 | end 95 | 96 | def get_key_flavor(key) 97 | data["FLAVOR"][data["FLAVOR"].keys.max][key] 98 | end 99 | 100 | def update_data_rollup(data_units = ManageIQ::Showback::Manager.load_column_units) 101 | generate_data(data_units) unless data.present? 102 | @metrics = resource.methods.include?(:metrics) ? metrics_time_range(end_time, start_time.end_of_month) : [] 103 | data.each do |key, fields| 104 | fields.keys.each do |dim| 105 | data[key][dim] = [generate_metric(key, dim), data_units[dim.to_sym] || ""] 106 | end 107 | end 108 | if @metrics.count.positive? 109 | self.end_time = @metrics.last.timestamp 110 | end 111 | collect_tags 112 | update_data_views 113 | end 114 | 115 | def generate_metric(key, dim) 116 | key == "FLAVOR" ? send("#{key}_#{dim}") : send("#{key}_#{dim}", get_group_value(key, dim).to_d) 117 | end 118 | 119 | def collect_tags 120 | if !self.context.present? 121 | self.context = {"tag" => {}} 122 | else 123 | self.context["tag"] = {} unless self.context.key?("tag") 124 | end 125 | resource.tagged_with(:ns => '/managed').each do |tag| 126 | entity = tag.classification.category 127 | self.context["tag"][entity] = [] unless self.context["tag"].key?(entity) 128 | self.context["tag"][entity] << tag.classification.name unless self.context["tag"][entity].include?(tag.classification.name) 129 | end 130 | end 131 | 132 | # 133 | # Get the metrics between two dates using metrics common for_time_range defined in CU MiQ 134 | # 135 | def metrics_time_range(start_time, end_time) 136 | resource.metrics.for_time_range(start_time, end_time) 137 | end 138 | 139 | # 140 | # Return the event days passed between start_time - end_time 141 | # 142 | def data_rollup_days 143 | time_span / (24 * 60 * 60) 144 | end 145 | 146 | def time_span 147 | (end_time - start_time).round.to_i 148 | end 149 | 150 | def month_duration 151 | (end_time.end_of_month - start_time.beginning_of_month).round.to_i 152 | end 153 | 154 | # Find a envelope 155 | def find_envelope(res) 156 | ManageIQ::Showback::Envelope.find_by( 157 | :resource => res, 158 | :state => "OPEN" 159 | ) 160 | end 161 | 162 | def assign_resource 163 | one_resource = resource 164 | # While I have resource loop looking for the parent find the envelope asssociate and add the event 165 | until one_resource.nil? 166 | find_envelope(one_resource)&.add_data_rollup(self) 167 | one_resource = ManageIQ::Showback::UtilsHelper.get_parent(one_resource) 168 | end 169 | end 170 | 171 | def assign_by_tag 172 | return unless context.key?("tag") 173 | context["tag"].each do |entity, array_children| 174 | t = Tag.lookup_by_classification_name(entity) 175 | find_envelope(t)&.add_data_rollup(self) 176 | array_children.each do |child_entity| 177 | tag_child = t.classification.children.detect { |c| c.name == child_entity } 178 | find_envelope(tag_child.tag)&.add_data_rollup(self) 179 | end 180 | end 181 | end 182 | 183 | def update_data_views 184 | ManageIQ::Showback::DataView.where(:data_rollup => self).each do |data_view| 185 | if data_view.open? 186 | data_view.update_data_snapshot 187 | end 188 | end 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/data_rollup/cpu.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback::DataRollup::Cpu 2 | # 3 | # Return the average acumulated with the new one 4 | # 5 | def CPU_average(value) 6 | if @metrics.count.positive? 7 | ((value * data_rollup_days + @metrics.average(:cpu_usage_rate_average)) / (data_rollup_days + 1)) 8 | else 9 | value 10 | end 11 | end 12 | 13 | # 14 | # Return Number Ocurrences 15 | # 16 | def CPU_number(value) 17 | value 18 | end 19 | 20 | # 21 | # Return the max number of cpu for object 22 | # 23 | def CPU_max_number_of_cpu(value) 24 | numcpus = case resource.class.name.ends_with?("Container") 25 | when true then resource.vim_performance_states.last.state_data[:numvcpus] 26 | else resource.methods.include?(:cpu_total_cores) ? resource.cpu_total_cores : 0 27 | end 28 | [value, numcpus].max.to_i 29 | end 30 | 31 | # for old chargeback integration 32 | def cpu_cpu_usagemhz_rate_average 33 | end 34 | 35 | def cpu_cores_v_derived_cpu_total_cores_used 36 | end 37 | 38 | def cpu_cores_derived_vm_numvcpus 39 | end 40 | 41 | def cpu_cores_cpu_usage_rate_average 42 | end 43 | 44 | def cpu_derived_vm_numvcpus 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/data_rollup/disk.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback::DataRollup::Disk 2 | # for old chargeback integration 3 | def disk_io_disk_usage_rate_average 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/data_rollup/fixed.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback::DataRollup::Fixed 2 | def fixed_fixed_compute_1 3 | end 4 | 5 | def fixed_fixed_compute_2 6 | end 7 | 8 | def fixed_fixed_storage_1 9 | end 10 | 11 | def fixed_fixed_storage_2 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/data_rollup/flavor.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback::DataRollup::Flavor 2 | # 3 | # Return Number Ocurrences 4 | # 5 | def FLAVOR_cpu_reserved 6 | numcpus = resource.class.name.ends_with?("Container") ? resource.vim_performance_states.last.state_data[:numvcpus] : resource.try(:cpu_total_cores) || 0 7 | update_value_flavor("cores", [numcpus, "cores"]) 8 | end 9 | 10 | # 11 | # Return Memory 12 | # 13 | def FLAVOR_memory_reserved 14 | tmem = resource.class.name.ends_with?("Container") ? resource.vim_performance_states.last.state_data[:total_mem] : resource.try(:ram_size) || 0 15 | update_value_flavor("memory", [tmem, "Mb"]) 16 | end 17 | 18 | private 19 | 20 | def update_value_flavor(k, v) 21 | self.data["FLAVOR"] = {} unless self.data.key?("FLAVOR") 22 | if self.data["FLAVOR"].empty? 23 | add_flavor(k => v) 24 | else 25 | t_last = self.data["FLAVOR"].keys.last 26 | if self.data["FLAVOR"][t_last].key?(k) 27 | add_flavor(k => v) unless self.data["FLAVOR"][t_last][k] == v 28 | else 29 | self.data["FLAVOR"][t_last][k] = v 30 | end 31 | end 32 | end 33 | 34 | def add_flavor(new_data) 35 | self.data["FLAVOR"][Time.current] = new_data 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/data_rollup/mem.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback::DataRollup::Mem 2 | # 3 | # Return the average acumulated with the new one 4 | # 5 | def MEM_max_mem(*) 6 | resource.class.name.ends_with?("Container") ? resource.vim_performance_states.last.state_data[:total_mem] : resource.try(:ram_size) || 0 7 | end 8 | 9 | # for old chargeback integration 10 | def memory_derived_memory_used 11 | end 12 | 13 | def memory_derived_memory_available 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/data_rollup/metering.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback::DataRollup::Metering 2 | # for old chargeback integration 3 | def metering_metering_used_hours 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/data_rollup/net.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback::DataRollup::Net 2 | # for old chargeback integration 3 | def net_io_net_usage_rate_average 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/data_rollup/storage.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback::DataRollup::Storage 2 | # for old chargeback integration 3 | def storage_derived_vm_used_disk_storage 4 | end 5 | 6 | def storage_derived_vm_allocated_disk_storage 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/data_view.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback 2 | class DataView < ApplicationRecord 3 | self.table_name = 'showback_data_views' 4 | 5 | monetize(:cost_subunits) 6 | 7 | default_value_for :cost, 0 8 | 9 | belongs_to :data_rollup, :inverse_of => :data_views, :foreign_key => :showback_data_rollup_id 10 | belongs_to :envelope, :inverse_of => :data_views, :foreign_key => :showback_envelope_id 11 | 12 | validates :envelope, :presence => true, :allow_nil => false 13 | validates :data_rollup, :presence => true, :allow_nil => false 14 | 15 | before_create :snapshot_data_rollup 16 | default_value_for :data_snapshot, {} 17 | default_value_for :context_snapshot, {} 18 | 19 | # 20 | # Set the cost to 0 21 | # 22 | def clean_cost 23 | self.cost = 0 24 | save 25 | end 26 | 27 | # Check if the envelope is in a Open State 28 | # 29 | # == Returns: 30 | # A boolean value with true if it's open 31 | # 32 | def open? 33 | envelope.state == "OPEN" 34 | end 35 | 36 | # A stored data is created when you create a dataview with a snapshoot of the event 37 | # Save the actual data of the event 38 | # 39 | # == Parameters: 40 | # t:: 41 | # A timestamp of the snapshot. This 42 | # can be a timestamp or `Time.now.utc`. 43 | # 44 | def snapshot_data_rollup(t = Time.now.utc) 45 | data_snapshot[t] = data_rollup.data unless data_snapshot != {} 46 | self.context_snapshot = data_rollup.context 47 | end 48 | 49 | # This returns the data information at the start of the envelope 50 | # 51 | # == Returns: 52 | # A json data of the snapshot at start 53 | # 54 | def data_snapshot_start 55 | data_snapshot[data_snapshot.keys.sort.first] || nil 56 | end 57 | 58 | # Get last snapshot of the stored data 59 | # 60 | # == Returns: 61 | # The data information in json format or nil if not exists 62 | # 63 | def data_snapshot_last 64 | data_snapshot[data_snapshot_last_key] || nil 65 | end 66 | 67 | # Get last timestamp of the snapshots 68 | # 69 | # == Returns: 70 | # A timestamp value of the last snapshot 71 | # 72 | def data_snapshot_last_key 73 | data_snapshot.keys.sort.last || nil 74 | end 75 | 76 | # This update the last snapshoot of the event 77 | def update_data_snapshot(t = Time.now.utc) 78 | data_snapshot.delete(data_snapshot_last_key) unless data_snapshot.keys.length == 1 79 | data_snapshot[t] = data_rollup.data 80 | save 81 | end 82 | 83 | def get_group(entity, field) 84 | get_data_group(data_snapshot_start, entity, field) 85 | end 86 | 87 | def get_last_group(entity, field) 88 | get_data_group(data_snapshot_last, entity, field) 89 | end 90 | 91 | # This return the entity|field group at the start and end of the envelope 92 | def get_envelope_group(entity, field) 93 | [get_data_group(data_snapshot_start, entity, field), 94 | get_data_group(data_snapshot_last, entity, field)] 95 | end 96 | 97 | def calculate_cost(price_plan = nil) 98 | # Find the price plan, there should always be one as it is seeded(Enterprise) 99 | price_plan ||= envelope.find_price_plan 100 | if price_plan.class == ManageIQ::Showback::PricePlan 101 | cost = price_plan.calculate_total_cost(data_rollup) 102 | save 103 | cost 104 | else 105 | errors.add(:price_plan, _('not found')) 106 | Money.new(0) 107 | end 108 | end 109 | 110 | private 111 | 112 | def get_data_group(data, entity, field) 113 | data[entity][field] || nil if data && data[entity] 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/envelope.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback 2 | class Envelope < ApplicationRecord 3 | self.table_name = 'showback_envelopes' 4 | 5 | belongs_to :resource, :polymorphic => true 6 | 7 | monetize :accumulated_cost_subunits 8 | default_value_for :accumulated_cost, Money.new(0) 9 | 10 | before_save :check_envelope_state, :if => :state_changed? 11 | 12 | has_many :data_views, 13 | :dependent => :destroy, 14 | :inverse_of => :envelope, 15 | :foreign_key => :showback_envelope_id 16 | has_many :data_rollups, 17 | :through => :data_views, 18 | :inverse_of => :envelopes, 19 | :foreign_key => :showback_envelope_id 20 | 21 | validates :name, :presence => true 22 | validates :description, :presence => true 23 | validates :resource, :presence => true 24 | validates :start_time, :end_time, :presence => true 25 | validates :state, :presence => true, :inclusion => { :in => %w(OPEN PROCESSING CLOSED) } 26 | 27 | # Test that end_time happens later than start_time. 28 | validate :start_time_before_end_time 29 | 30 | def start_time_before_end_time 31 | errors.add(:end_time, _('should happen after start_time')) unless end_time.to_i > start_time.to_i 32 | end 33 | 34 | def check_envelope_state 35 | case state_was 36 | when 'OPEN' then 37 | raise _("Envelope can't change state to CLOSED from OPEN") unless state != 'CLOSED' 38 | # s_time = (self.start_time + 1.months).beginning_of_month # This is never used 39 | s_time = end_time != start_time.end_of_month ? end_time : (start_time + 1.month).beginning_of_month 40 | e_time = s_time.end_of_month 41 | generate_envelope(s_time, e_time) unless ManageIQ::Showback::Envelope.exists?(:resource => resource, :start_time => s_time) 42 | when 'PROCESSING' then raise _("Envelope can't change state to OPEN from PROCESSING") unless state != 'OPEN' 43 | when 'CLOSED' then raise _("Envelope can't change state when it's CLOSED") 44 | end 45 | end 46 | 47 | def add_data_rollup(data_rollup) 48 | if data_rollup.kind_of?(ManageIQ::Showback::DataRollup) 49 | # verify that the event is not already there 50 | if data_rollups.include?(data_rollup) 51 | errors.add(:data_rollups, 'duplicate') 52 | else 53 | dataview = ManageIQ::Showback::DataView.new(:data_rollup => data_rollup, :envelope => self) 54 | dataview.save 55 | end 56 | else 57 | errors.add(:data_rollups, "Error Type #{data_rollup.type} is not ManageIQ::Showback::DataRollup") 58 | end 59 | end 60 | 61 | # Remove events from a envelope, no error is thrown 62 | 63 | def remove_data_rollup(data_rollup) 64 | if data_rollup.kind_of?(ManageIQ::Showback::DataRollup) 65 | if data_rollups.include?(data_rollup) 66 | data_rollups.delete(data_rollup) 67 | else 68 | errors.add(:data_rollups, "not found") 69 | end 70 | else 71 | errors.add(:data_rollups, "Error Type #{data_rollup.type} is not ManageIQ::Showback::DataRollup") 72 | end 73 | end 74 | 75 | def get_data_view(input) 76 | ch = find_data_view(input) 77 | if ch.nil? 78 | Money.new(0) 79 | else 80 | ch.cost 81 | end 82 | end 83 | 84 | def update_data_view(input, cost) 85 | ch = find_data_view(input) 86 | unless ch.nil? 87 | ch.cost = Money.new(cost) 88 | ch 89 | end 90 | end 91 | 92 | def add_data_view(input, cost) 93 | ch = find_data_view(input) 94 | # updates an existing dataviews 95 | if ch 96 | ch.cost = Money.new(cost) 97 | elsif input.class == ManageIQ::Showback::DataRollup # Or create a new one 98 | ch = data_views.new(:data_rollup => input, 99 | :cost => cost) 100 | else 101 | errors.add(:input, 'bad class') 102 | return 103 | end 104 | ch.save 105 | ch 106 | end 107 | 108 | def clear_data_view(input) 109 | ch = find_data_view(input) 110 | ch.cost = 0 111 | ch.save 112 | end 113 | 114 | def sum_of_data_views 115 | a = Money.new(0) 116 | data_views.each do |x| 117 | a += x.cost if x.cost 118 | end 119 | a 120 | end 121 | 122 | def clean_all_data_views 123 | data_views.each(&:clean_cost) 124 | end 125 | 126 | def calculate_data_view(input) 127 | ch = find_data_view(input) 128 | if ch.kind_of?(ManageIQ::Showback::DataView) 129 | ch.cost = ch.calculate_cost(find_price_plan) || Money.new(0) 130 | save 131 | elsif input.nil? 132 | errors.add(:data_view, 'not found') 133 | Money.new(0) 134 | else 135 | input.errors.add(:data_view, 'not found') 136 | Money.new(0) 137 | end 138 | end 139 | 140 | def calculate_all_data_views 141 | # plan = find_price_plan 142 | data_views.each do |x| 143 | calculate_data_view(x) 144 | end 145 | end 146 | 147 | def find_price_plan 148 | # TODO 149 | # For the demo: return one price plan, we will create the logic later 150 | # parent = resource 151 | # do 152 | # result = ManageIQ::Providers::Showback::Manager::PricePlan.where(resource: parent) 153 | # parent = parent.parent if !result 154 | # while !result || !parent 155 | # result || ManageIQ::Providers::Showback::Manager::PricePlan.where(resource = MiqEnterprise) 156 | ManageIQ::Showback::PricePlan.first 157 | end 158 | 159 | def find_data_view(input) 160 | if input.kind_of?(ManageIQ::Showback::DataRollup) 161 | data_views.find_by(:data_rollup => input, :envelope => self) 162 | elsif input.kind_of?(ManageIQ::Showback::DataView) && (input.envelope == self) 163 | input 164 | end 165 | end 166 | 167 | private 168 | 169 | def generate_envelope(s_time, e_time) 170 | envelope = ManageIQ::Showback::Envelope.create(:name => name, 171 | :description => description, 172 | :resource => resource, 173 | :start_time => s_time, 174 | :end_time => e_time, 175 | :state => 'OPEN') 176 | data_views.each do |data_view| 177 | ManageIQ::Showback::DataView.create( 178 | :stored_data => { 179 | data_view.stored_data_last_key => data_view.stored_data_last 180 | }, 181 | :data_rollup => data_view.data_rollup, 182 | :envelope => envelope, 183 | :cost_subunits => data_view.cost_subunits, 184 | :cost_currency => data_view.cost_currency 185 | ) 186 | end 187 | end 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/input_measure.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback 2 | class InputMeasure < ApplicationRecord 3 | validates :description, :entity, :group, :fields, :presence => true 4 | 5 | serialize :fields, :type => Array 6 | 7 | self.table_name = "showback_input_measures" 8 | 9 | def name 10 | "#{entity}::#{group}" 11 | end 12 | 13 | def self.seed 14 | seed_data.each do |input_group_attributtes| 15 | input_measure_entity = input_group_attributtes[:entity] 16 | input_measure_group = input_group_attributtes[:group] 17 | next if ManageIQ::Showback::InputMeasure.find_by(:entity => input_measure_entity, :group => input_measure_group) 18 | log_attrs = input_group_attributtes.slice(:entity, :description, :group, :fields) 19 | _log.info("Creating consumption usage type with parameters #{log_attrs.inspect}") 20 | _log.info("Creating #{input_measure_entity} consumption usage type...") 21 | input_measure_new = create(input_group_attributtes) 22 | input_measure_new.save 23 | _log.info("Creating #{input_measure_entity} consumption usage type... Complete") 24 | end 25 | end 26 | 27 | def self.seed_file_name 28 | @seed_file_name ||= Pathname.new(Gem.loaded_specs['manageiq-consumption'].full_gem_path).join("db", "fixtures", "input_measures.yml") 29 | end 30 | 31 | def self.seed_data 32 | File.exist?(seed_file_name) ? YAML.load_file(seed_file_name) : [] 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/manager.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback 2 | class Manager 3 | def self.name 4 | "Showback" 5 | end 6 | 7 | def self.ems_type 8 | @ems_type ||= "consumption_manager".freeze 9 | end 10 | 11 | def self.description 12 | @description ||= "Showback Manager".freeze 13 | end 14 | 15 | def self.update_data_rollups 16 | data_units = load_column_units 17 | generate_new_month unless Time.now.utc.strftime("%d").to_i != 1 18 | ManageIQ::Showback::DataRollup.data_rollups_actual_month.each do |event| 19 | event.update_data_rollup(data_units) 20 | event.save 21 | end 22 | end 23 | 24 | def self.init_month 25 | DateTime.now.utc.beginning_of_month 26 | end 27 | 28 | def self.generate_new_month 29 | data_rollups = ManageIQ::Showback::DataRollup.data_rollups_past_month 30 | data_rollups.each do |dr| 31 | next if ManageIQ::Showback::DataRollup.where(["start_time >= ?", init_month]).exists?(:resource=> dr.resource) 32 | generate_data_rollup_resource(dr.resource, DateTime.now.utc.beginning_of_month, load_column_units) 33 | end 34 | data_rollups 35 | end 36 | 37 | RESOURCES_TYPES = %w(Vm Container Service).freeze 38 | 39 | def self.generate_data_rollups 40 | RESOURCES_TYPES.each do |resource| 41 | resource.constantize.all.each do |one_resource| 42 | next if ManageIQ::Showback::DataRollup.where(["start_time >= ?", init_month]).exists?(:resource => one_resource) 43 | generate_data_rollup_resource(one_resource, DateTime.now.utc, load_column_units) 44 | end 45 | end 46 | end 47 | 48 | def self.generate_data_rollup_resource(resource, date, data_units) 49 | data_rollup = ManageIQ::Showback::DataRollup.new( 50 | :resource => resource, 51 | :start_time => date, 52 | :end_time => date 53 | ) 54 | data_rollup.generate_data(data_units) 55 | data_rollup.collect_tags 56 | data_rollup.assign_resource 57 | data_rollup.assign_by_tag 58 | data_rollup.save! 59 | end 60 | 61 | def self.seed 62 | ManageIQ::Showback::InputMeasure.seed 63 | ManageIQ::Showback::PricePlan.seed 64 | end 65 | 66 | def self.load_column_units 67 | File.exist?(seed_file_name) ? YAML.load_file(seed_file_name) : [] 68 | end 69 | 70 | def self.seed_file_name 71 | @seed_file_name ||= Pathname.new(Gem.loaded_specs['manageiq-consumption'].full_gem_path).join("app/models/manageiq/showback", "column_units.yml") 72 | end 73 | private_class_method :seed_file_name 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/price_plan.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback 2 | class PricePlan < ApplicationRecord 3 | self.table_name = 'showback_price_plans' 4 | has_many :rates, 5 | :dependent => :destroy, 6 | :inverse_of => :price_plan, 7 | :foreign_key => :showback_price_plan_id 8 | 9 | belongs_to :resource, :polymorphic => true 10 | 11 | validates :name, :presence => true 12 | validates :description, :presence => true 13 | validates :resource, :presence => true 14 | 15 | ################################################################### 16 | # Calculate the total cost of an event 17 | # Called with an event 18 | # Returns the total accumulated costs for all rates that apply 19 | ################################################################### 20 | def calculate_total_cost(data_rollup, cycle_duration = nil) 21 | total = 0 22 | calculate_list_of_costs(data_rollup, cycle_duration).each do |x| 23 | total += x[0] 24 | end 25 | total 26 | end 27 | 28 | def calculate_list_of_costs(data_rollup, cycle_duration = nil) 29 | cycle_duration ||= data_rollup.month_duration 30 | resource_type = data_rollup.resource&.type || data_rollup.resource_type 31 | # Accumulator 32 | tc = [] 33 | # For each group type in InputMeasure, I need to find the rates applying to the different fields 34 | # If there is a rate associated to it, we call it with a group (that can be 0) 35 | ManageIQ::Showback::InputMeasure.where(:entity => resource_type).each do |usage| 36 | usage.fields.each do |dim| 37 | price_plan_rates = rates.where(:entity => usage.entity, :group => usage.group, :field => dim) 38 | price_plan_rates.each do |r| 39 | next unless ManageIQ::Showback::UtilsHelper.included_in?(data_rollup.context, r.screener) 40 | tc << [r.rate(data_rollup, cycle_duration), r] 41 | end 42 | end 43 | end 44 | tc 45 | end 46 | 47 | # Calculate the list of costs using input data instead of an event 48 | def calculate_list_of_costs_input(resource_type:, 49 | data:, 50 | context: nil, 51 | start_time: nil, 52 | end_time: nil, 53 | cycle_duration: nil) 54 | data_rollup = ManageIQ::Showback::DataRollup.new 55 | data_rollup.resource_type = resource_type 56 | data_rollup.data = data 57 | data_rollup.context = context || {} 58 | data_rollup.start_time = start_time || Time.current.beginning_of_month 59 | data_rollup.end_time = end_time || Time.current.end_of_month 60 | calculate_list_of_costs(data_rollup, cycle_duration) 61 | end 62 | 63 | # 64 | # Seeding one global price plan in the system that will be used as a fallback 65 | # 66 | def self.seed 67 | seed_data.each do |plan_attributes| 68 | plan_attributes_name = plan_attributes[:name] 69 | plan_attributes_description = plan_attributes[:description] 70 | plan_attributes_resource = plan_attributes[:resource_type].constantize.send(:find_by, :name => plan_attributes[:resource_name]) 71 | 72 | next if ManageIQ::Showback::PricePlan.find_by(:name => plan_attributes_name, :resource => plan_attributes_resource) 73 | log_attrs = plan_attributes.slice(:name, :description, :resource_name, :resource_type) 74 | _log.info("Creating consumption price plan with parameters #{log_attrs.inspect}") 75 | _log.info("Creating #{plan_attributes_name} consumption price plan...") 76 | price_plan_new = create(:name => plan_attributes_name, :description => plan_attributes_description, :resource => plan_attributes_resource) 77 | price_plan_new.save 78 | _log.info("Creating #{plan_attributes_name} consumption price plan... Complete") 79 | end 80 | end 81 | 82 | def self.seed_file_name 83 | @seed_file_name ||= Pathname.new(Gem.loaded_specs['manageiq-consumption'].full_gem_path).join("db", "fixtures", "price_plans.yml") 84 | end 85 | 86 | def self.seed_data 87 | File.exist?(seed_file_name) ? YAML.load_file(seed_file_name) : [] 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/rate.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback 2 | class Rate < ApplicationRecord 3 | VALID_RATE_CALCULATIONS = %w(occurrence duration quantity).freeze 4 | self.table_name = 'showback_rates' 5 | 6 | belongs_to :price_plan, :inverse_of => :rates, :foreign_key => :showback_price_plan_id 7 | has_many :tiers, 8 | :inverse_of => :rate, 9 | :foreign_key => :showback_rate_id 10 | 11 | validates :calculation, :presence => true, :inclusion => { :in => VALID_RATE_CALCULATIONS } 12 | validates :entity, :presence => true 13 | validates :field, :presence => true 14 | 15 | # There is no fixed_rate unit (only presence or not, and time), TODO: change column name in a migration 16 | validates :group, :presence => true 17 | default_value_for :group, '' 18 | 19 | default_value_for :screener, { } 20 | 21 | # Variable uses_single_tier to indicate if the rate only apply in the tier where the value is included 22 | # (defaults to `true`) 23 | # @return [Boolean] 24 | default_value_for :uses_single_tier, true 25 | 26 | # Variable tiers_use_full_value 27 | # (defaults to `true`) 28 | # @return [Boolean] 29 | default_value_for :tiers_use_full_value, true 30 | 31 | default_value_for :tier_input_variable, '' 32 | 33 | validates :screener, :exclusion => { :in => [nil] } 34 | 35 | after_create :create_zero_tier 36 | 37 | def name 38 | "#{entity}:#{group}:#{field}" 39 | end 40 | 41 | # Create a Zero tier when the rate is create 42 | # (defaults to `:html`) 43 | # 44 | # == Returns: 45 | # A Tier created with interval 0 - Infinity 46 | # 47 | def create_zero_tier 48 | ManageIQ::Showback::Tier.create(:tier_start_value => 0, :tier_end_value => Float::INFINITY, :rate => self) 49 | end 50 | 51 | def rate(event, cycle_duration = nil) 52 | # Find tier (use context) 53 | # Calculate value within tier 54 | # For each tier used, calculate costs 55 | value, groupment = event.get_group(group, field) # Returns group and the unit 56 | tiers = get_tiers(value || 0) 57 | duration = cycle_duration || event.month_duration 58 | # To do event.resource.type should be eq to entity 59 | acc = 0 60 | adjusted_value = value # Just in case we need to update it 61 | # If there is a step defined, we use it to adjust input to it 62 | tiers.each do |tier| 63 | # If there is a step defined, we use it to adjust input to it 64 | unless tier.step_value.nil? || tier.step_unit.nil? 65 | # Convert step and value to the same unit (variable_rate_per_unit) and calculate real values with the minimum step) 66 | adjusted_step = UnitsConverterHelper.to_unit(tier.step_value, tier.step_unit, tier.variable_rate_per_unit) 67 | tier_start_value = UnitsConverterHelper.to_unit(tier.tier_start_value, tier_input_variable, groupment) 68 | tier_value = tiers_use_full_value ? value : value - tier_start_value 69 | divmod = UnitsConverterHelper.to_unit(tier_value, groupment, tier.variable_rate_per_unit).divmod(adjusted_step) 70 | adjusted_value = (divmod[0] + (divmod[1].zero? ? 0 : 1)) * adjusted_step 71 | groupment = tier.variable_rate_per_unit # Updated value with new groupment as we have updated values 72 | end 73 | # If there is a step time defined, we use it to adjust input to it 74 | adjusted_time_span = event.time_span 75 | acc += rate_with_values(tier, adjusted_value, groupment, adjusted_time_span, duration) 76 | end 77 | acc 78 | end 79 | 80 | def rate_with_values(tier, value, group, time_span, cycle_duration, date = Time.current) 81 | send(calculation.downcase, tier, value, group, time_span, cycle_duration, date) 82 | end 83 | 84 | private 85 | 86 | def get_tiers(value) 87 | if uses_single_tier 88 | tiers.where("tier_start_value <= ? AND tier_end_value > ?", value, value) 89 | else 90 | tiers.where("tier_start_value <= ?", value) 91 | end 92 | end 93 | 94 | def occurrence(tier, value, _group, _time_span, cycle_duration, date) 95 | # Returns fixed_cost always + variable_cost sometimes 96 | # Fixed cost are always added fully, variable costs are only added if value is not nil 97 | # fix_inter: number of intervals in the calculation => how many times do we need to apply the rate to get a monthly (cycle) rate (min = 1) 98 | # fix_inter * fixed_rate == interval_rate (i.e. monthly) 99 | # var_inter * variable_rate == interval_rate (i.e. monthly) 100 | fix_inter = TimeConverterHelper.number_of_intervals( 101 | :period => cycle_duration, 102 | :interval => tier.fixed_rate_per_time, 103 | :calculation_date => date 104 | ) 105 | var_inter = TimeConverterHelper.number_of_intervals( 106 | :period => cycle_duration, 107 | :interval => tier.variable_rate_per_time, 108 | :calculation_date => date 109 | ) 110 | fix_inter * tier.fixed_rate + (value ? var_inter * tier.variable_rate : 0) # fixed always, variable if value 111 | end 112 | 113 | def duration(tier, value, group, time_span, cycle_duration, date) 114 | # Returns fixed_cost + variable costs taking into account value and duration 115 | # Fixed cost and variable costs are prorated on time 116 | # time_span = end_time - start_time (duration of the event) 117 | # cycle_duration: duration of the cycle (i.e 1.month, 1.week, 1.hour) 118 | # fix_inter: number of intervals in the calculation => how many time do we need to apply the rate to get a monthly (cycle) rate (min = 1) 119 | # fix_inter * fixed_rate == interval_rate (i.e. monthly from hourly) 120 | # var_inter * variable_rate == interval_rate (i.e. monthly variable from hourly) 121 | return Money.new(0) unless value # If value is null, the event is not present and thus we return 0 122 | fix_inter = TimeConverterHelper.number_of_intervals( 123 | :period => cycle_duration, 124 | :interval => tier.fixed_rate_per_time, 125 | :calculation_date => date 126 | ) 127 | var_inter = TimeConverterHelper.number_of_intervals( 128 | :period => cycle_duration, 129 | :interval => tier.variable_rate_per_time, 130 | :calculation_date => date 131 | ) 132 | value_in_rate_units = UnitsConverterHelper.to_unit(value.to_f, group, tier.variable_rate_per_unit) || 0 133 | ((fix_inter * tier.fixed_rate) + (var_inter * value_in_rate_units * tier.variable_rate)) * time_span.to_f / cycle_duration 134 | end 135 | 136 | def quantity(tier, value, group, _time_span, cycle_duration, date) 137 | # Returns costs based on quantity (independently of duration). 138 | # Fixed cost are calculated per period (i.e. 5€/month). You could use occurrence or duration 139 | # time_span = end_time - start_time 140 | # cycle_duration: duration of the cycle (i.e 1.month) 141 | # fix_inter: number of intervals in the calculation => how many time do we need to apply the rate to get a monthly rate 142 | # fix_inter * fixed_rate == interval_rate (i.e. monthly) 143 | return Money.new(0) unless value # If value is null, the event is not present and thus we return 0 144 | fix_inter = TimeConverterHelper.number_of_intervals( 145 | :period => cycle_duration, 146 | :interval => tier.fixed_rate_per_time, 147 | :calculation_date => date 148 | ) 149 | value_in_rate_units = UnitsConverterHelper.to_unit(value, group, tier.variable_rate_per_unit) || 0 150 | (fix_inter * tier.fixed_rate) + (value_in_rate_units * tier.variable_rate) 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /app/models/manageiq/showback/tier.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ::Showback 2 | class Tier < ApplicationRecord 3 | self.table_name = 'showback_tiers' 4 | belongs_to :rate, :inverse_of => :tiers, :foreign_key => :showback_rate_id 5 | 6 | # Fixed rate costs 7 | # (defaults to `0`) 8 | # @return [Float] the subunits of a fixed rate 9 | monetize(:fixed_rate_subunits, :with_model_currency => :currency) 10 | default_value_for :fixed_rate, Money.new(0) 11 | 12 | # Variable rate costs 13 | # (defaults to `0`) 14 | # @return [Float] the subunits of a fixed rate 15 | monetize(:variable_rate_subunits, :with_model_currency => :currency) 16 | default_value_for :variable_rate, Money.new(0) 17 | 18 | # Fixed rate time apply 19 | # (defaults to `monthly`) 20 | # @return [String] a value of VALID_INTERVAL_UNITS = %w(hourly daily weekly monthly yearly) 21 | validates :fixed_rate_per_time, :inclusion => { :in => TimeConverterHelper::VALID_INTERVAL_UNITS } 22 | default_value_for :fixed_rate_per_time, 'monthly' 23 | 24 | # Variable rate time apply 25 | # (defaults to `monthly`) 26 | # @return [String] a value of VALID_INTERVAL_UNITS = %w(hourly daily weekly monthly yearly) 27 | validates :variable_rate_per_time, :inclusion => { :in => TimeConverterHelper::VALID_INTERVAL_UNITS } 28 | default_value_for :variable_rate_per_time, 'monthly' 29 | 30 | validates :variable_rate_per_unit, :presence => true, :allow_blank => true 31 | validates :variable_rate_per_unit, :exclusion => { :in => [nil] } 32 | default_value_for :variable_rate_per_unit, '' 33 | 34 | # Variable tier_start_value is the start value of the tier interval 35 | # @return [Float] 36 | validates :tier_start_value, :numericality => {:greater_than_or_equal_to => 0, :less_than => Float::INFINITY} 37 | 38 | # Variable tier_start_value is the end value of the tier interval 39 | # @return [Float] 40 | validates :tier_end_value, :numericality => {:greater_than_or_equal_to => 0} 41 | 42 | validate :validate_interval 43 | 44 | # Get a representation string of the object 45 | # 46 | # @return [String] the definition of the object 47 | def name 48 | "#{rate.entity}:#{rate.group}:#{rate.field}:Tier:#{tier_start_value}-#{tier_end_value}" 49 | end 50 | 51 | # Validate the interval bvefore save 52 | # 53 | # @return Nothing or Error if the interval is in another tier in the same rate 54 | def validate_interval 55 | raise _("Start value of interval is greater than end value") unless tier_start_value < tier_end_value 56 | ManageIQ::Showback::Tier.where(:rate => rate).each do |tier| 57 | # Returns true == overlap, false == no overlap 58 | next unless self != tier 59 | if tier.tier_end_value == Float::INFINITY && (tier_start_value > tier.tier_start_value || tier_end_value > tier.tier_start_value) 60 | raise _("Interval or subinterval is in a tier with Infinity at the end") 61 | end 62 | raise _("Interval or subinterval is in another tier") if included?(tier.tier_start_value, tier.tier_end_value) 63 | end 64 | end 65 | 66 | # Get the range of the tier in appropiate format 67 | # 68 | # (see #set_range) 69 | # @return [Range] the range of the tier. 70 | def range 71 | (tier_start_value..tier_end_value) # or as an array, or however you want to return it 72 | end 73 | 74 | # Set the range of the tier 75 | # 76 | # (see #range) 77 | def set_range(srange, erange) 78 | self.tier_start_value = srange 79 | self.tier_end_value = erange 80 | self.save 81 | end 82 | 83 | # Method to create another tier partition 84 | # == Parameters: 85 | # ::value 86 | # Is the value to split the tier 87 | # == Returns: 88 | # Create the new tier from value to the end of the original tier 89 | # 90 | def divide_tier(value) 91 | old = tier_end_value 92 | self.tier_end_value = value 93 | self.save 94 | new_tier = self.dup 95 | new_tier.tier_end_value = old 96 | new_tier.tier_start_value = value 97 | new_tier.save 98 | end 99 | 100 | # Method to convert to float the value 101 | # == Parameters: 102 | # ::value 103 | # Is the value to convert 104 | # == Returns: 105 | # Float value 106 | # 107 | def self.to_float(s) 108 | if s.to_s.include?("Infinity") 109 | Float::INFINITY 110 | else 111 | s 112 | end 113 | end 114 | 115 | # Check if value is inside the interval of the tier 116 | # == Parameters: 117 | # ::value 118 | # Is the value to check if is inside 119 | # == Returns: 120 | # True if value is included 121 | # False is not 122 | # 123 | def includes?(value) 124 | starts_with_zero? && value.zero? || value > tier_start_value && value.to_f <= tier_end_value 125 | end 126 | 127 | # Check if the tier start in ZERO 128 | # 129 | # == Returns: 130 | # True if tier_start value is ZERO 131 | # False is not 132 | # 133 | def starts_with_zero? 134 | tier_start_value.zero? 135 | end 136 | 137 | # Check if the tier end in INFINITY 138 | # 139 | # == Returns: 140 | # True if tier_end value is INFINITY 141 | # False is not 142 | # 143 | def ends_with_infinity? 144 | tier_end_value == Float::INFINITY 145 | end 146 | 147 | # Check if the tier is free 148 | # 149 | # == Returns: 150 | # True if fixed_rate and variable_rate are zero 151 | # False is not 152 | # 153 | def free? 154 | fixed_rate.zero? && variable_rate.zero? 155 | end 156 | 157 | private 158 | 159 | # Check if the tier is included in a interval 160 | # 161 | # == Parameters: 162 | # start_value:: 163 | # Is the initial value of the interval 164 | # end_value:: 165 | # Is the end value of the interval 166 | # == Returns: 167 | # True if is included 168 | # False is not 169 | # 170 | def included?(start_value, end_value) 171 | return false if tier_end_value < start_value 172 | return false if tier_start_value >= end_value 173 | true 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /bin/before_install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -n "$CI" ]; then 4 | echo "== Installing system packages ==" 5 | sudo apt-get update 6 | sudo apt-get install -y libcurl4-openssl-dev 7 | echo 8 | fi 9 | 10 | gem_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." &>/dev/null && pwd)" 11 | spec_manageiq="$gem_root/spec/manageiq" 12 | 13 | if [ -n "$MANAGEIQ_REPO" ]; then 14 | echo "== Symlinking spec/manageiq to $MANAGEIQ_REPO ==" 15 | rm -rf "$spec_manageiq" 16 | ln -s "$(cd "$MANAGEIQ_REPO" &>/dev/null && pwd)" "$spec_manageiq" 17 | elif [ ! -d "$spec_manageiq" ]; then 18 | echo "== Cloning manageiq sample app ==" 19 | git clone https://github.com/ManageIQ/manageiq.git --branch master --depth 1 "$spec_manageiq" 20 | fi 21 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "manageiq/consumption" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 6 | ENGINE_PATH = File.expand_path('../../lib/manageiq/consumption/engine', __FILE__) 7 | APP_PATH = File.expand_path('../../spec/manageiq/config/application', __FILE__) 8 | 9 | # Set up gems listed in the Gemfile. 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 11 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 12 | 13 | require 'rails/all' 14 | require 'rails/engine/commands' 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "pathname" 4 | 5 | gem_root = Pathname.new(__dir__).join("..") 6 | system(gem_root.join("bin/before_install").to_s) 7 | 8 | require gem_root.join("spec/manageiq/lib/manageiq/environment") 9 | ManageIQ::Environment.manageiq_plugin_setup(gem_root) 10 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | gem_root = Pathname.new(__dir__).join("..") 5 | 6 | if gem_root.join("spec/manageiq").symlink? 7 | puts "== SKIPPING update of spec/manageiq because it is symlinked ==" 8 | else 9 | puts "== Updating manageiq sample app ==" 10 | system("git pull", :chdir => gem_root.join("spec/manageiq")) 11 | end 12 | 13 | require gem_root.join("spec/manageiq/lib/manageiq/environment").to_s 14 | ManageIQ::Environment.manageiq_plugin_update(gem_root) 15 | -------------------------------------------------------------------------------- /bundler.d/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-consumption/15df18d6945094f22a4e4848a535bd2d3517112c/bundler.d/.keep -------------------------------------------------------------------------------- /config/initializers/money.rb: -------------------------------------------------------------------------------- 1 | # encoding : utf-8 2 | Money.locale_backend = :i18n 3 | 4 | MoneyRails.configure do |config| 5 | 6 | # To set the default currency 7 | # 8 | 9 | # Set default bank object 10 | # 11 | # Example: 12 | # config.default_bank = EuCentralBank.new 13 | 14 | # Add exchange rates to current money bank object. 15 | # (The conversion rate refers to one direction only) 16 | # 17 | # Example: 18 | # config.add_rate "USD", "CAD", 1.24515 19 | # config.add_rate "CAD", "USD", 0.803115 20 | 21 | # To handle the inclusion of validations for monetized fields 22 | # The default value is true 23 | # 24 | # config.include_validations = true 25 | 26 | # Default ActiveRecord migration configuration values for columns: 27 | # 28 | config.amount_column = { prefix: '', # column name prefix 29 | postfix: '_subunits', # column name postfix 30 | column_name: nil, # full column name (overrides prefix, postfix and accessor name) 31 | type: :bigint, # column type 32 | present: true, # column will be created 33 | null: false, # other options will be treated as column options 34 | default: 0 35 | } 36 | 37 | config.currency_column = { prefix: '', 38 | postfix: '_currency', 39 | column_name: nil, 40 | type: :string, 41 | present: true, 42 | null: false, 43 | default: 'US8' 44 | } 45 | 46 | config.register_currency = { 47 | :priority => 1, 48 | :iso_code => :us8, 49 | :name => "US Dollar with subunit of 8 digits", 50 | :symbol => "$", 51 | :symbol_first => true, 52 | :subunit => "Subcent", 53 | :subunit_to_unit => 100_000_000, 54 | :thousands_separator => ",", 55 | :decimal_mark => "." 56 | } 57 | 58 | config.add_rate "USD", "US8", 1 59 | config.add_rate "US8", "USD", 1 60 | 61 | config.default_currency = :us8 62 | 63 | # Register a custom currency 64 | # 65 | # Example: 66 | # config.register_currency = { 67 | # :priority => 1, 68 | # :iso_code => "EU4", 69 | # :name => "Euro with subunit of 4 digits", 70 | # :symbol => "€", 71 | # :symbol_first => true, 72 | # :subunit => "Subcent", 73 | # :subunit_to_unit => 10000, 74 | # :thousands_separator => ".", 75 | # :decimal_mark => "," 76 | # } 77 | 78 | # Specify a rounding mode 79 | # Any one of: 80 | # 81 | # BigDecimal::ROUND_UP, 82 | # BigDecimal::ROUND_DOWN, 83 | # BigDecimal::ROUND_HALF_UP, 84 | # BigDecimal::ROUND_HALF_DOWN, 85 | # BigDecimal::ROUND_HALF_EVEN, 86 | # BigDecimal::ROUND_CEILING, 87 | # BigDecimal::ROUND_FLOOR 88 | # 89 | # set to BigDecimal::ROUND_HALF_EVEN by default 90 | # 91 | config.rounding_mode = BigDecimal::ROUND_HALF_EVEN 92 | 93 | # Set default money format globally. 94 | # Default value is nil meaning "ignore this option". 95 | # Example: 96 | # 97 | # config.default_format = { 98 | # :no_cents_if_whole => nil, 99 | # :symbol => nil, 100 | # :sign_before_symbol => nil 101 | # } 102 | 103 | # Set default raise_error_on_money_parsing option 104 | # It will be raise error if assigned different currency 105 | # The default value is false 106 | # 107 | # Example: 108 | # config.raise_error_on_money_parsing = false 109 | end 110 | -------------------------------------------------------------------------------- /config/settings.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-consumption/15df18d6945094f22a4e4848a535bd2d3517112c/config/settings.yml -------------------------------------------------------------------------------- /db/fixtures/input_measures.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - :entity: "Vm" 3 | :description: "Calculate CPU Showback of VM" 4 | :group: "CPU" 5 | :fields: ["average", "number", "max_number_of_cpu"] 6 | - :entity: "Vm" 7 | :description: "Calculate MEM Showback of VM" 8 | :group: "MEM" 9 | :fields: ["max_mem"] 10 | - :entity: "Vm" 11 | :description: "Template Showback of VM" 12 | :group: "FLAVOR" 13 | :fields: ["cpu_reserved","memory_reserved"] 14 | - :entity: "Container" 15 | :description: "Calculate CPU Showback of Container" 16 | :group: "CPU" 17 | :fields: ["max_number_of_cpu"] 18 | - :entity: "Container" 19 | :description: "Calculate MEM Showback of Container" 20 | :group: "MEM" 21 | :fields: ["max_mem"] 22 | - :entity: "Container" 23 | :description: "Template Showback of VM" 24 | :group: "FLAVOR" 25 | :fields: ["cpu_reserved","memory_reserved"] 26 | # For integration with old chargeback 27 | - :entity: "Container" 28 | :description: "TBD" 29 | :group: "cpu" 30 | :fields: ["cpu_usagemhz_rate_average"] 31 | - :entity: "Container" 32 | :description: "TBD" 33 | :group: "cpu_cores" 34 | :fields: ["v_derived_cpu_total_cores_used", "derived_vm_numvcpus", "cpu_usage_rate_average"] 35 | - :entity: "Container" 36 | :description: "TBD" 37 | :group: "memory" 38 | :fields: ["derived_memory_used", "derived_memory_available"] 39 | - :entity: "Container" 40 | :description: "TBD" 41 | :group: "net_io" 42 | :fields: ["net_usage_rate_average"] 43 | - :entity: "Container" 44 | :description: "TBD" 45 | :group: "disk_io" 46 | :fields: ["disk_usage_rate_average"] 47 | - :entity: "Container" 48 | :description: "TBD" 49 | :group: "fixed" 50 | :fields: ["fixed_compute_1", "fixed_compute_2", "fixed_storage_1", "fixed_storage_2"] 51 | 52 | - :entity: "Container" 53 | :description: "TBD" 54 | :group: "metering" 55 | :fields: ["metering_used_hours"] 56 | - :entity: "ContainerImage" 57 | :description: "TBD" 58 | :group: "cpu" 59 | :fields: ["cpu_usagemhz_rate_average"] 60 | - :entity: "ContainerImage" 61 | :description: "TBD" 62 | :group: "cpu_cores" 63 | :fields: ["v_derived_cpu_total_cores_used", "derived_vm_numvcpus", "cpu_usage_rate_average"] 64 | - :entity: "ContainerImage" 65 | :description: "TBD" 66 | :group: "memory" 67 | :fields: ["derived_memory_used", "derived_memory_available"] 68 | - :entity: "ContainerImage" 69 | :description: "TBD" 70 | :group: "net_io" 71 | :fields: ["net_usage_rate_average"] 72 | - :entity: "ContainerImage" 73 | :description: "TBD" 74 | :group: "disk_io" 75 | :fields: ["disk_usage_rate_average"] 76 | - :entity: "ContainerImage" 77 | :description: "TBD" 78 | :group: "fixed" 79 | :fields: ["fixed_compute_1", "fixed_compute_2", "fixed_storage_1", "fixed_storage_2"] 80 | - :entity: "ContainerImage" 81 | :description: "TBD" 82 | :group: "metering" 83 | :fields: ["metering_used_hours"] 84 | 85 | - :entity: "Vm" 86 | :description: "TBD" 87 | :group: "cpu" 88 | :fields: ["derived_vm_numvcpus", "cpu_usagemhz_rate_average"] 89 | - :entity: "Vm" 90 | :description: "TBD" 91 | :group: "cpu_cores" 92 | :fields: ["v_derived_cpu_total_cores_used", "cpu_usage_rate_average"] 93 | - :entity: "Vm" 94 | :description: "TBD" 95 | :group: "memory" 96 | :fields: ["derived_memory_used", "derived_memory_available"] 97 | - :entity: "Vm" 98 | :description: "TBD" 99 | :group: "net_io" 100 | :fields: ["net_usage_rate_average"] 101 | - :entity: "Vm" 102 | :description: "TBD" 103 | :group: "disk_io" 104 | :fields: ["disk_usage_rate_average"] 105 | - :entity: "Vm" 106 | :description: "TBD" 107 | :group: "fixed" 108 | :fields: ["fixed_compute_1", "fixed_compute_2", "fixed_storage_1", "fixed_storage_2", "fixed_storage_2", "fixed_storage_1"] 109 | - :entity: "Vm" 110 | :description: "TBD" 111 | :group: "metering" 112 | :fields: ["metering_used_hours"] 113 | - :entity: "Vm" 114 | :description: "TBD" 115 | :group: "storage" 116 | :fields: ["derived_vm_allocated_disk_storage", "derived_vm_used_disk_storage"] 117 | -------------------------------------------------------------------------------- /db/fixtures/price_plans.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - :name: "Enterprise" 3 | :description: "Enterprise price plan" 4 | :resource_name: "Enterprise" 5 | :resource_type: "MiqEnterprise" -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Manageiq-Consumption Gem Documentation 2 | 3 | # Demo 4 | 5 | Go to [demo](/docs/demo) section 6 | 7 | # Benchmarks 8 | 9 | Go to [benchmarks](/docs/benchmarks) section 10 | 11 | # Architecture 12 | 13 | -------------------------------------------------------------------------------- /docs/benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | ## EVENTS 4 | ### Runner 5 | 6 | PATH_TO_RUBY_BENCHMARK_FILE = [/docs/benchmarks/benchmark_events.rb](/docs/benchmarks/benchmark_events.rb) 7 | 8 | INPUT 4 integers. 9 | 10 | - PROVIDERS (default 2) 11 | - HOSTS x PROVIDERS (default 2) 12 | - VMS X HOSTS x PROVIDERS (default 4) 13 | - METRICS X VMS X HOSTS x PROVIDERS (default 10) 14 | - NEWMETRICS to add for each vm after first event generation (default 4) 15 | 16 | Example 17 | ```ruby 18 | 19 | bin/rails r PATH_TO_RUBY_BENCHMARK_FILE 2 2 4 10 4 20 | 21 | ``` 22 | 23 | This generate: 24 | 25 | - 2 providers with 3 hosts each provider (6 hosts) 26 | - 4 vms each hosts (24 vms) 27 | - 10 metrics by vm (240 metrics) 28 | 29 | 30 | Output 31 | 32 | ```bash 33 | Benchmark show user CPU time, system CPU time and elapsed real time 34 | Generating infrastructure for 2 providers with 2 hosts and 4 vms for each host, the vms have 10 metrics and 4 after generate 35 | Generated infrastructure in 7.199999999999999 0.6799999999999999 10.425814000000173 36 | Generated ShowbackUsageType seed in 0.010000000000001563 0.0 0.0137959999992745 37 | Generated ShowbackPricePlan seed in 0.05999999999999872 0.0 0.058042000000568805 38 | Generated Pools for 2 Providers0.02000000000000135 0.010000000000000231 0.02380999999877531 39 | Generated events for 16 vms in 6.8199999999999985 0.19999999999999973 7.687425000000076 40 | Updating events in 6.359999999999999 0.1499999999999999 8.571742000000086 41 | Adding new metrics 4 x 4 (16) vms in 6.190000000000001 0.3900000000000001 14.060204999999769 42 | Updating events with new metrics in 7.159999999999997 0.20000000000000018 10.020586000000549 43 | Clean infrastructure in 0.010000000000005116 0.0 0.019666000000142958 44 | ``` -------------------------------------------------------------------------------- /docs/benchmarks/benchmark_events.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | 3 | PROVIDERS = ARGV[0] || 2 4 | HOSTS = ARGV[1] || 2 5 | VMS = ARGV[2] || 4 6 | METRICS = ARGV[3] || 10 7 | NEWMETRICS = ARGV[4] || 4 8 | 9 | 10 | class String 11 | def black; "\033[30m#{self}\033[0m" end 12 | def red; "\033[31m#{self}\033[0m" end 13 | def green; "\033[32m#{self}\033[0m" end 14 | def brown; "\033[33m#{self}\033[0m" end 15 | def blue; "\033[34m#{self}\033[0m" end 16 | def magenta; "\033[35m#{self}\033[0m" end 17 | def cyan; "\033[36m#{self}\033[0m" end 18 | def gray; "\033[37m#{self}\033[0m" end 19 | end 20 | 21 | def generateHardware(n) 22 | h = Hardware.new 23 | h.cpu_sockets = n 24 | h.cpu_cores_per_socket = n%2 25 | h.cpu_total_cores = n 26 | h.memory_mb = n*2048 27 | h.save 28 | return h 29 | end 30 | 31 | 32 | def generateInfrastructure 33 | date_seed = DateTime.now + 5.hours 34 | for i in 0..PROVIDERS.to_i do 35 | ext = ExtManagementSystem.new 36 | ext.name = "Provider_#{i}" 37 | ext.zone = Zone.first 38 | ext.hostname = "rhev.manageiq.consumption" 39 | ext.save 40 | for h in 0..HOSTS.to_i do 41 | h = Host.new 42 | h.name = "Host_#{i}" 43 | h.hardware = generateHardware(i) 44 | h.hostname = "hostname_#{i}" 45 | h.vmm_vendor = "redhat" 46 | h.ems_id = ext.id 47 | if i%2==0 48 | h.tags=Tag.where(:name => "/managed/location/ny") 49 | else 50 | h.tags=Tag.where(:name => "/managed/location/chicago") 51 | end 52 | h.save 53 | for n in 0..VMS.to_i do 54 | v = Vm.new 55 | v.name = "Vm_#{n}" 56 | v.location = "unknown" 57 | v.hardware = generateHardware(n) 58 | v.vendor = "redhat" 59 | v.template = false 60 | v.host_id = h.id 61 | v.ems_id = ext.id 62 | v.save 63 | if i%2==0 64 | v.tags=Tag.where(:name => "/managed/location/ny") 65 | else 66 | v.tags=Tag.where(:name => "/managed/location/chicago") 67 | end 68 | v.save 69 | for m in 0..METRICS.to_i do 70 | v.metrics << Metric.create( 71 | :capture_interval_name => "realtime", 72 | :resource_type => "Vm", 73 | :timestamp => date_seed + m.hours + rand(0...2).days, 74 | :cpu_usage_rate_average => rand(0...100), 75 | # Multiply by a factor of 1000 to make it more realistic and enable testing virtual col v_pct_cpu_ready_delta_summation 76 | :cpu_ready_delta_summation => rand(0...100) * 1000, 77 | :sys_uptime_absolute_latest => rand(0...100) 78 | ) 79 | end 80 | end 81 | end 82 | end 83 | end 84 | 85 | def add_new_metrics 86 | date_seed = DateTime.now + 5.hours 87 | Vm.all.each do |v| 88 | for m in 0..NEWMETRICS.to_i do 89 | v.metrics << Metric.create( 90 | :capture_interval_name => "realtime", 91 | :resource_type => "Vm", 92 | :timestamp => date_seed + METRICS.to_i.hours + 3.days + m.hours, 93 | :cpu_usage_rate_average => rand(0...100), 94 | # Multiply by a factor of 1000 to make it more realistic and enable testing virtual col v_pct_cpu_ready_delta_summation 95 | :cpu_ready_delta_summation => rand(0...100) * 1000, 96 | :sys_uptime_absolute_latest => rand(0...100) 97 | ) 98 | end 99 | end 100 | end 101 | 102 | def cleanInfrastructure 103 | ExtManagementSystem.delete_all 104 | Host.delete_all 105 | Vm.delete_all 106 | Metric.delete_all 107 | ManageIQ::Consumption::ShowbackUsageType.delete_all 108 | ManageIQ::Consumption::ShowbackPricePlan.delete_all 109 | ManageIQ::Consumption::ShowbackEvent.delete_all 110 | ManageIQ::Consumption::ShowbackPool.delete_all 111 | ManageIQ::Consumption::ShowbackRate.delete_all 112 | ManageIQ::Consumption::ShowbackCharge.delete_all 113 | end 114 | 115 | def getPrettyBenchmark(array) 116 | "#{array[1].to_s.brown} #{array[2].to_s.blue} #{array[5].to_s.green}" 117 | end 118 | 119 | puts "Benchmark show user CPU time, system CPU time and elapsed real time" 120 | puts "Generating infrastructure for #{PROVIDERS} providers with #{HOSTS} hosts and #{VMS} vms for each host, the vms have #{METRICS} metrics and #{NEWMETRICS} after generate" 121 | puts "Generated infrastructure in " + getPrettyBenchmark(Benchmark.measure {generateInfrastructure()}.to_a) 122 | puts "Generated ShowbackUsageType seed in " + getPrettyBenchmark(Benchmark.measure {ManageIQ::Consumption::ShowbackUsageType.seed}.to_a) 123 | puts "Generated ShowbackPricePlan seed in " + getPrettyBenchmark(Benchmark.measure {ManageIQ::Consumption::ShowbackPricePlan.seed}.to_a) 124 | puts "Generated Pools for #{PROVIDERS} Providers " + getPrettyBenchmark(Benchmark.measure { 125 | ExtManagementSystem.all.each do |provider| 126 | ManageIQ::Consumption::ShowbackPool.new( 127 | :name => "Pool #{provider.name}", 128 | :description=>"one provider", 129 | :resource =>provider, 130 | :start_time => DateTime.now.beginning_of_month, 131 | :end_time => DateTime.now.end_of_month, 132 | :state => "OPEN").save 133 | end 134 | }.to_a) 135 | puts "Generated events for #{PROVIDERS.to_i*HOSTS.to_i*VMS.to_i} vms in "+ getPrettyBenchmark(Benchmark.measure {ManageIQ::Consumption::ConsumptionManager.generate_events}.to_a) 136 | puts "Updating events in "+ getPrettyBenchmark(Benchmark.measure {ManageIQ::Consumption::ConsumptionManager.update_events}.to_a) 137 | puts "Adding new metrics #{NEWMETRICS} x #{VMS} (#{NEWMETRICS.to_i * VMS.to_i}) vms in "+ getPrettyBenchmark(Benchmark.measure {add_new_metrics}.to_a) 138 | puts "Updating events with new metrics in "+ getPrettyBenchmark(Benchmark.measure {ManageIQ::Consumption::ConsumptionManager.update_events}.to_a) 139 | puts "Clean infrastructure in " + getPrettyBenchmark(Benchmark.measure {cleanInfrastructure()}.to_a) 140 | 141 | -------------------------------------------------------------------------------- /docs/demo/README.md: -------------------------------------------------------------------------------- 1 | # Manageiq Demo 2 | 3 | ## Install the Gem in manageiq 4 | 5 | Set in the Gemfile (for development, demos, you can use a .rb file in /bundler.d) 6 | 7 | ```ruby 8 | gem "manageiq-consumption", :git => "https://github.com/ManageIQ/manageiq-consumption.git", :branch => "master" 9 | ``` 10 | ## Set a play data 11 | 12 | PATH_TO_RUBY_SEED_PLAY_FILE is in [/docs/demo/seed_play_data.rb](/docs/demo/seed_play_data.rb) 13 | 14 | ```ruby 15 | bin/rails r PATH_TO_RUBY_SEED_PLAY_FILE 16 | ``` 17 | 18 | ### You could use a console where the information is deleted after you get out 19 | 20 | ```ruby 21 | bin/rails c -s 22 | ``` 23 | 24 | ```ruby 25 | load PATH_TO_RUBY_SEED_PLAY_FILE 26 | ``` 27 | 28 | ## Go to Rails Console if you haven't used the temporary console 29 | 30 | ```ruby 31 | 32 | bin/rails c 33 | ``` 34 | First we need to create ours seeds 35 | ```ruby 36 | 37 | ManageIQ::Consumption::ShowbackUsageType.seed 38 | ManageIQ::Consumption::ShowbackPricePlan.seed 39 | 40 | ``` 41 | We get the first Vm and his host and we define a pool associated to the host 42 | 43 | ```ruby 44 | host = Host.first 45 | ManageIQ::Consumption::ShowbackPool.new(:name => "Pool host",:description=>"First host",:resource =>host,:start_time => DateTime.now.beginning_of_month,:end_time => DateTime.now.end_of_month, :state => "OPEN").save 46 | ``` 47 | 48 | ConsumptionManager generate the events and we can get the event of the first vm of this host 49 | 50 | ```ruby 51 | ManageIQ::Consumption::ShowbackEvent.count 52 | # Returns 0 53 | ManageIQ::Consumption::ConsumptionManager.generate_events 54 | ManageIQ::Consumption::ShowbackEvent.where(:resource => host.vms.first) 55 | ``` 56 | 57 | This Event will generate with an empty data data: {"CPU"=>{"average"=>0, "number"=>0, "max_number_of_cpu"=>0}, "MEM"=>{"total_mem"=>0}} and context {"tag"=>{"location"=>["chicago"]}} 58 | 59 | ```ruby 60 | ManageIQ::Consumption::ConsumptionManager.update_events 61 | ``` 62 | 63 | After that if we call again to our event 64 | 65 | ```ruby 66 | ManageIQ::Consumption::ShowbackEvent.where(:resource => host.vms.first) 67 | ``` 68 | 69 | We get an updated data data: {"CPU"=>{"average"=>"51.7142857142857", "number"=>"0.0", "max_number_of_cpu"=>1}, "MEM"=>{"total_mem"=>0}} 70 | 71 | 72 | Now we can check that there is 4 events in the pool of the host, one event for each vm. 73 | 74 | ```ruby 75 | pool = ManageIQ::Consumption::ShowbackPool.first 76 | pool.showback_events 77 | ``` 78 | 79 | If we get the sum of this charges we get #, 80 | there are not charges nor price plans associated to the pool 81 | 82 | ```ruby 83 | pool.sum_of_charges 84 | ``` 85 | Now we can add some charges 86 | 87 | ```ruby 88 | pool.add_charge(pool.showback_events.first,10) 89 | pool.add_charge(pool.showback_events.second,20) 90 | pool.showback_charges.reload 91 | ``` 92 | And if we make now the sum we get the total 93 | ```ruby 94 | pool.sum_of_charges 95 | ``` 96 | 97 | # Showback 98 | 99 | Now we can define our PricePlan, we create one associated with our host. 100 | 101 | ```ruby 102 | ManageIQ::Consumption::ShowbackPricePlan.create(:name => "Host in chicago",:description=>"This host is in chicago",:resource => host).save 103 | plan = ManageIQ::Consumption::ShowbackPricePlan.where(:name=>"Host in chicago").first 104 | ``` 105 | 106 | And we define our rate 107 | 108 | ```ruby 109 | ManageIQ::Consumption::ShowbackRate.create(:showback_price_plan => plan, 110 | :fixed_rate => Money.new(11), 111 | :variable_rate => Money.new(7), 112 | :calculation => "duration", 113 | :category => "CPU", 114 | :dimension => "max_number_of_cpu").save! 115 | ``` 116 | 117 | Now the rate is associated to the price plan, and thus if you call calculate_charge in the pool it will use it 118 | -------------------------------------------------------------------------------- /docs/demo/seed_clean_db.rb: -------------------------------------------------------------------------------- 1 | ExtManagementSystem.delete_all 2 | Host.delete_all 3 | Vm.delete_all 4 | Metric.delete_all 5 | Hardware.delete_all 6 | ManageIQ::Consumption::ShowbackEvent.delete_all 7 | ManageIQ::Consumption::ShowbackPool.delete_all 8 | ManageIQ::Consumption::ShowbackRate.delete_all 9 | ManageIQ::Consumption::ShowbackCharge.delete_all -------------------------------------------------------------------------------- /docs/demo/seed_play_data.rb: -------------------------------------------------------------------------------- 1 | 2 | 3 | def generateHardware(n) 4 | h = Hardware.new 5 | h.cpu_sockets = n 6 | h.cpu_cores_per_socket = n%2 7 | h.cpu_total_cores = n 8 | h.memory_mb = n*2048 9 | h.save 10 | return h 11 | end 12 | 13 | #RHEV ExtManagement 14 | ext = ExtManagementSystem.new 15 | ext.name = "RHEV" 16 | ext.zone = Zone.first 17 | ext.hostname = "rhev.manageiq.consumption" 18 | ext.save 19 | 20 | for i in 1..9 do 21 | h = Host.new 22 | h.name = "host_#{i}" 23 | h.hardware = generateHardware(i) 24 | h.hostname = "hostname_#{i}" 25 | h.vmm_vendor = "redhat" 26 | h.ems_id = ext.id 27 | if i%2==0 28 | h.tags=Tag.where(:name => "/managed/location/ny") 29 | else 30 | h.tags=Tag.where(:name => "/managed/location/chicago") 31 | end 32 | h.save 33 | for n in 1..4 do 34 | v = Vm.new 35 | v.name = "vm_#{n}" 36 | v.location = "unknown" 37 | v.hardware = generateHardware(n) 38 | v.vendor = "redhat" 39 | v.template = false 40 | v.host_id = h.id 41 | v.ems_id = ext.id 42 | v.save 43 | if i%2==0 44 | v.tags=Tag.where(:name => "/managed/location/ny") 45 | else 46 | v.tags=Tag.where(:name => "/managed/location/chicago") 47 | end 48 | v.save 49 | date_seed = DateTime.now + 5.hours 50 | times = [ 51 | date_seed, 52 | date_seed + 1.days, 53 | date_seed + 1.days + 3.hours, 54 | date_seed + 2.days + 4.hours, 55 | date_seed + 2.days + 5.hours, 56 | date_seed + 2.days, 57 | date_seed + 3.days, 58 | ] 59 | times.each do |t| 60 | v.metrics << Metric.create( 61 | :capture_interval_name => "realtime", 62 | :resource_type => "Vm", 63 | :timestamp => t, 64 | :cpu_usage_rate_average => rand(0...100), 65 | # Multiply by a factor of 1000 to make it more realistic and enable testing virtual col v_pct_cpu_ready_delta_summation 66 | :cpu_ready_delta_summation => rand(0...100) * 1000, 67 | :sys_uptime_absolute_latest => rand(0...100) 68 | ) 69 | end 70 | end 71 | end 72 | 73 | -------------------------------------------------------------------------------- /lib/manageiq/consumption.rb: -------------------------------------------------------------------------------- 1 | require "manageiq/showback/engine" 2 | require "manageiq/showback/version" 3 | -------------------------------------------------------------------------------- /lib/manageiq/showback/engine.rb: -------------------------------------------------------------------------------- 1 | require 'money-rails' 2 | 3 | module ManageIQ 4 | module Showback 5 | class Engine < ::Rails::Engine 6 | isolate_namespace ManageIQ::Showback 7 | 8 | config.autoload_paths << root.join('lib').to_s 9 | 10 | def self.vmdb_plugin? 11 | true 12 | end 13 | 14 | def self.plugin_name 15 | _('Showback') 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/manageiq/showback/version.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ 2 | module Showback 3 | VERSION = "0.0.1".freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/tasks/README.md: -------------------------------------------------------------------------------- 1 | Tasks (.rake files) in this directory will be available as public tasks in the main 2 | ManageIQ app. They can be executed in the plugin gem via the app: namespace 3 | 4 | ```shell 5 | bin/rails app: 6 | ``` 7 | 8 | Since these tasks are public, please namespace them, as in the following example: 9 | 10 | ```ruby 11 | namespace "manageiq:consumption" do 12 | desc "Explaining what the task does" 13 | task :your_task do 14 | # Task goes here 15 | end 16 | end 17 | ``` 18 | 19 | Tasks places in the lib/tasks_private directory will be private to the plugin 20 | and not available in the ManageIQ app. 21 | -------------------------------------------------------------------------------- /lib/tasks_private/spec.rake: -------------------------------------------------------------------------------- 1 | namespace :spec do 2 | desc "Setup environment specs" 3 | task :setup => ["app:test:vmdb:setup"] 4 | end 5 | 6 | desc "Run all specs" 7 | RSpec::Core::RakeTask.new(:spec => 'app:test:spec_deps') do |t| 8 | EvmTestHelper.init_rspec_task(t) 9 | end 10 | -------------------------------------------------------------------------------- /locale/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-consumption/15df18d6945094f22a4e4848a535bd2d3517112c/locale/.keep -------------------------------------------------------------------------------- /manageiq-consumption.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'manageiq/showback/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "manageiq-consumption" 8 | spec.version = ManageIQ::Showback::VERSION 9 | spec.authors = ["ManageIQ Authors"] 10 | 11 | spec.summary = "Consumption plugin for ManageIQ." 12 | spec.description = "Consumption plugin for ManageIQ." 13 | spec.homepage = "https://github.com/ManageIQ/manageiq-consumption" 14 | spec.license = "Apache-2.0" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = "exe" 18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "money-rails", "~> 1.9" 22 | spec.add_dependency "hashdiff", "~> 1.0" 23 | 24 | spec.add_development_dependency "manageiq-style" 25 | spec.add_development_dependency "simplecov", ">= 0.21.2" 26 | end 27 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "inheritConfig": true, 4 | "inheritConfigRepoName": "manageiq/renovate-config" 5 | } 6 | -------------------------------------------------------------------------------- /spec/factories/data_rollup.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :data_rollup, :class => ManageIQ::Showback::DataRollup do 3 | association :resource, :factory => :vm 4 | start_time { 4.hours.ago } 5 | end_time { 1.hour.ago } 6 | context { { } } 7 | data { { } } 8 | 9 | trait :with_tags_in_context do 10 | context do 11 | { 12 | "tag" => { 13 | "environment" => ["test"] 14 | } 15 | } 16 | end 17 | end 18 | 19 | # trait :with_several_tags_in_context do 20 | 21 | trait :with_vm_data do 22 | data do 23 | { 24 | "CPU" => { 25 | "average" => [29.8571428571429, "percent"], 26 | "number" => [2.0, "cores"], 27 | "max_number_of_cpu" => [2, "cores"] 28 | }, 29 | "MEM" => { 30 | "max_mem" => [2048, "Mib"] 31 | }, 32 | "FLAVOR" => {} 33 | } 34 | end 35 | end 36 | 37 | trait :full_month do 38 | start_time { DateTime.now.utc.beginning_of_month } 39 | end_time { DateTime.now.utc.end_of_month } 40 | end 41 | 42 | trait :first_half_month do 43 | start_time { DateTime.now.utc.beginning_of_month } 44 | end_time { DateTime.now.utc.change(:day => 15).end_of_day } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/factories/data_view.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :data_view, :class => ManageIQ::Showback::DataView do 3 | envelope 4 | data_rollup 5 | cost { 0 } 6 | data_snapshot { { } } 7 | context_snapshot { { } } 8 | 9 | trait :with_data_snapshot do 10 | data_snapshot do 11 | { 12 | Time.now.utc.beginning_of_month => { 13 | "CPU" => { 14 | "average" => [39.859, "percent"], 15 | "number" => [2.0, "cores"], 16 | "max_number_of_cpu" => [2, "cores"] 17 | }, 18 | "MEM" => { 19 | "max_mem" => [2048, "Mib"] 20 | }, 21 | "FLAVOR" => {} 22 | }, 23 | Time.now.utc.beginning_of_month + 15.days => { 24 | "CPU" => { 25 | "average" => [49.8571428571429, "percent"], 26 | "number" => [4.0, "cores"], 27 | "max_number_of_cpu" => [4, "cores"] 28 | }, 29 | "MEM" => { 30 | "max_mem" => [8192, "Mib"] 31 | }, 32 | "FLAVOR" => {} 33 | } 34 | } 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/factories/envelope.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :envelope, :class => ManageIQ::Showback::Envelope do 3 | sequence(:name) { |n| "factory_pool_#{seq_padded_for_sorting(n)}" } 4 | sequence(:description) { |n| "pool_description_#{seq_padded_for_sorting(n)}" } 5 | start_time { 4.hours.ago } 6 | end_time { 1.hour.ago } 7 | state { 'OPEN' } 8 | association :resource, :factory => :miq_enterprise, :strategy => :build_stubbed 9 | 10 | trait :processing do 11 | state { 'PROCESSING' } 12 | end 13 | 14 | trait :closed do 15 | state { 'CLOSED' } 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/factories/input_measure.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :input_measure, :class => ManageIQ::Showback::InputMeasure do 3 | entity { 'Vm' } 4 | sequence(:description) { |s| "Description #{s}" } 5 | group { 'CPU' } 6 | fields { ['average'] } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/factories/price_plan.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :price_plan, :class => ManageIQ::Showback::PricePlan do 3 | sequence(:name) { |n| "factory__price_plan_#{seq_padded_for_sorting(n)}" } 4 | sequence(:description) { |n| "price_plan_description_#{seq_padded_for_sorting(n)}" } 5 | association :resource, :factory => :miq_enterprise, :strategy => :build_stubbed 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/rate.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :rate, :class => ManageIQ::Showback::Rate do 3 | entity { 'Vm' } 4 | field { 'max_number_of_cpu' } 5 | group { 'CPU' } 6 | sequence(:concept) { |n| "Concept #{n}" } 7 | screener { {} } 8 | calculation { 'duration' } 9 | price_plan 10 | 11 | trait :occurrence do 12 | calculation { 'occurrence' } 13 | end 14 | trait :duration do 15 | calculation { 'duration' } 16 | end 17 | trait :quantity do 18 | calculation { 'quantity' } 19 | end 20 | trait :with_screener do 21 | screener do 22 | { 23 | 'tag' => { 24 | 'environment' => ['test'] 25 | } 26 | } 27 | end 28 | end 29 | trait :CPU_average do 30 | group { 'CPU' } 31 | field { 'average' } 32 | end 33 | trait :CPU_number do 34 | group { 'CPU' } 35 | field { 'number' } 36 | end 37 | trait :CPU_max_number_of_cpu do 38 | group { 'CPU' } 39 | field { 'max_number_of_cpu' } 40 | end 41 | 42 | trait :MEM_max_mem do 43 | group { 'MEM' } 44 | field { 'max_mem' } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/factories/tier.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :tier, :class => ManageIQ::Showback::Tier do 3 | tier_start_value { 0 } 4 | tier_end_value { Float::INFINITY } 5 | fixed_rate { Money.new(rand(5..200), 'USD') } 6 | fixed_rate_per_time { 'monthly' } 7 | variable_rate { Money.new(rand(5..200), 'USD') } 8 | variable_rate_per_unit { 'cores' } 9 | variable_rate_per_time { 'monthly' } 10 | rate 11 | 12 | trait :daily do 13 | fixed_rate_per_time { 'daily' } 14 | variable_rate_per_time { 'daily' } 15 | end 16 | 17 | trait :with_rate_tests do 18 | fixed_rate { Money.new(11) } 19 | variable_rate { Money.new(7) } 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/helpers/time_converter_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | # Specs in this file have access to a helper object that includes 3 | # the UnitsConverterHelper. For example: 4 | # 5 | # describe ApplicationHelper do 6 | # describe "string concat" do 7 | # it "concats two strings with spaces" do 8 | # expect(helper.concat_strings("this","that")).to eq("this that") 9 | # end 10 | # end 11 | # end 12 | 13 | RSpec.describe ManageIQ::Showback::TimeConverterHelper, :type => :helper do 14 | let(:time_current) { Time.parse('Mon, 05 Nov 2018 18:39:38 UTC +00:00').utc } 15 | 16 | before do 17 | Timecop.travel(time_current) 18 | end 19 | 20 | after do 21 | Timecop.return 22 | end 23 | 24 | let(:constants) do 25 | [described_class::VALID_INTERVAL_UNITS] 26 | end 27 | context 'CONSTANTS' do 28 | it 'symbols should be constant' do 29 | constants.each do |x| 30 | expect(x).to be_frozen 31 | end 32 | end 33 | end 34 | 35 | context '#number of intervals on this month' do 36 | let(:time_values) { [0.seconds, 15.minutes, 45.minutes, 1.hour, 90.minutes, 5.hours, 1.day, 1.5.days, 1.week, 1.4.weeks, 1.month - 1.second] } 37 | 38 | it 'minutely' do 39 | interval = 'minutely' 40 | results = [1, 15, 45, 60, 90, 300, 24 * 60, 15 * 24 * 6, 7 * 24 * 60, 14 * 7 * 24 * 6, 24 * 60 * Time.days_in_month(Time.current.month)] 41 | expect(results.length).to eq(time_values.length) 42 | start_t = Time.current.beginning_of_month 43 | time_values.each_with_index do |x, y| 44 | end_t = start_t + x 45 | next unless start_t.month == end_t.month 46 | conversion = described_class.number_of_intervals( 47 | :period => end_t - start_t, 48 | :interval => interval 49 | ) 50 | expect(conversion) 51 | .to eq(results[y]), 52 | "Expected with #{interval} for #{x} s to match #{results[y]}, start: #{start_t}, end: #{end_t}, got #{conversion}" 53 | end 54 | end 55 | 56 | it 'hourly' do 57 | interval = 'hourly' 58 | results = [1, 1, 1, 1, 2, 5, 24, 36, 168, 236, 24 * Time.days_in_month(Time.current.month)] 59 | expect(results.length).to eq(time_values.length) 60 | start_t = Time.current.beginning_of_month 61 | time_values.each_with_index do |x, y| 62 | end_t = start_t + x 63 | next unless start_t.month == end_t.month 64 | conversion = described_class.number_of_intervals( 65 | :period => end_t - start_t, 66 | :interval => interval 67 | ) 68 | expect(conversion) 69 | .to eq(results[y]), 70 | "Expected with #{interval} for #{x} s to match #{results[y]}, start: #{start_t}, end: #{end_t}, got #{conversion}" 71 | end 72 | end 73 | 74 | it 'daily' do 75 | interval = 'daily' 76 | results = [1, 1, 1, 1, 1, 1, 1, 2, 7, 10, Time.days_in_month(Time.current.month)] 77 | expect(results.length).to eq(time_values.length) 78 | start_t = Time.current.beginning_of_month 79 | time_values.each_with_index do |x, y| 80 | end_t = start_t + x 81 | next unless start_t.month == end_t.month 82 | conversion = described_class.number_of_intervals( 83 | :period => end_t - start_t, 84 | :interval => interval 85 | ) 86 | expect(conversion) 87 | .to eq(results[y]), 88 | "Expected with #{interval} for #{x} s to match #{results[y]}, start: #{start_t}, end: #{end_t}, got #{conversion}" 89 | end 90 | end 91 | 92 | it 'weekly' do 93 | interval = 'weekly' 94 | results = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 5, 4] 95 | time_values.push(28.days) 96 | expect(results.length).to eq(time_values.length) 97 | start_t = Time.current.beginning_of_month 98 | time_values.each_with_index do |x, y| 99 | end_t = start_t + x 100 | next unless start_t.month == end_t.month 101 | conversion = described_class.number_of_intervals( 102 | :period => end_t - start_t, 103 | :interval => interval 104 | ) 105 | expect(conversion) 106 | .to eq(results[y]), 107 | "Expected with #{interval} for #{x} s to match #{results[y]}, start: #{start_t}, end: #{end_t}, got #{conversion}" 108 | end 109 | end 110 | 111 | it 'monthly' do 112 | interval = 'monthly' 113 | results = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 114 | expect(results.length).to eq(time_values.length) 115 | start_t = Time.current.beginning_of_month 116 | time_values.each_with_index do |x, y| 117 | end_t = start_t + x 118 | next unless start_t.month == end_t.month 119 | conversion = described_class.number_of_intervals( 120 | :period => end_t - start_t, 121 | :interval => interval 122 | ) 123 | expect(conversion) 124 | .to eq(results[y]), 125 | "Expected with #{interval} for #{x} s to match #{results[y]}, start: #{start_t}, end: #{end_t}, got #{conversion}" 126 | end 127 | end 128 | 129 | it 'yearly' do 130 | interval = 'yearly' 131 | results = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 132 | expect(results.length).to eq(time_values.length) 133 | start_t = Time.current.beginning_of_month 134 | time_values.each_with_index do |x, y| 135 | end_t = start_t + x 136 | next unless start_t.month == end_t.month 137 | conversion = described_class.number_of_intervals( 138 | :period => end_t - start_t, 139 | :interval => interval 140 | ) 141 | expect(conversion) 142 | .to eq(results[y]), 143 | "Expected with #{interval} for #{x} s to match #{results[y]}, start: #{start_t}, end: #{end_t}, got #{conversion}" 144 | end 145 | end 146 | end 147 | 148 | context 'calculating for a different month than current' do 149 | let(:time_values) { [0.seconds, 15.minutes, 45.minutes, 1.hour, 90.minutes, 5.hours, 1.day, 1.5.days, 1.week, 1.4.weeks, 28.days - 1.second] } 150 | 151 | it 'hourly' do 152 | time = Time.zone.local(2017, 2, 1, 0, 0, 1) 153 | interval = 'hourly' 154 | results = [1, 1, 1, 1, 2, 5, 24, 36, 168, 236, 28 * 24] 155 | expect(results.length).to eq(time_values.length) 156 | start_t = time.beginning_of_month 157 | time_values.each_with_index do |x, y| 158 | end_t = start_t + x 159 | next unless start_t.month == end_t.month 160 | conversion = described_class.number_of_intervals( 161 | :period => end_t - start_t, 162 | :interval => interval, 163 | :calculation_date => time 164 | ) 165 | expect(conversion) 166 | .to eq(results[y]), 167 | "Expected with #{interval} for #{x} s to match #{results[y]}, start: #{start_t}, end: #{end_t}, got #{conversion}" 168 | end 169 | end 170 | end 171 | 172 | context "calculating with given lenghts" do 173 | let(:time_values) { [0.seconds, 15.minutes, 45.minutes, 1.hour, 90.minutes, 5.hours, 1.day, 1.5.days, 1.week, 1.4.weeks, 28.days - 1.second] } 174 | 175 | it 'monthly' do 176 | interval = 'monthly' 177 | days_in_month = 7 # Just testing that it work with different numbers 178 | results = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 4] 179 | expect(results.length).to eq(time_values.length) 180 | start_t = Time.current.beginning_of_month 181 | time_values.each_with_index do |x, y| 182 | end_t = start_t + x 183 | next unless start_t.month == end_t.month 184 | conversion = described_class.number_of_intervals( 185 | :period => end_t - start_t, 186 | :interval => interval, 187 | :days_in_month => days_in_month 188 | ) 189 | expect(conversion) 190 | .to eq(results[y]), 191 | "Expected with #{interval} for #{x} s to match #{results[y]}, start: #{start_t}, end: #{end_t}, got #{conversion}" 192 | end 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /spec/helpers/units_converter_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | # Specs in this file have access to a helper object that includes 3 | # the UnitsConverterHelper. For example: 4 | # 5 | # describe ApplicationHelper do 6 | # describe "string concat" do 7 | # it "concats two strings with spaces" do 8 | # expect(helper.concat_strings("this","that")).to eq("this that") 9 | # end 10 | # end 11 | # end 12 | 13 | RSpec.describe ManageIQ::Showback::UnitsConverterHelper, :type => :helper do 14 | let(:constants) do 15 | [described_class::SYMBOLS, 16 | described_class::SI_PREFIX, 17 | described_class::BINARY_PREFIX, 18 | described_class::ALL_PREFIXES] 19 | end 20 | context 'CONSTANTS' do 21 | it 'symbols should be constant' do 22 | constants.each do |x| 23 | expect(x).to be_frozen 24 | end 25 | end 26 | end 27 | 28 | context '#extract_prefix' do 29 | it 'SI symbol prefixes should be extracted' do 30 | described_class::SYMBOLS.each do |sym| 31 | described_class::SI_PREFIX.each do |pf| 32 | expect(described_class.extract_prefix(pf[0].to_s + sym)).to eq(pf[0].to_s) 33 | end 34 | end 35 | end 36 | 37 | it 'BINARY symbol prefixes should be extracted' do 38 | described_class::SYMBOLS.each do |sym| 39 | described_class::BINARY_PREFIX.each do |pf| 40 | expect(described_class.extract_prefix(pf[0].to_s + sym)).to eq(pf[0].to_s) 41 | end 42 | end 43 | end 44 | 45 | it 'not found prefixes should return the full unit' do 46 | expect(described_class.extract_prefix('UNKNOWN')).to eq('UNKNOWN') 47 | end 48 | 49 | it 'nil unit returns empty string' do 50 | expect(described_class.extract_prefix(nil)).to eq('') 51 | end 52 | end 53 | 54 | context '#distance' do 55 | it 'SI symbol returns distance to base unit' do 56 | described_class::SI_PREFIX.each do |pf| 57 | expect(described_class.distance(pf[0].to_s)).to eq(pf[1][:value]) 58 | end 59 | end 60 | 61 | it 'BINARY symbol returns distance to base unit' do 62 | described_class::BINARY_PREFIX.each do |pf| 63 | expect(described_class.distance(pf[0].to_s, '', :BINARY_PREFIX)).to eq(pf[1][:value]) 64 | end 65 | end 66 | 67 | it 'ALL_PREFIXES (default) symbol returns distance to base unit' do 68 | described_class::ALL_PREFIXES.each do |pf| 69 | expect(described_class.distance(pf[0].to_s, '', :ALL_PREFIXES)).to eq(pf[1][:value]) 70 | expect(described_class.distance(pf[0].to_s, '')).to eq(pf[1][:value]) 71 | end 72 | end 73 | 74 | it 'returns nil if origin or destination are not found' do 75 | described_class::ALL_PREFIXES.each do |pf| 76 | expect(described_class.distance(pf[0].to_s, 'UNKNOWN')).to eq(nil) 77 | expect(described_class.distance('UNKNOWN', pf[0].to_s)).to eq(nil) 78 | end 79 | end 80 | 81 | it 'SI symbol returns distance between symbols' do 82 | origin = finish = ['', 'K', 'M'] 83 | units = described_class::SI_PREFIX 84 | origin.each do |x| 85 | finish.each do |y| 86 | expect(described_class.distance(x, y)) 87 | .to eq(units[x][:value].to_r / units[y][:value]) 88 | end 89 | end 90 | end 91 | 92 | it 'BINARY symbol returns distance between symbols' do 93 | origin = finish = ['', 'Ki', 'Mi'] 94 | units = described_class::BINARY_PREFIX 95 | origin.each do |x| 96 | finish.each do |y| 97 | expect(described_class.distance(x, y, 'BINARY_PREFIX')) 98 | .to eq(units[x][:value].to_r / units[y][:value]) 99 | end 100 | end 101 | end 102 | 103 | it 'Default symbols returns distance between symbols' do 104 | origin = ['', 'K', 'M'] 105 | finish = ['', 'Ki', 'Mi'] 106 | units = described_class::ALL_PREFIXES 107 | origin.each do |x| 108 | finish.each do |y| 109 | expect(described_class.distance(x, y)) 110 | .to eq(units[x][:value].to_r / units[y][:value]) 111 | end 112 | end 113 | end 114 | end 115 | 116 | context '#to_unit' do 117 | it 'SI symbol returns value in base unit' do 118 | expect(described_class.to_unit(7)) 119 | .to eq(7) 120 | expect(described_class.to_unit(7, 'KB')) 121 | .to eq(7000) 122 | end 123 | 124 | it 'BINARY symbol returns value in base unit' do 125 | expect(described_class.to_unit(7, 'KiB', '', 'BINARY_PREFIX')) 126 | .to eq(7168) 127 | end 128 | 129 | it 'SI symbol returns value in destination unit' do 130 | expect(described_class.to_unit(7, 'MB', 'KB')) 131 | .to eq(7000) 132 | end 133 | 134 | it 'BINARY symbol returns value in destination unit' do 135 | expect(described_class.to_unit(7, 'PiB', 'TiB', 'BINARY_PREFIX')) 136 | .to eq(7168) 137 | end 138 | 139 | it 'SI symbol returns value in destination unit' do 140 | expect(described_class.to_unit(7, 'PB', 'TiB')) 141 | .to eq(6366.462912410498) 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/helpers/utils_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | # Specs in this file have access to a helper object that includes 3 | # the UnitsConverterHelper. For example: 4 | # 5 | # describe ApplicationHelper do 6 | # describe "string concat" do 7 | # it "concats two strings with spaces" do 8 | # expect(helper.concat_strings("this","that")).to eq("this that") 9 | # end 10 | # end 11 | # end 12 | 13 | RSpec.describe ManageIQ::Showback::UtilsHelper, :type => :helper do 14 | let(:json_1) { JSON.parse '{ "tag": { "uno": 1, "dos": 2, "tres": 3, "cuatro": { "cinco": 5, "seis": 6} } }' } 15 | let(:json_2) { JSON.parse '{ "tag": { "cuatro": { "cinco": 5, "seis": 6 } } }' } 16 | let(:json_3) { JSON.parse '{ "cuatro": { "cinco": 5, "seis": 6, "siete": 7 } }' } 17 | let(:json_4) { JSON.parse '{ "siete": { "ocho": 8, "nueve": 9 } }' } 18 | 19 | context "#included_in?" do 20 | it "returns false if nil context or test" do 21 | expect(described_class.included_in?(nil, "")).to be false 22 | expect(described_class.included_in?("", nil)).to be false 23 | expect(described_class.included_in?(nil, nil)).to be false 24 | end 25 | 26 | it "context and test are independent" do 27 | expect(described_class.included_in?(json_1, json_4)).to be false 28 | end 29 | 30 | it "context includes the test fully" do 31 | expect(described_class.included_in?(json_1, json_2)).to be true 32 | end 33 | 34 | it "content includes half of the test" do 35 | expect(described_class.included_in?(json_1, json_3)).to be false 36 | end 37 | 38 | it "content is empty" do 39 | expect(described_class.included_in?("", json_3)).to be false 40 | end 41 | 42 | it "test is empty" do 43 | expect(described_class.included_in?(json_1, "")).to be true 44 | end 45 | 46 | it "contest and test are emtpy" do 47 | expect(described_class.included_in?(json_1, "")).to be true 48 | end 49 | end 50 | 51 | context "#get_parent" do 52 | it "Return parent of vm resource" do 53 | host = FactoryBot.create(:host) 54 | vm = FactoryBot.create(:vm, :host => host) 55 | expect(described_class.get_parent(vm)).to eq(host) 56 | end 57 | 58 | it "Should return nil if not parent vm" do 59 | vm = FactoryBot.create(:vm) 60 | expect(described_class.get_parent(vm)).to be_nil 61 | end 62 | 63 | it "Should return nil if not parent container" do 64 | cont = FactoryBot.create(:container) 65 | expect(described_class.get_parent(cont)).to be_nil 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/models/data_rollup/cpu_spec.rb: -------------------------------------------------------------------------------- 1 | describe ManageIQ::Showback::DataRollup::Cpu do 2 | let(:data_rollup) { FactoryBot.build(:data_rollup) } 3 | let(:con_data_rollup) { FactoryBot.build(:data_rollup) } 4 | context "CPU in vm" do 5 | before(:each) do 6 | @vm_metrics = FactoryBot.create(:vm, :hardware => FactoryBot.create(:hardware, :cpu1x2, :memory_mb => 4096)) 7 | cases = [ 8 | "2010-04-13T20:52:30Z", 100.0, 9 | "2010-04-13T21:51:10Z", 1.0, 10 | "2010-04-14T21:51:30Z", 2.0, 11 | "2010-04-14T22:51:50Z", 4.0, 12 | "2010-04-14T22:52:10Z", 8.0, 13 | "2010-04-14T22:52:30Z", 15.0, 14 | "2010-04-15T23:52:30Z", 100.0, 15 | ] 16 | cases.each_slice(2) do |t, v| 17 | @vm_metrics.metrics << FactoryBot.create(:metric_vm_rt, 18 | :timestamp => t, 19 | :cpu_usage_rate_average => v, 20 | # Multiply by a factor of 1000 to make it more realistic and enable testing virtual col v_pct_cpu_ready_delta_summation 21 | :cpu_ready_delta_summation => v * 1000, 22 | :sys_uptime_absolute_latest => v) 23 | end 24 | data_rollup.resource = @vm_metrics 25 | data_rollup.start_time = "2010-04-13T00:00:00Z" 26 | data_rollup.end_time = "2010-04-14T00:00:00Z" 27 | group = FactoryBot.build(:input_measure) 28 | group.save 29 | data_rollup.generate_data 30 | end 31 | 32 | it "Calculate CPU average" do 33 | data_rollup.instance_variable_set(:@metrics, data_rollup.resource.metrics) 34 | expect(data_rollup.CPU_average(50)).to be_within(0.0001).of(41.4286) 35 | end 36 | 37 | it "Calculate CPU average with no metrics" do 38 | data_rollup.instance_variable_set(:@metrics, []) 39 | expect(data_rollup.CPU_average(50)).to eq(50) 40 | end 41 | 42 | it "Calculate CPU_number" do 43 | expect(data_rollup.CPU_number(2)).to eq(2) 44 | end 45 | 46 | it "Calculate CPU_max_number_of_cpu" do 47 | expect(data_rollup.CPU_max_number_of_cpu(3)).to eq(3) 48 | end 49 | end 50 | 51 | context "CPU in container" do 52 | before(:each) do 53 | @con_metrics = FactoryBot.create(:container) 54 | data_rollup.resource = @con_metrics 55 | data_rollup.resource.type = "Container" 56 | data_rollup.start_time = "2010-04-13T00:00:00Z" 57 | data_rollup.end_time = "2010-04-14T00:00:00Z" 58 | Range.new(data_rollup.start_time, data_rollup.end_time, true).step_value(1.hour).each do |t| 59 | @con_metrics.vim_performance_states << FactoryBot.create(:vim_performance_state, 60 | :timestamp => t, 61 | :image_tag_names => "environment/prod", 62 | :state_data => {:numvcpus => 2}) 63 | end 64 | group = FactoryBot.build(:input_measure) 65 | group.save 66 | data_rollup.generate_data 67 | end 68 | 69 | it "Calculate CPU_max_number_of_cpu" do 70 | expect(data_rollup.CPU_max_number_of_cpu(3)).to eq(3) 71 | expect(data_rollup.CPU_max_number_of_cpu(1)).to eq(2) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/models/data_rollup/flavor_spec.rb: -------------------------------------------------------------------------------- 1 | describe ManageIQ::Showback::DataRollup::Flavor do 2 | let(:data_rollup) { FactoryBot.build(:data_rollup) } 3 | context "FLAVOR in vm" do 4 | before(:each) do 5 | ManageIQ::Showback::InputMeasure.seed 6 | @vm_metrics = FactoryBot.create(:vm, :hardware => FactoryBot.create(:hardware, :cpu1x2, :memory_mb => 4096)) 7 | cases = [ 8 | "2010-04-13T20:52:30Z", 100.0, 9 | "2010-04-13T21:51:10Z", 1.0, 10 | "2010-04-14T21:51:30Z", 2.0, 11 | "2010-04-14T22:51:50Z", 4.0, 12 | "2010-04-14T22:52:10Z", 8.0, 13 | "2010-04-14T22:52:30Z", 15.0, 14 | "2010-04-15T23:52:30Z", 100.0, 15 | ] 16 | cases.each_slice(2) do |t, v| 17 | @vm_metrics.metrics << FactoryBot.create(:metric_vm_rt, 18 | :timestamp => t, 19 | :cpu_usage_rate_average => v, 20 | # Multiply by a factor of 1000 to make it more realistic and enable testing virtual col v_pct_cpu_ready_delta_summation 21 | :cpu_ready_delta_summation => v * 1000, 22 | :sys_uptime_absolute_latest => v) 23 | end 24 | data_rollup.resource = @vm_metrics 25 | data_rollup.start_time = "2010-04-13T00:00:00Z" 26 | data_rollup.end_time = "2010-04-14T00:00:00Z" 27 | group = FactoryBot.build(:input_measure) 28 | group.save 29 | data_rollup.generate_data 30 | end 31 | 32 | it "Calculate FLAVOR_number" do 33 | data_rollup.FLAVOR_cpu_reserved 34 | expect(data_rollup.data["FLAVOR"]).not_to be_empty 35 | expect(data_rollup.data["FLAVOR"].length).to eq(1) 36 | expect(data_rollup.data["FLAVOR"].values.first["cores"]).to eq([2, "cores"]) 37 | data_rollup.resource.hardware = FactoryBot.create(:hardware, :cpu4x2, :memory_mb => 8192) 38 | data_rollup.FLAVOR_cpu_reserved 39 | expect(data_rollup.data["FLAVOR"].length).to eq(2) 40 | expect(data_rollup.data["FLAVOR"].values.first["cores"]).not_to eq(data_rollup.data["FLAVOR"].values.last["cores"]) 41 | expect(data_rollup.data["FLAVOR"].values.last["cores"]).to eq([8, "cores"]) 42 | end 43 | 44 | it "Calculate FLAVOR_memory_reserve" do 45 | data_rollup.FLAVOR_memory_reserved 46 | expect(data_rollup.data["FLAVOR"]).not_to be_empty 47 | expect(data_rollup.data["FLAVOR"].length).to eq(1) 48 | expect(data_rollup.data["FLAVOR"].values.first["memory"]).to eq([4096, "Mb"]) 49 | data_rollup.resource.hardware = FactoryBot.create(:hardware, :cpu4x2, :memory_mb => 8192) 50 | data_rollup.FLAVOR_memory_reserved 51 | expect(data_rollup.data["FLAVOR"].length).to eq(2) 52 | expect(data_rollup.data["FLAVOR"].values.first["memory"]).not_to eq(data_rollup.data["FLAVOR"].values.last["memory"]) 53 | expect(data_rollup.data["FLAVOR"].values.last["memory"]).to eq([8192, "Mb"]) 54 | end 55 | 56 | it "Calculate FLAVOR_memory_reserve and number" do 57 | data_rollup.FLAVOR_memory_reserved 58 | expect(data_rollup.data["FLAVOR"]).not_to be_empty 59 | expect(data_rollup.data["FLAVOR"].length).to eq(1) 60 | expect(data_rollup.data["FLAVOR"].values.first["memory"]).to eq([4096, "Mb"]) 61 | data_rollup.resource.hardware = FactoryBot.create(:hardware, :cpu4x2, :memory_mb => 8192) 62 | data_rollup.FLAVOR_memory_reserved 63 | expect(data_rollup.data["FLAVOR"].length).to eq(2) 64 | expect(data_rollup.data["FLAVOR"].values.first["memory"]).not_to eq(data_rollup.data["FLAVOR"].values.last["memory"]) 65 | expect(data_rollup.data["FLAVOR"].values.last["memory"]).to eq([8192, "Mb"]) 66 | end 67 | end 68 | 69 | context "FLAVOR methods" do 70 | it "update_value_flavor" do 71 | data_rollup.send(:update_value_flavor, "cores", "2") 72 | data_rollup.send(:update_value_flavor, "memory", "2048") 73 | expect(data_rollup.data["FLAVOR"].keys.length).to eq(1) 74 | expect(data_rollup.data["FLAVOR"].first.second.values.length).to eq(2) 75 | end 76 | end 77 | 78 | context "FLAVOR in container" do 79 | before(:each) do 80 | ManageIQ::Showback::InputMeasure.seed 81 | @con_metrics = FactoryBot.create(:container) 82 | data_rollup.resource = @con_metrics 83 | data_rollup.resource.type = "Container" 84 | data_rollup.start_time = "2010-04-13T00:00:00Z" 85 | data_rollup.end_time = "2010-04-14T00:00:00Z" 86 | Range.new(data_rollup.start_time, data_rollup.end_time, true).step_value(1.hour).each do |t| 87 | @con_metrics.vim_performance_states << FactoryBot.create(:vim_performance_state, 88 | :timestamp => t, 89 | :image_tag_names => "environment/prod", 90 | :state_data => {:numvcpus => 2, :total_mem => 4096}) 91 | end 92 | group = FactoryBot.build(:input_measure) 93 | group.save 94 | data_rollup.generate_data 95 | end 96 | 97 | it "Calculate FLAVOR_number" do 98 | data_rollup.FLAVOR_cpu_reserved 99 | expect(data_rollup.data["FLAVOR"]).not_to be_empty 100 | expect(data_rollup.data["FLAVOR"].length).to eq(1) 101 | expect(data_rollup.data["FLAVOR"].values.first["cores"]).to eq([2, "cores"]) 102 | data_rollup.resource.vim_performance_states << FactoryBot.create(:vim_performance_state, 103 | :timestamp => "2016-04-13T00:00:00Z", 104 | :image_tag_names => "environment/prod", 105 | :state_data => {:numvcpus => 8, :total_mem => 4096}) 106 | data_rollup.FLAVOR_cpu_reserved 107 | expect(data_rollup.data["FLAVOR"].length).to eq(2) 108 | expect(data_rollup.data["FLAVOR"].values.first["cores"]).not_to eq(data_rollup.data["FLAVOR"].values.last["cores"]) 109 | expect(data_rollup.data["FLAVOR"].values.last["cores"]).to eq([8, "cores"]) 110 | end 111 | 112 | it "Calculate FLAVOR_memory_reserve" do 113 | data_rollup.FLAVOR_memory_reserved 114 | expect(data_rollup.data["FLAVOR"]).not_to be_empty 115 | expect(data_rollup.data["FLAVOR"].length).to eq(1) 116 | expect(data_rollup.data["FLAVOR"].values.first["memory"]).to eq([4096, "Mb"]) 117 | data_rollup.resource.vim_performance_states << FactoryBot.create(:vim_performance_state, 118 | :timestamp => "2016-04-13T00:00:00Z", 119 | :image_tag_names => "environment/prod", 120 | :state_data => {:numvcpus => 8, :total_mem => 8192}) 121 | data_rollup.FLAVOR_memory_reserved 122 | expect(data_rollup.data["FLAVOR"].length).to eq(2) 123 | expect(data_rollup.data["FLAVOR"].values.first["memory"]).not_to eq(data_rollup.data["FLAVOR"].values.last["memory"]) 124 | expect(data_rollup.data["FLAVOR"].values.last["memory"]).to eq([8192, "Mb"]) 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /spec/models/data_rollup/mem_spec.rb: -------------------------------------------------------------------------------- 1 | describe ManageIQ::Showback::DataRollup::Mem do 2 | let(:data_rollup) { FactoryBot.build(:data_rollup) } 3 | let(:con_data_rollup) { FactoryBot.build(:data_rollup) } 4 | context "memory in vm" do 5 | before(:each) do 6 | @vm_metrics = FactoryBot.create(:vm, :hardware => FactoryBot.create(:hardware, :cpu1x2, :memory_mb => 4096)) 7 | @vm_metrics.memory_reserve = 1024 8 | cases = [ 9 | "2010-04-13T20:52:30Z", 100.0, 10 | "2010-04-13T21:51:10Z", 1.0, 11 | "2010-04-14T21:51:30Z", 2.0, 12 | "2010-04-14T22:51:50Z", 4.0, 13 | "2010-04-14T22:52:10Z", 8.0, 14 | "2010-04-14T22:52:30Z", 15.0, 15 | "2010-04-15T23:52:30Z", 100.0, 16 | ] 17 | cases.each_slice(2) do |t, v| 18 | @vm_metrics.metrics << FactoryBot.create( 19 | :metric_vm_rt, 20 | :timestamp => t, 21 | :cpu_usage_rate_average => v, 22 | # Multiply by a factor of 1000 to make it more realistic and enable testing virtual col v_pct_cpu_ready_delta_summation 23 | :cpu_ready_delta_summation => v * 1000, 24 | :sys_uptime_absolute_latest => v 25 | ) 26 | end 27 | data_rollup.resource = @vm_metrics 28 | data_rollup.start_time = "2010-04-13T00:00:00Z" 29 | data_rollup.end_time = "2010-04-14T00:00:00Z" 30 | group = FactoryBot.build(:input_measure) 31 | group.save 32 | data_rollup.generate_data 33 | end 34 | 35 | it "Calculate MEM_total_mem" do 36 | expect(data_rollup.MEM_max_mem(1024)).to eq(4096) 37 | expect(data_rollup.MEM_max_mem(4096)).to eq(4096) 38 | end 39 | end 40 | 41 | context "memory in container" do 42 | before(:each) do 43 | @con_metrics = FactoryBot.create(:container) 44 | data_rollup.resource = @con_metrics 45 | data_rollup.resource.type = "Container" 46 | data_rollup.start_time = "2010-04-13T00:00:00Z" 47 | data_rollup.end_time = "2010-04-14T00:00:00Z" 48 | Range.new(data_rollup.start_time, data_rollup.end_time, true).step_value(1.hour).each do |t| 49 | @con_metrics.vim_performance_states << FactoryBot.create(:vim_performance_state, 50 | :timestamp => t, 51 | :image_tag_names => "environment/prod", 52 | :state_data => {:total_mem => 1024}) 53 | end 54 | group = FactoryBot.build(:input_measure) 55 | group.save 56 | data_rollup.generate_data 57 | end 58 | 59 | it "Calculate MEM_total_mem" do 60 | expect(data_rollup.MEM_max_mem(1024)).to eq(1024) 61 | expect(data_rollup.MEM_max_mem(0)).to eq(1024) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/models/data_view_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'money-rails/test_helpers' 3 | 4 | RSpec.describe ManageIQ::Showback::DataView, :type => :model do 5 | before(:each) do 6 | ManageIQ::Showback::InputMeasure.seed 7 | end 8 | 9 | context 'basic life cycle' do 10 | let(:data_view) { FactoryBot.build(:data_view) } 11 | let(:cost) { Money.new(1) } 12 | 13 | it 'has a valid factory' do 14 | expect(data_view).to be_valid 15 | end 16 | 17 | it 'monetizes cost' do 18 | expect(described_class).to monetize(:cost) 19 | expect(data_view).to monetize(:cost) 20 | end 21 | 22 | it 'cost defaults to 0' do 23 | expect(described_class.new.cost).to eq(Money.new(0)) 24 | end 25 | 26 | it 'you can add a data_view without cost' do 27 | data_view.cost = nil 28 | data_view.valid? 29 | expect(data_view).to be_valid 30 | end 31 | 32 | it 'you can add a data_view with cost' do 33 | data_view.cost = cost 34 | data_view.valid? 35 | expect(data_view).to be_valid 36 | end 37 | 38 | it 'you can read data_views' do 39 | data_view.cost = cost 40 | data_view.save 41 | expect(data_view.reload.cost).to eq(cost) 42 | end 43 | 44 | it 'can delete cost' do 45 | data_view.cost = Money.new(10) 46 | data_view.save 47 | data_view.clean_cost 48 | data_view.reload 49 | expect(data_view.cost).to eq(Money.new(0)) # default is 0 50 | end 51 | end 52 | 53 | context '#validate price_plan_missing and snapshot' do 54 | let(:event) do 55 | FactoryBot.build(:data_rollup, 56 | :with_vm_data, 57 | :full_month) 58 | end 59 | 60 | let(:data_view) do 61 | FactoryBot.build(:data_view, 62 | :data_rollup => event) 63 | end 64 | 65 | it "fails if can't find a price plan" do 66 | event.save 67 | event.reload 68 | data_view.save 69 | expect(ManageIQ::Showback::PricePlan.count).to eq(0) 70 | expect(data_view.calculate_cost).to eq(Money.new(0)) 71 | end 72 | 73 | it "fails if snapshot of data_view is not the event data after create" do 74 | event.save 75 | data_view.save 76 | expect(data_view.data_snapshot.first[1]).to eq(event.data) 77 | event.data = {"CPU" => {"average" => [2, "percent"], "max_number_of_cpu" => [40, "cores"]}} 78 | event.save 79 | data_view.save 80 | expect(data_view.data_snapshot.first[1]).not_to eq(event.data) 81 | end 82 | 83 | it "Return the stored data at start" do 84 | event.save 85 | data_view.save 86 | expect(data_view.data_snapshot_start).to eq(event.data) 87 | end 88 | 89 | it "Return the last stored data" do 90 | event.save 91 | data_view.save 92 | expect(data_view.data_snapshot.length).to eq(1) 93 | event.data = {"CPU" => {"average" => [2, "percent"], "max_number_of_cpu" => [40, "cores"]}} 94 | data_view.update_data_snapshot 95 | expect(data_view.data_snapshot_last).to eq(event.data) 96 | end 97 | 98 | it "Return the last stored data key" do 99 | event.save 100 | data_view.data_snapshot = { 3.hours.ago => {"CPU" => {"average" => [2, "percent"], "max_number_of_cpu" => [40, "cores"]}}, 101 | Time.now.utc => {"CPU" => {"average" => [2, "percent"], "max_number_of_cpu" => [40, "cores"]}}} 102 | t = data_view.data_snapshot.keys.sort.last 103 | expect(data_view.data_snapshot_last_key).to eq(t) 104 | end 105 | end 106 | 107 | context '#stored data' do 108 | let(:data_view_data) { FactoryBot.build(:data_view, :with_data_snapshot) } 109 | let(:event_for_data_view) { FactoryBot.create(:data_rollup) } 110 | let(:envelope_of_event) do 111 | FactoryBot.create(:envelope, 112 | :resource => event_for_data_view.resource) 113 | end 114 | 115 | it "stored event" do 116 | event_for_data_view.data = { 117 | "CPU" => { 118 | "average" => [29.8571428571429, "percent"], 119 | "number" => [2.0, "cores"], 120 | "max_number_of_cpu" => [2, "cores"] 121 | }, 122 | "MEM" => { 123 | "max_mem" => [2048, "Mib"] 124 | }, 125 | "FLAVOR" => {} 126 | } 127 | data_view1 = FactoryBot.create(:data_view, 128 | :envelope => envelope_of_event, 129 | :data_rollup => event_for_data_view) 130 | expect(data_view1.data_snapshot_start).to eq(event_for_data_view.data) 131 | data_view1.snapshot_data_rollup 132 | expect(data_view1.data_snapshot_start).to eq(event_for_data_view.data) 133 | end 134 | 135 | it "get group" do 136 | expect(data_view_data.get_group("CPU", "number")).to eq([2.0, "cores"]) 137 | end 138 | 139 | it "get last group" do 140 | expect(data_view_data.get_last_group("CPU", "number")).to eq([4.0, "cores"]) 141 | end 142 | 143 | it "get envelope group" do 144 | expect(data_view_data.get_envelope_group("CPU", "number")).to eq([[2.0, "cores"], [4.0, "cores"]]) 145 | end 146 | end 147 | context '#calculate_cost' do 148 | let(:cost) { Money.new(32) } 149 | let(:envelope) { FactoryBot.create(:envelope) } 150 | let!(:plan) { FactoryBot.create(:price_plan) } # By default is :enterprise 151 | let(:plan2) { FactoryBot.create(:price_plan) } 152 | let(:fixed_rate1) { Money.new(3) } 153 | let(:fixed_rate2) { Money.new(5) } 154 | let(:variable_rate1) { Money.new(7) } 155 | let(:variable_rate2) { Money.new(7) } 156 | let(:rate1) do 157 | FactoryBot.create(:rate, 158 | :CPU_average, 159 | :price_plan => plan) 160 | end 161 | let(:tier1) { rate1.tiers.first } 162 | let(:rate2) do 163 | FactoryBot.create(:rate, 164 | :CPU_average, 165 | :price_plan => plan2) 166 | end 167 | let(:tier2) { rate2.tiers.first } 168 | let(:event) do 169 | FactoryBot.create(:data_rollup, 170 | :with_vm_data, 171 | :full_month) 172 | end 173 | 174 | let(:data_view) do 175 | FactoryBot.create(:data_view, 176 | :envelope => envelope, 177 | :cost => cost, 178 | :data_rollup => event) 179 | end 180 | 181 | context 'without price_plan' do 182 | it 'calculates cost using default price plan' do 183 | rate1 184 | event.reload 185 | data_view.save 186 | tier1 187 | tier1.fixed_rate = fixed_rate1 188 | tier1.variable_rate = variable_rate1 189 | tier1.variable_rate_per_unit = "percent" 190 | tier1.save 191 | expect(event.data).not_to be_nil # making sure that the default is not empty 192 | expect(ManageIQ::Showback::PricePlan.count).to eq(1) 193 | expect(data_view.data_rollup).to eq(event) 194 | expect(data_view.calculate_cost).to eq(fixed_rate1 + variable_rate1 * event.data['CPU']['average'].first) 195 | end 196 | end 197 | context 'with price_plan' do 198 | it 'calculates cost using price plan' do 199 | rate1.reload 200 | rate2.reload 201 | event.reload 202 | data_view.save 203 | tier1 204 | tier1.fixed_rate = fixed_rate1 205 | tier1.variable_rate = variable_rate1 206 | tier1.variable_rate_per_unit = "percent" 207 | tier1.save 208 | tier2 209 | tier2.fixed_rate = fixed_rate2 210 | tier2.variable_rate = variable_rate2 211 | tier2.variable_rate_per_unit = "percent" 212 | tier2.save 213 | expect(event.data).not_to be_nil 214 | plan2.reload 215 | expect(ManageIQ::Showback::PricePlan.count).to eq(2) 216 | expect(data_view.data_rollup).to eq(event) 217 | # Test that it works without a plan 218 | expect(data_view.calculate_cost).to eq(fixed_rate1 + variable_rate1 * event.get_group_value('CPU', 'average')) 219 | # Test that it changes if you provide a plan 220 | expect(data_view.calculate_cost(plan2)).to eq(fixed_rate2 + variable_rate2 * event.get_group_value('CPU', 'average')) 221 | end 222 | 223 | it 'raises an error if the plan provider is not working' do 224 | rate1 225 | rate2 226 | event.reload 227 | data_view.save 228 | tier1 229 | tier1.fixed_rate = fixed_rate1 230 | tier1.variable_rate = variable_rate1 231 | tier1.variable_rate_per_unit = "percent" 232 | tier1.save 233 | tier2 234 | tier2.fixed_rate = fixed_rate2 235 | tier2.variable_rate = variable_rate2 236 | tier2.variable_rate_per_unit = "percent" 237 | tier2.save 238 | expect(event.data).not_to be_nil 239 | expect(ManageIQ::Showback::PricePlan.count).to eq(2) 240 | expect(data_view.data_rollup).to eq(event) 241 | # Test that it works without a plan 242 | expect(data_view.calculate_cost).to eq(fixed_rate1 + variable_rate1 * event.get_group_value('CPU', 'average')) 243 | # Test that it changes if you provide a plan 244 | expect(data_view.calculate_cost('ERROR')).to eq(Money.new(0)) 245 | expect(data_view.errors.details[:price_plan]).to include(:error => 'not found') 246 | end 247 | end 248 | end 249 | end 250 | -------------------------------------------------------------------------------- /spec/models/envelope_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'money-rails/test_helpers' 3 | 4 | RSpec.describe ManageIQ::Showback::Envelope, :type => :model do 5 | before(:each) do 6 | ManageIQ::Showback::InputMeasure.seed 7 | end 8 | let(:resource) { FactoryBot.create(:vm) } 9 | let(:envelope) { FactoryBot.build(:envelope) } 10 | let(:data_rollup) { FactoryBot.build(:data_rollup, :with_vm_data, :full_month, :resource => resource) } 11 | let(:data_rollup2) { FactoryBot.build(:data_rollup, :with_vm_data, :full_month, :resource => resource) } 12 | let(:enterprise_plan) { FactoryBot.create(:price_plan) } 13 | 14 | context '#basic lifecycle' do 15 | it 'has a valid factory' do 16 | envelope.valid? 17 | expect(envelope).to be_valid 18 | end 19 | 20 | it 'is not valid without an association to a parent element' do 21 | envelope.resource = nil 22 | envelope.valid? 23 | expect(envelope.errors.details[:resource]). to include(:error => :blank) 24 | end 25 | 26 | it 'is not valid without a name' do 27 | envelope.name = nil 28 | envelope.valid? 29 | expect(envelope.errors.details[:name]). to include(:error => :blank) 30 | end 31 | 32 | it 'is not valid without a description' do 33 | envelope.description = nil 34 | envelope.valid? 35 | expect(envelope.errors.details[:description]). to include(:error => :blank) 36 | end 37 | 38 | it 'monetizes accumulated cost' do 39 | expect(ManageIQ::Showback::Envelope).to monetize(:accumulated_cost) 40 | end 41 | 42 | it 'deletes costs associated when deleting the envelope' do 43 | 2.times do 44 | FactoryBot.create(:data_view, :envelope => envelope) 45 | end 46 | expect(envelope.data_views.count).to be(2) 47 | expect { envelope.destroy }.to change(ManageIQ::Showback::DataView, :count).from(2).to(0) 48 | expect(envelope.data_views.count).to be(0) 49 | end 50 | 51 | it 'deletes costs associated when deleting the data_rollup' do 52 | 2.times do 53 | FactoryBot.create(:data_view, :envelope => envelope) 54 | end 55 | expect(envelope.data_views.count).to be(2) 56 | d_rollup = envelope.data_views.first.data_rollup 57 | expect { d_rollup.destroy }.to change(ManageIQ::Showback::DataView, :count).from(2).to(1) 58 | expect(envelope.data_rollups).not_to include(data_rollup) 59 | end 60 | 61 | it 'it only can be in approved states' do 62 | envelope.state = 'ERROR' 63 | expect(envelope).not_to be_valid 64 | expect(envelope.errors.details[:state]).to include(:error => :inclusion, :value => 'ERROR') 65 | end 66 | 67 | it 'it can not be different of states open, processing, closed' do 68 | states = %w(CLOSED PROCESSING OPEN) 69 | states.each do |x| 70 | envelope.state = x 71 | expect(envelope).to be_valid 72 | end 73 | end 74 | 75 | it 'start time should happen earlier than end time' do 76 | envelope.start_time = envelope.end_time 77 | envelope.valid? 78 | expect(envelope.errors.details[:end_time]).to include(:error => 'should happen after start_time') 79 | end 80 | end 81 | 82 | context '.control lifecycle state' do 83 | let(:envelope_lifecycle) { FactoryBot.create(:envelope) } 84 | 85 | it 'it can transition from open to processing' do 86 | envelope_lifecycle.state = 'PROCESSING' 87 | envelope_lifecycle.valid? 88 | expect(envelope).to be_valid 89 | end 90 | 91 | it 'a new envelope is created automatically when transitioning from open to processing if not exists' do 92 | envelope_lifecycle.state = 'PROCESSING' 93 | envelope_lifecycle.save 94 | # There should be two envelopes when I save, the one in processing state + the one in OPEN state 95 | expect(described_class.count).to eq(2) 96 | # ERROR ERROR ERROR 97 | end 98 | 99 | it 'it can not transition from open to closed' do 100 | envelope_lifecycle.state = 'CLOSED' 101 | expect { envelope_lifecycle.save }.to raise_error(RuntimeError, _("Envelope can't change state to CLOSED from OPEN")) 102 | end 103 | 104 | it 'it can not transition from processing to open' do 105 | envelope_lifecycle = FactoryBot.create(:envelope, :processing) 106 | envelope_lifecycle.state = 'OPEN' 107 | expect { envelope_lifecycle.save }.to raise_error(RuntimeError, _("Envelope can't change state to OPEN from PROCESSING")) 108 | end 109 | 110 | it 'it can transition from processing to closed' do 111 | envelope_lifecycle = FactoryBot.create(:envelope, :processing) 112 | envelope_lifecycle.state = 'CLOSED' 113 | expect { envelope_lifecycle.save }.not_to raise_error 114 | end 115 | 116 | it 'it can not transition from closed to open or processing' do 117 | envelope_lifecycle = FactoryBot.create(:envelope, :closed) 118 | envelope_lifecycle.state = 'OPEN' 119 | expect { envelope_lifecycle.save }.to raise_error(RuntimeError, _("Envelope can't change state when it's CLOSED")) 120 | envelope_lifecycle = FactoryBot.create(:envelope, :closed) 121 | envelope_lifecycle.state = 'PROCESSING' 122 | expect { envelope_lifecycle.save }.to raise_error(RuntimeError, _("Envelope can't change state when it's CLOSED")) 123 | end 124 | 125 | pending 'it can not exists 2 envelopes opened from one resource' 126 | end 127 | 128 | describe 'methods for data_rollups' do 129 | it 'can add an data_rollup to a envelope' do 130 | expect { envelope.add_data_rollup(data_rollup) }.to change(envelope.data_rollups, :count).by(1) 131 | expect(envelope.data_rollups).to include(data_rollup) 132 | end 133 | 134 | it 'throws an error for duplicate data_rollups when using Add data_rollup to a Envelope' do 135 | envelope.add_data_rollup(data_rollup) 136 | envelope.add_data_rollup(data_rollup) 137 | expect(envelope.errors.details[:data_rollups]). to include(:error => "duplicate") 138 | end 139 | 140 | it 'Throw error in add data_rollup if it is not of a proper type' do 141 | obj = FactoryBot.create(:vm) 142 | envelope.add_data_rollup(obj) 143 | expect(envelope.errors.details[:data_rollups]). to include(:error => "Error Type #{obj.type} is not ManageIQ::Showback::DataRollup") 144 | end 145 | 146 | it 'Remove data_rollup from a Envelope' do 147 | envelope.add_data_rollup(data_rollup) 148 | expect { envelope.remove_data_rollup(data_rollup) }.to change(envelope.data_rollups, :count).by(-1) 149 | expect(envelope.data_rollups).not_to include(data_rollup) 150 | end 151 | 152 | it 'Throw error in Remove data_rollup from a Envelope if the data_rollup can not be found' do 153 | envelope.add_data_rollup(data_rollup) 154 | envelope.remove_data_rollup(data_rollup) 155 | envelope.remove_data_rollup(data_rollup) 156 | expect(envelope.errors.details[:data_rollups]). to include(:error => "not found") 157 | end 158 | 159 | it 'Throw error in Remove data_rollup if the type is not correct' do 160 | obj = FactoryBot.create(:vm) 161 | envelope.remove_data_rollup(obj) 162 | expect(envelope.errors.details[:data_rollups]). to include(:error => "Error Type #{obj.type} is not ManageIQ::Showback::DataRollup") 163 | end 164 | end 165 | 166 | describe 'methods with #data_view' do 167 | it 'add data_view directly' do 168 | data_view = FactoryBot.create(:data_view, :envelope => envelope) 169 | envelope.add_data_view(data_view, 2) 170 | expect(data_view.cost). to eq(Money.new(2)) 171 | end 172 | 173 | it 'add data_view directly' do 174 | data_view = FactoryBot.create(:data_view, :cost => Money.new(7)) # different envelope 175 | envelope.add_data_view(data_view, 2) 176 | # data_view won't be updated as it does not belongs to the envelope 177 | expect(data_view.cost).not_to eq(Money.new(2)) 178 | expect(data_view.envelope).not_to eq(envelope) 179 | end 180 | 181 | it 'add data_view from an data_rollup' do 182 | data_rollup = FactoryBot.create(:data_rollup) 183 | data_view = FactoryBot.create(:data_view, :data_rollup => data_rollup, :envelope => envelope) 184 | expect(data_rollup.data_views).to include(data_view) 185 | expect(envelope.data_views).to include(data_view) 186 | end 187 | 188 | it 'get_data_view from a data_view' do 189 | data_view = FactoryBot.create(:data_view, :envelope => envelope, :cost => Money.new(10)) 190 | expect(envelope.get_data_view(data_view)).to eq(Money.new(10)) 191 | end 192 | 193 | it 'get_data_view from an data_rollup' do 194 | data_view = FactoryBot.create(:data_view, :envelope => envelope, :cost => Money.new(10)) 195 | data_rollup = data_view.data_rollup 196 | expect(envelope.get_data_view(data_rollup)).to eq(Money.new(10)) 197 | end 198 | 199 | it 'get_data_view from nil get 0' do 200 | expect(envelope.get_data_view(nil)).to eq(0) 201 | end 202 | 203 | it 'calculate_data_view with an error' do 204 | data_view = FactoryBot.create(:data_view, :cost => Money.new(10)) 205 | envelope.calculate_data_view(data_view) 206 | expect(data_view.errors.details[:data_view]). to include(:error => 'not found') 207 | expect(envelope.calculate_data_view(data_view)). to eq(Money.new(0)) 208 | end 209 | 210 | it 'calculate_data_view fails with no data_view' do 211 | enterprise_plan 212 | expect(envelope.find_price_plan).to eq(ManageIQ::Showback::PricePlan.first) 213 | envelope.calculate_data_view(nil) 214 | expect(envelope.errors.details[:data_view]). to include(:error => "not found") 215 | expect(envelope.calculate_data_view(nil)). to eq(0) 216 | end 217 | 218 | it 'find a price plan' do 219 | ManageIQ::Showback::PricePlan.seed 220 | expect(envelope.find_price_plan).to eq(ManageIQ::Showback::PricePlan.first) 221 | end 222 | 223 | pending 'find a price plan associated to the resource' 224 | pending 'find a price plan associated to a parent resource' 225 | pending 'find a price plan finds the default price plan if not found' 226 | 227 | it '#calculate data_view' do 228 | enterprise_plan 229 | sh = FactoryBot.create(:rate, 230 | :CPU_average, 231 | :price_plan => ManageIQ::Showback::PricePlan.first) 232 | st = sh.tiers.first 233 | st.fixed_rate = Money.new(67) 234 | st.variable_rate = Money.new(12) 235 | st.variable_rate_per_unit = 'percent' 236 | st.save 237 | envelope.add_data_rollup(data_rollup2) 238 | data_rollup2.reload 239 | envelope.data_views.reload 240 | data_view = envelope.data_views.find_by(:data_rollup => data_rollup2) 241 | data_view.cost = Money.new(0) 242 | data_view.save 243 | expect { envelope.calculate_data_view(data_view) }.to change(data_view, :cost) 244 | .from(Money.new(0)).to(Money.new((data_rollup2.reload.get_group_value('CPU', 'average') * 12) + 67)) 245 | end 246 | 247 | it '#Add an data_rollup' do 248 | data_rollup = FactoryBot.create(:data_rollup) 249 | expect { envelope.add_data_view(data_rollup, 5) }.to change(envelope.data_views, :count).by(1) 250 | end 251 | 252 | it 'update a data_view in the envelope with add_data_view' do 253 | data_view = FactoryBot.create(:data_view, :envelope => envelope) 254 | expect { envelope.add_data_view(data_view, 5) }.to change(data_view, :cost).to(Money.new(5)) 255 | end 256 | 257 | it 'update a data_view in the envelope with update_data_view' do 258 | data_view = FactoryBot.create(:data_view, :envelope => envelope) 259 | expect { envelope.update_data_view(data_view, 5) }.to change(data_view, :cost).to(Money.new(5)) 260 | end 261 | 262 | it 'update a data_view in the envelope gets nil if the data_view is not there' do 263 | data_view = FactoryBot.create(:data_view) # not in the envelope 264 | expect(envelope.update_data_view(data_view, 5)).to be_nil 265 | end 266 | 267 | it '#clear_data_view' do 268 | envelope.add_data_rollup(data_rollup) 269 | envelope.data_views.reload 270 | data_view = envelope.data_views.find_by(:data_rollup => data_rollup) 271 | data_view.cost = Money.new(5) 272 | expect { envelope.clear_data_view(data_view) }.to change(data_view, :cost).from(Money.new(5)).to(Money.new(0)) 273 | end 274 | 275 | it '#clear all data_views' do 276 | envelope.add_data_view(data_rollup, Money.new(57)) 277 | envelope.add_data_view(data_rollup2, Money.new(123)) 278 | envelope.clean_all_data_views 279 | envelope.data_views.each do |x| 280 | expect(x.cost).to eq(Money.new(0)) 281 | end 282 | end 283 | 284 | it '#sum_of_data_views' do 285 | envelope.add_data_view(data_rollup, Money.new(57)) 286 | envelope.add_data_view(data_rollup2, Money.new(123)) 287 | expect(envelope.sum_of_data_views).to eq(Money.new(180)) 288 | end 289 | 290 | it 'calculate_all_data_views' do 291 | enterprise_plan 292 | vm = FactoryBot.create(:vm) 293 | sh = FactoryBot.create(:rate, 294 | :CPU_average, 295 | :price_plan => ManageIQ::Showback::PricePlan.first) 296 | tier = sh.tiers.first 297 | tier.fixed_rate = Money.new(67) 298 | tier.variable_rate = Money.new(12) 299 | tier.save 300 | ev = FactoryBot.create(:data_rollup, :with_vm_data, :full_month, :resource => vm) 301 | ev2 = FactoryBot.create(:data_rollup, :with_vm_data, :full_month, :resource => vm) 302 | envelope.add_data_rollup(ev) 303 | envelope.add_data_rollup(ev2) 304 | envelope.data_views.reload 305 | envelope.data_views.each do |x| 306 | expect(x.cost).to eq(Money.new(0)) 307 | end 308 | envelope.data_views.reload 309 | envelope.calculate_all_data_views 310 | envelope.data_views.each do |x| 311 | expect(x.cost).not_to eq(Money.new(0)) 312 | end 313 | end 314 | end 315 | 316 | describe '#state:open' do 317 | it 'new data_rollups can be associated to the envelope' do 318 | envelope.save 319 | # data_rollup.save 320 | data_rollup 321 | expect { envelope.data_rollups << data_rollup }.to change(envelope.data_rollups, :count).by(1) 322 | expect(envelope.data_rollups.last).to eq(data_rollup) 323 | end 324 | it 'data_rollups can be associated to costs' do 325 | envelope.save 326 | # data_rollup.save 327 | data_rollup 328 | expect { envelope.data_rollups << data_rollup }.to change(envelope.data_views, :count).by(1) 329 | data_view = envelope.data_views.last 330 | expect(data_view.data_rollup).to eq(data_rollup) 331 | expect { data_view.cost = Money.new(3) }.to change(data_view, :cost).from(0).to(Money.new(3)) 332 | end 333 | 334 | it 'monetized cost' do 335 | expect(ManageIQ::Showback::DataView).to monetize(:cost) 336 | end 337 | 338 | pending 'data_views can be updated for an data_rollup' 339 | pending 'data_views can be updated for all data_rollups in the envelope' 340 | pending 'data_views can be deleted for an data_rollup' 341 | pending 'data_views can be deleted for all data_rollups in the envelope' 342 | pending 'is possible to return data_views for an data_rollup' 343 | pending 'is possible to return data_views for all data_rollups' 344 | pending 'sum of data_views can be calculated for the envelope' 345 | pending 'sum of data_views can be calculated for an data_rollup type' 346 | end 347 | 348 | describe '#state:processing' do 349 | pending 'new data_rollups are associated to a new or open envelope' 350 | pending 'new data_rollups can not be associated to the envelope' 351 | pending 'data_views can be deleted for an data_rollup' 352 | pending 'data_views can be deleted for all data_rollups in the envelope' 353 | pending 'data_views can be updated for an data_rollup' 354 | pending 'data_views can be updated for all data_rollups in the envelope' 355 | pending 'is possible to return data_views for an data_rollup' 356 | pending 'is possible to return data_views for all data_rollups' 357 | pending 'sum of data_views can be calculated for the envelope' 358 | pending 'sum of data_views can be calculated for an data_rollup type' 359 | end 360 | 361 | describe '#state:closed' do 362 | pending 'new data_rollups can not be associated to the envelope' 363 | pending 'new data_rollups are associated to a new or existing open envelope' 364 | pending 'data_views can not be deleted for an data_rollup' 365 | pending 'data_views can not be deleted for all data_rollups in the envelope' 366 | pending 'data_views can not be updated for an data_rollup' 367 | pending 'data_views can not be updated for all data_rollups in the envelope' 368 | pending 'is possible to return data_views for an data_rollup' 369 | pending 'is possible to return data_views for all data_rollups' 370 | pending 'sum of data_views can be calculated for the envelope' 371 | pending 'sum of data_views can be calculated for an data_rollup type' 372 | end 373 | end 374 | -------------------------------------------------------------------------------- /spec/models/input_measure_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'money-rails/test_helpers' 3 | 4 | describe ManageIQ::Showback::InputMeasure do 5 | before(:each) do 6 | ManageIQ::Showback::InputMeasure.delete_all 7 | end 8 | 9 | context "validations" do 10 | let(:input_measure) { FactoryBot.build(:input_measure) } 11 | let(:data_rollup) { FactoryBot.build(:data_rollup) } 12 | 13 | it "has a valid factory" do 14 | expect(input_measure).to be_valid 15 | end 16 | it "should ensure presence of entity" do 17 | input_measure.entity = nil 18 | expect(input_measure).not_to be_valid 19 | end 20 | 21 | it "should ensure presence of entity included in VALID_entity fail" do 22 | input_measure.entity = "region" 23 | expect(input_measure).to be_valid 24 | end 25 | 26 | it "should ensure presence of description" do 27 | input_measure.description = nil 28 | input_measure.valid? 29 | expect(input_measure.errors[:description]).to include "can't be blank" 30 | end 31 | 32 | it "should ensure presence of group measure" do 33 | input_measure.group = nil 34 | input_measure.valid? 35 | expect(input_measure.errors.messages[:group]).to include "can't be blank" 36 | end 37 | 38 | it "should invalidate incorrect group measure" do 39 | input_measure.group = "AA" 40 | expect(input_measure).to be_valid 41 | end 42 | 43 | it "should validate correct group measure" do 44 | input_measure.group = "CPU" 45 | expect(input_measure).to be_valid 46 | end 47 | 48 | it "should ensure presence of fields included in VALID_TYPES" do 49 | input_measure.fields = %w(average number) 50 | expect(input_measure).to be_valid 51 | end 52 | 53 | it 'should return entity::group' do 54 | expect(input_measure.name).to eq("Vm::CPU") 55 | end 56 | 57 | it 'should be a function to calculate this usage' do 58 | described_class.seed 59 | described_class.all.each do |usage| 60 | usage.fields.each do |dim| 61 | expect(data_rollup).to respond_to("#{usage.group}_#{dim}") 62 | end 63 | end 64 | end 65 | end 66 | 67 | context ".seed" do 68 | let(:expected_input_measure_count) { 28 } 69 | 70 | it "empty table" do 71 | described_class.seed 72 | expect(described_class.count).to eq(expected_input_measure_count) 73 | end 74 | 75 | it "run twice" do 76 | described_class.seed 77 | described_class.seed 78 | expect(described_class.count).to eq(expected_input_measure_count) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/models/manageiq/showback/version_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe ManageIQ::Showback do 2 | it 'has a version number' do 3 | expect(ManageIQ::Showback::VERSION).not_to be nil 4 | end 5 | 6 | it 'should have a version var that is constant' do 7 | expect(defined?(ManageIQ::Showback::VERSION)).to be == 'constant' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/models/manager_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'money-rails/test_helpers' 3 | 4 | RSpec.describe ManageIQ::Showback::Manager, :type => :model do 5 | it ".name" do 6 | expect(described_class.name).to eq('Showback') 7 | end 8 | 9 | it ".ems_type" do 10 | expect(described_class.ems_type).to eq('consumption_manager') 11 | end 12 | 13 | it ".description" do 14 | expect(described_class.description).to eq('Showback Manager') 15 | end 16 | 17 | it "generate new month for actual events" do 18 | vm = FactoryBot.create(:vm, :hardware => FactoryBot.create(:hardware, :cpu1x2, :memory_mb => 4096)) 19 | FactoryBot.create(:data_rollup, 20 | :start_time => DateTime.now.utc.beginning_of_month - 1.month, 21 | :end_time => DateTime.now.utc.end_of_month - 1.month, 22 | :resource => vm) 23 | expect(ManageIQ::Showback::DataRollup.where(:resource =>vm).count).to eq(1) 24 | described_class.generate_new_month 25 | expect(ManageIQ::Showback::DataRollup.where(:resource =>vm).count).to eq(2) 26 | end 27 | 28 | it "should generate events for new resources" do 29 | FactoryBot.create(:vm) 30 | expect(ManageIQ::Showback::DataRollup.all.count).to eq(0) 31 | described_class.generate_data_rollups 32 | expect(ManageIQ::Showback::DataRollup.all.count).to eq(1) 33 | expect(ManageIQ::Showback::DataRollup.first.start_time.month).to eq(ManageIQ::Showback::DataRollup.first.end_time.month) 34 | end 35 | 36 | it "should not generate the same ShowbackEvent 2 times of the same resource" do 37 | FactoryBot.create(:vm) 38 | described_class.generate_data_rollups 39 | described_class.generate_data_rollups 40 | expect(ManageIQ::Showback::DataRollup.all.count).to eq(1) 41 | end 42 | 43 | it "should generate new Showbackevent of resource if not has an event for actual month" do 44 | vm = FactoryBot.create(:vm) 45 | FactoryBot.create(:data_rollup, 46 | :start_time => DateTime.now.utc.beginning_of_month - 1.month, 47 | :end_time => DateTime.now.utc.end_of_month - 1.month, 48 | :resource => vm) 49 | count = ManageIQ::Showback::DataRollup.all.count 50 | described_class.generate_data_rollups 51 | expect(ManageIQ::Showback::DataRollup.all.count).to eq(count + 1) 52 | end 53 | 54 | it "should generate a showbackevent of service" do 55 | serv = FactoryBot.create(:service) 56 | described_class.generate_data_rollups 57 | expect(ManageIQ::Showback::DataRollup.first.resource).to eq(serv) 58 | expect(ManageIQ::Showback::DataRollup.first.context).not_to be_nil 59 | end 60 | 61 | it "should update the events" do 62 | event_metric = FactoryBot.create(:data_rollup, :start_time => DateTime.now.utc.beginning_of_month, :end_time => DateTime.now.utc.beginning_of_month + 2.days) 63 | event_metric.data = { 64 | "CPU" => { 65 | "average" => [52.67, "percent"], 66 | "max_number_of_cpu" => [4, "cores"] 67 | } 68 | } 69 | data_new = { 70 | "CPU" => { 71 | "average" => [52.67, "percent"], 72 | "max_number_of_cpu" => [4, "cores"] 73 | } 74 | } 75 | @vm_metrics = FactoryBot.create(:vm, :hardware => FactoryBot.create(:hardware, :cpu1x2, :memory_mb => 4096)) 76 | cases = [ 77 | DateTime.now.utc.beginning_of_month, 100.0, 78 | DateTime.now.utc.beginning_of_month + 1.hour, 1.0, 79 | DateTime.now.utc.beginning_of_month + 3.days, 2.0, 80 | DateTime.now.utc.beginning_of_month + 4.days, 4.0, 81 | DateTime.now.utc.beginning_of_month + 5.days, 8.0, 82 | DateTime.now.utc.beginning_of_month + 6.days, 15.0, 83 | DateTime.now.utc.beginning_of_month + 7.days, 100.0, 84 | ] 85 | cases.each_slice(2) do |t, v| 86 | @vm_metrics.metrics << FactoryBot.create( 87 | :metric_vm_rt, 88 | :timestamp => t, 89 | :cpu_usage_rate_average => v, 90 | # Multiply by a factor of 1000 to make it more realistic and enable testing virtual col v_pct_cpu_ready_delta_summation 91 | :cpu_ready_delta_summation => v * 1000, 92 | :sys_uptime_absolute_latest => v 93 | ) 94 | end 95 | event_metric.resource_id = @vm_metrics.id 96 | event_metric.resource_type = @vm_metrics.class.name 97 | event_metric.save! 98 | new_average = (event_metric.get_group_value("CPU", "average").to_d * event_metric.data_rollup_days + 99 | event_metric.resource.metrics.for_time_range(event_metric.end_time, nil).average(:cpu_usage_rate_average)) / (event_metric.data_rollup_days + 1) 100 | data_new["CPU"]["average"][0] = new_average.to_s 101 | described_class.update_data_rollups 102 | event_metric.reload 103 | expect(event_metric.data).to eq(data_new) 104 | expect(event_metric.end_time).to eq(@vm_metrics.metrics.last.timestamp) 105 | end 106 | 107 | it "Make the seed of ShowbackInputgroup and PricePlan" do 108 | expect(ManageIQ::Showback::PricePlan).to receive(:seed) 109 | expect(ManageIQ::Showback::InputMeasure).to receive(:seed) 110 | described_class.seed 111 | end 112 | 113 | context 'Units' do 114 | it "Should be the unit defined in YAML" do 115 | ManageIQ::Showback::InputMeasure.seed 116 | data_units = ManageIQ::Showback::Manager.load_column_units 117 | ManageIQ::Showback::InputMeasure.all.each do |usage| 118 | usage.fields.each do |dim| 119 | expect(data_units).to include(dim.to_sym) 120 | end 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/models/price_plan_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'money-rails/test_helpers' 3 | 4 | RSpec.describe ManageIQ::Showback::PricePlan, :type => :model do 5 | # We need to ShowbackInputgroup list to know what groups we should be looking for 6 | before(:each) do 7 | ManageIQ::Showback::InputMeasure.seed 8 | end 9 | 10 | context 'basic tests' do 11 | let(:plan) { FactoryBot.create(:price_plan) } 12 | 13 | it 'has a valid factory' do 14 | plan.valid? 15 | expect(plan).to be_valid 16 | end 17 | 18 | it 'is not valid without a name' do 19 | plan.name = nil 20 | plan.valid? 21 | expect(plan.errors.details[:name]). to include(:error => :blank) 22 | end 23 | 24 | it 'is not valid without a description' do 25 | plan.description = nil 26 | plan.valid? 27 | expect(plan.errors.details[:description]). to include(:error => :blank) 28 | end 29 | 30 | it 'is not valid without an association to a parent element' do 31 | plan.resource = nil 32 | plan.valid? 33 | expect(plan.errors.details[:resource]). to include(:error => :blank) 34 | end 35 | 36 | it 'is possible to add new rates to the price plan' do 37 | plan.save 38 | expect { FactoryBot.create(:rate, :price_plan => plan) }.to change(plan.rates, :count).from(0).to(1) 39 | end 40 | 41 | it 'rates are deleted when deleting the plan' do 42 | FactoryBot.create(:rate, :price_plan => plan) 43 | FactoryBot.create(:rate, :price_plan => plan) 44 | expect(plan.rates.count).to be(2) 45 | expect { plan.destroy }.to change(ManageIQ::Showback::Rate, :count).from(2).to(0) 46 | end 47 | 48 | context 'rating with no context' do 49 | let(:resource) { FactoryBot.create(:vm) } 50 | let(:event) { FactoryBot.build(:data_rollup, :with_vm_data, :full_month, :resource => resource) } 51 | let(:fixed_rate) { Money.new(11) } 52 | let(:variable_rate) { Money.new(7) } 53 | let(:fixed_rate2) { Money.new(5) } 54 | let(:variable_rate2) { Money.new(13) } 55 | let(:plan) { FactoryBot.create(:price_plan) } 56 | let(:rate) do 57 | FactoryBot.create(:rate, 58 | :CPU_average, 59 | :calculation => 'occurrence', 60 | :price_plan => plan) 61 | end 62 | let(:tier) do 63 | tier = rate.tiers.first 64 | tier.fixed_rate = fixed_rate 65 | tier.variable_rate = variable_rate 66 | tier.variable_rate_per_unit = "percent" 67 | tier.save 68 | tier 69 | end 70 | let(:rate2) do 71 | FactoryBot.create(:rate, 72 | :CPU_max_number_of_cpu, 73 | :calculation => 'duration', 74 | :price_plan => plan) 75 | end 76 | let(:tier2) do 77 | tier = rate2.tiers.first 78 | tier.fixed_rate = fixed_rate2 79 | tier.variable_rate = variable_rate2 80 | tier.variable_rate_per_unit = "cores" 81 | tier.save 82 | tier 83 | end 84 | 85 | it 'calculates costs when rate is not found' do 86 | event.save 87 | event.reload 88 | # Make rate entity not found 89 | rate.entity = 'not-found' 90 | rate.save 91 | expect(plan.calculate_total_cost(event)).to(eq(Money.new(0))) 92 | end 93 | 94 | it 'calculates list of costs when rate is not found and default event data' do 95 | rate.entity = 'not-found' 96 | rate.save 97 | resource_type = event.resource.type 98 | data = event.data 99 | expect(plan.calculate_list_of_costs_input(:resource_type => resource_type, :data => data)).to eq(plan.calculate_list_of_costs(event)) 100 | end 101 | 102 | it 'calculates costs when rate is not found and event data' do 103 | rate.entity = 'not-found' 104 | rate.save 105 | resource_type = event.resource.type 106 | data = event.data 107 | start_time = event.start_time 108 | end_time = event.end_time 109 | context = event.context 110 | expect(plan.calculate_list_of_costs_input(:resource_type => resource_type, 111 | :data => data, 112 | :context => context, 113 | :start_time => start_time, 114 | :end_time => end_time)).to eq(plan.calculate_list_of_costs(event)) 115 | expect(plan.calculate_list_of_costs_input(:resource_type => resource_type, :data => data)).to eq(plan.calculate_list_of_costs(event)) 116 | end 117 | 118 | it 'test that data is right' do 119 | event.save 120 | event.reload 121 | # test that the event has the information we need in data 122 | expect(event.data['CPU']).not_to be_nil 123 | expect(event.data['CPU']['average']).not_to be_nil 124 | expect(event.data['CPU']['max_number_of_cpu']).not_to be_nil 125 | end 126 | 127 | it 'calculates costs with one rate2' do 128 | tier 129 | event.save 130 | event.reload 131 | rate.save 132 | # Rating now should return the value 133 | expect(plan.calculate_total_cost(event)).to eq(Money.new(18)) 134 | end 135 | 136 | it 'calculates costs when more than one rate applies' do 137 | event.save 138 | event.reload 139 | rate.save 140 | rate2.save 141 | # Rating now should return the value 142 | expect(plan.calculate_total_cost(event)).to eq(rate.rate(event) + rate2.rate(event)) 143 | end 144 | end 145 | 146 | context 'rating with context' do 147 | let(:resource) { FactoryBot.create(:vm) } 148 | let(:event) { FactoryBot.build(:data_rollup, :with_vm_data, :full_month, :with_tags_in_context, :resource => resource) } 149 | let(:fixed_rate) { Money.new(11) } 150 | let(:variable_rate) { Money.new(7) } 151 | let(:plan) { FactoryBot.create(:price_plan) } 152 | let(:rate) do 153 | FactoryBot.build(:rate, 154 | :CPU_average, 155 | :price_plan => plan) 156 | end 157 | let(:tier1) { rate.tiers.first } 158 | let(:rate2) do 159 | FactoryBot.build(:rate, 160 | :CPU_max_number_of_cpu, 161 | :price_plan => plan) 162 | end 163 | let(:tier2) { rate2.tiers.first } 164 | 165 | it 'test that data is right' do 166 | event.save 167 | event.reload 168 | # test that the event has the information we need in data 169 | expect(event.data['CPU']).not_to be_nil 170 | expect(event.data['CPU']['average']).not_to be_nil 171 | expect(event.data['CPU']['max_number_of_cpu']).not_to be_nil 172 | end 173 | 174 | it 'returns list of costs when no rate is found' do 175 | event.save 176 | event.reload 177 | # Make rate entity not found 178 | rate.entity = 'not-found' 179 | rate.save 180 | expect(plan.calculate_list_of_costs(event)).to be_empty 181 | end 182 | 183 | it 'calculates costs when rate is not found' do 184 | event.save 185 | event.reload 186 | # Make rate entity not found 187 | rate.entity = 'not-found' 188 | rate.save 189 | expect(plan.calculate_total_cost(event)).to eq(Money.new(0)) 190 | end 191 | 192 | it 'calculates costs with one rate' do 193 | event.save 194 | event.reload 195 | rate2.save 196 | # Rating now should return the value 197 | expect(plan.calculate_total_cost(event)).to eq(rate2.rate(event)) 198 | end 199 | 200 | it 'returns list of costs with one rate' do 201 | event.save 202 | event.reload 203 | rate2.save 204 | # Rating now should return the value 205 | expect(plan.calculate_list_of_costs(event)).to match_array([[rate2.rate(event), rate2]]) 206 | end 207 | 208 | it 'calculates costs when more than one rate applies' do 209 | event.save 210 | event.reload 211 | rate.save 212 | rate2.save 213 | # Rating now should return the value 214 | expect(plan.calculate_total_cost(event)).to eq(rate.rate(event) + rate2.rate(event)) 215 | end 216 | 217 | it 'return list of costs when more than one rate applies' do 218 | event.save 219 | event.reload 220 | rate.save 221 | rate2.save 222 | # Rating now should return the value 223 | expect(plan.calculate_list_of_costs(event)).to match_array([[rate.rate(event), rate], [rate2.rate(event), rate2]]) 224 | end 225 | end 226 | end 227 | 228 | context '.seed' do 229 | let(:expected_price_plan_count) { 1 } 230 | let!(:resource) { FactoryBot.create(:miq_enterprise, :name => 'Enterprise') } 231 | 232 | it 'empty table' do 233 | described_class.seed 234 | expect(described_class.count).to eq(expected_price_plan_count) 235 | end 236 | 237 | it 'run twice' do 238 | described_class.seed 239 | described_class.seed 240 | expect(described_class.count).to eq(expected_price_plan_count) 241 | end 242 | end 243 | end 244 | -------------------------------------------------------------------------------- /spec/models/rate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'money-rails/test_helpers' 3 | module ManageIQ::Showback 4 | describe Rate do 5 | let(:time_current) { Time.parse('Tue, 05 Feb 2019 18:53:19 UTC +00:00').utc } 6 | 7 | before do 8 | skip # TODO: fix these specs 9 | Timecop.travel(time_current) 10 | end 11 | 12 | before(:each) do 13 | Timecop.travel(time_current) 14 | InputMeasure.seed 15 | end 16 | 17 | after do 18 | Timecop.return 19 | end 20 | 21 | describe 'model validations' do 22 | let(:rate) { FactoryBot.build(:rate) } 23 | 24 | it 'has a valid factory' do 25 | expect(rate).to be_valid 26 | end 27 | 28 | it 'has a tier after create' do 29 | sr = FactoryBot.create(:rate) 30 | expect(sr.tiers.count).to eq(1) 31 | end 32 | 33 | it 'returns name as entity + field' do 34 | entity = rate.entity 35 | field = rate.field 36 | group = rate.group 37 | expect(rate.name).to eq("#{entity}:#{group}:#{field}") 38 | end 39 | 40 | it 'is not valid with a nil calculation' do 41 | rate.calculation = nil 42 | rate.valid? 43 | expect(rate.errors.details[:calculation]).to include(:error=>:blank) 44 | end 45 | 46 | it 'calculation is valid when included in VALID_RATE_CALCULATIONS' do 47 | calculations = %w(occurrence duration quantity) 48 | expect(ManageIQ::Showback::Rate::VALID_RATE_CALCULATIONS).to eq(calculations) 49 | calculations.each do |calc| 50 | rate.calculation = calc 51 | expect(rate).to be_valid 52 | end 53 | end 54 | 55 | it 'calculation is not valid if it is not in VALID_RATE_CALCULATIONS' do 56 | rate.calculation = 'ERROR' 57 | expect(rate).not_to be_valid 58 | expect(rate.errors.details[:calculation]). to include(:error => :inclusion, :value => 'ERROR') 59 | end 60 | 61 | it 'is not valid with a nil entity' do 62 | rate.entity = nil 63 | rate.valid? 64 | expect(rate.errors.details[:entity]).to include(:error=>:blank) 65 | end 66 | 67 | it 'is not valid with a nil field' do 68 | rate.field = nil 69 | rate.valid? 70 | expect(rate.errors.details[:field]).to include(:error=>:blank) 71 | end 72 | 73 | it 'is valid with a nil concept' do 74 | rate.concept = nil 75 | rate.valid? 76 | expect(rate).to be_valid 77 | end 78 | 79 | it '#group is valid with a non empty string' do 80 | rate.group = 'Hz' 81 | rate.valid? 82 | expect(rate).to be_valid 83 | end 84 | 85 | it '#group is not valid when nil' do 86 | rate.group = nil 87 | rate.valid? 88 | expect(rate.errors.details[:group]).to include(:error => :blank) 89 | end 90 | 91 | it 'is valid with a JSON screener' do 92 | rate.screener = JSON.generate('tag' => { 'environment' => ['test'] }) 93 | rate.valid? 94 | expect(rate).to be_valid 95 | end 96 | 97 | pending 'is not valid with a wronly formatted screener' do 98 | rate.screener = JSON.generate('tag' => { 'environment' => ['test'] }) 99 | rate.valid? 100 | expect(rate).not_to be_valid 101 | end 102 | 103 | it 'is not valid with a nil screener' do 104 | rate.screener = nil 105 | rate.valid? 106 | expect(rate.errors.details[:screener]).to include(:error => :exclusion, :value => nil) 107 | end 108 | end 109 | 110 | describe 'when the data_rollup lasts for the full month and the rates too' do 111 | let(:fixed_rate) { Money.new(11) } 112 | let(:variable_rate) { Money.new(7) } 113 | let(:rate) { FactoryBot.create(:rate, :CPU_number) } 114 | let(:tier) do 115 | tier = rate.tiers.first 116 | tier.fixed_rate = fixed_rate 117 | tier.variable_rate = variable_rate 118 | tier.variable_rate_per_unit = "cores" 119 | tier.save 120 | tier 121 | end 122 | let(:data_rollup_fm) { FactoryBot.create(:data_rollup, :full_month, :with_vm_data) } 123 | 124 | context 'empty #context, default rate per_time and per_unit' do 125 | it 'should data_view an data_rollup by occurrence when data_rollup exists' do 126 | tier 127 | data_rollup_fm.reload 128 | rate.calculation = 'occurrence' 129 | expect(rate.rate(data_rollup_fm)).to eq(fixed_rate + variable_rate) 130 | end 131 | 132 | it 'should data_view an data_rollup by occurrence only the fixed rate when value is nil' do 133 | tier 134 | data_rollup_fm.reload 135 | rate.calculation = 'occurrence' 136 | data_rollup_fm.data = {} # There is no data for this rate in the data_rollup 137 | expect(rate.rate(data_rollup_fm)).to eq(fixed_rate) 138 | end 139 | 140 | it 'should data_view an data_rollup by duration' do 141 | tier 142 | data_rollup_fm.reload 143 | rate.calculation = 'duration' 144 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(11 + 7 * 2)) 145 | end 146 | 147 | it 'should data_view an data_rollup by quantity' do 148 | tier 149 | data_rollup_fm.reload 150 | rate.calculation = 'quantity' 151 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(11 + 7 * 2)) 152 | end 153 | end 154 | 155 | context 'minimum step' do 156 | let(:fixed_rate) { Money.new(11) } 157 | let(:variable_rate) { Money.new(7) } 158 | let(:rate) { FactoryBot.create(:rate, :MEM_max_mem) } 159 | let(:tier) do 160 | tier = rate.tiers.first 161 | tier.fixed_rate = fixed_rate 162 | tier.variable_rate = variable_rate 163 | tier.variable_rate_per_unit = "MiB" 164 | tier.save 165 | tier 166 | end 167 | let(:data_rollup_fm) { FactoryBot.create(:data_rollup, :full_month, :with_vm_data) } 168 | let(:data_rollup_hm) { FactoryBot.create(:data_rollup, :first_half_month, :with_vm_data) } 169 | 170 | it 'nil step should behave like no step' do 171 | data_rollup_fm.reload 172 | tier.step_unit = nil 173 | tier.step_value = nil 174 | tier.step_time_value = nil 175 | tier.step_time_unit = nil 176 | tier.save 177 | rate.calculation = 'duration' 178 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(11 + 7 * 2048)) 179 | end 180 | 181 | it 'basic unit step should behave like no step' do 182 | data_rollup_fm.reload 183 | tier.step_unit = 'b' 184 | tier.step_value = 1 185 | tier.step_time_value = nil 186 | tier.step_time_unit = nil 187 | tier.save 188 | rate.calculation = 'duration' 189 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(11 + 7 * 2048)) 190 | end 191 | 192 | it 'when input is 0 it works' do 193 | tier.step_unit = 'b' 194 | tier.step_value = 1 195 | tier.step_time_value = nil 196 | tier.step_time_unit = nil 197 | rate.calculation = 'duration' 198 | data_rollup_fm.data["MEM"]["max_mem"][0] = 0 199 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(11)) 200 | end 201 | 202 | it 'should work if step unit is a subunit of the tier' do 203 | data_rollup_fm.reload 204 | tier.step_unit = 'Gib' 205 | tier.step_value = 1 206 | tier.step_time_value = nil 207 | tier.step_time_unit = nil 208 | rate.calculation = 'duration' 209 | tier.save 210 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(11 + 7 * 2048)) 211 | 212 | tier.step_value = 4 213 | tier.step_unit = 'Gib' 214 | tier.save 215 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(11 + 7 * 4096)) 216 | 217 | # Modify the input data so the data is not a multiple 218 | data_rollup_fm.data["MEM"]["max_mem"][0] = 501 219 | data_rollup_fm.data["MEM"]["max_mem"][1] = 'MiB' 220 | 221 | tier.step_unit = 'MiB' 222 | tier.step_value = 384 223 | tier.save 224 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(11 + 7 * 384 * 2)) 225 | end 226 | 227 | pending 'step time moves half_month to full_month' do 228 | tier.step_unit = 'b' 229 | tier.step_value = 1 230 | tier.step_time_value = 1 231 | tier.step_time_unit = 'month' 232 | rate.calculation = 'duration' 233 | expect(rate.rate(data_rollup_hm)).to eq(rate.rate(data_rollup_fm)) 234 | end 235 | 236 | pending 'step is not a subunit of the tier' do 237 | # Rate is using Vm:CPU:Number 238 | tier.step_unit = 'cores' 239 | tier.step_value = 1 240 | tier.step_time_value = nil 241 | tier.step_time_unit = nil 242 | rate.calculation = 'duration' 243 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(11 + 7 * 2)) 244 | end 245 | 246 | pending 'step is higher than the tier' 247 | end 248 | 249 | context 'empty #context, modified per_time' do 250 | it 'should data_view an data_rollup by occurrence' do 251 | data_rollup_fm.reload 252 | rate.calculation = 'occurrence' 253 | tier.fixed_rate_per_time = 'daily' 254 | tier.variable_rate_per_time = 'daily' 255 | tier.save 256 | days_in_month = Time.days_in_month(Time.current.month) 257 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(days_in_month * (11 + 7))) 258 | end 259 | 260 | it 'should data_view an data_rollup by duration' do 261 | tier 262 | data_rollup_fm.reload 263 | rate.calculation = 'duration' 264 | tier.fixed_rate_per_time = 'daily' 265 | tier.variable_rate_per_time = 'daily' 266 | tier.save 267 | days_in_month = Time.days_in_month(Time.current.month) 268 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(days_in_month * (11 + 7 * 2))) 269 | end 270 | 271 | it 'should data_view an data_rollup by quantity' do 272 | data_rollup_fm.reload 273 | rate.calculation = 'quantity' 274 | tier.fixed_rate_per_time = 'daily' 275 | tier.variable_rate_per_time = 'daily' 276 | tier.save 277 | days_in_month = Time.days_in_month(Time.current.month) 278 | # Fixed is 11 per day, variable is 7 per CPU, data_rollup has average of 2 CPU 279 | expect(rate.rate(data_rollup_fm)).to eq(Money.new((days_in_month * 11) + (7 * 2))) 280 | end 281 | end 282 | 283 | context 'empty context, modified per unit' do 284 | it 'should data_view an data_rollup by duration' do 285 | data_rollup_fm.reload 286 | rate.calculation = 'duration' 287 | rate.field = 'max_mem' 288 | rate.group = 'MEM' 289 | tier.variable_rate_per_unit = 'b' 290 | tier.save 291 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(11 + (2048 * 1024 * 1024 * 7))) 292 | tier.variable_rate_per_unit = 'Kib' 293 | tier.save 294 | expect(rate.rate(data_rollup_fm)).to eq(Money.new(11 + (2048 * 1024 * 7))) 295 | end 296 | 297 | pending 'should data_view an data_rollup by quantity' 298 | end 299 | 300 | context 'tiered on input value' do 301 | pending 'it should data_view an data_rollup by occurrence' 302 | pending 'it should data_view an data_rollup by duration' 303 | pending 'it should data_view an data_rollup by quantity' 304 | end 305 | 306 | context 'tiered on non-input value in #context' do 307 | pending 'it should data_view an data_rollup by occurrence' 308 | pending 'it should data_view an data_rollup by duration' 309 | pending 'it should data_view an data_rollup by quantity' 310 | end 311 | end 312 | 313 | describe 'more than 1 tier in the rate' do 314 | let(:fixed_rate) { Money.new(11) } 315 | let(:variable_rate) { Money.new(7) } 316 | let(:rate) { FactoryBot.create(:rate, :CPU_number, :calculation => 'quantity') } 317 | let(:data_rollup_hm) { FactoryBot.create(:data_rollup, :first_half_month, :with_vm_data) } 318 | let(:tier) do 319 | tier = rate.tiers.first 320 | tier.fixed_rate = fixed_rate 321 | tier.tier_end_value = 3.0 322 | tier.step_unit = 'cores' 323 | tier.step_value = 1 324 | tier.variable_rate = variable_rate 325 | tier.variable_rate_per_unit = "cores" 326 | tier.save 327 | tier 328 | end 329 | let(:tier_second) do 330 | FactoryBot.create(:tier, 331 | :rate => rate, 332 | :tier_start_value => 3.0, 333 | :tier_end_value => Float::INFINITY, 334 | :step_value => 1, 335 | :step_unit => 'cores', 336 | :fixed_rate => Money.new(15), 337 | :variable_rate => Money.new(10), 338 | :variable_rate_per_unit => "cores") 339 | end 340 | context 'use only a single tier' do 341 | it 'should data_view an data_rollup by quantity with 1 tier with tiers_use_full_value' do 342 | data_rollup_hm.reload 343 | tier 344 | tier_second 345 | expect(rate.rate(data_rollup_hm)).to eq(Money.new(11 + 7 * 2)) 346 | data_rollup_hm.data['CPU']['number'][0] = 4.0 347 | expect(rate.rate(data_rollup_hm)).to eq(Money.new(15 + 10 * 4)) 348 | end 349 | it 'should data_view an data_rollup by quantity with 1 tier with not tiers_use_full_value' do 350 | data_rollup_hm.reload 351 | tier 352 | tier_second 353 | rate.tiers_use_full_value = false 354 | rate.tier_input_variable = 'cores' 355 | expect(rate.rate(data_rollup_hm)).to eq(Money.new(11 + 7 * (2 - 0))) 356 | data_rollup_hm.data['CPU']['number'][0] = 4.0 357 | expect(rate.rate(data_rollup_hm)).to eq(Money.new(15 + (10 * (4 - 3.0)))) 358 | end 359 | end 360 | 361 | context 'with all tiers' do 362 | it 'should data_view an data_rollup by quantity with 2 tiers with tiers_use_full_value' do 363 | data_rollup_hm.reload 364 | tier 365 | tier_second 366 | rate.uses_single_tier = false 367 | data_rollup_hm.data['CPU']['number'][0] = 4.0 368 | expect(rate.rate(data_rollup_hm)).to eq(Money.new(11 + 7 * 4) + Money.new(15 + 10 * 4)) 369 | end 370 | 371 | it 'should data_view an data_rollup by quantity with 2 tiers with not tiers_use_full_value' do 372 | data_rollup_hm.reload 373 | tier 374 | tier_second 375 | rate.uses_single_tier = false 376 | rate.tiers_use_full_value = false 377 | rate.tier_input_variable = 'cores' 378 | data_rollup_hm.data['CPU']['number'][0] = 4.0 379 | expect(rate.rate(data_rollup_hm)).to eq(Money.new(11 + 7 * (4 - 0)) + Money.new(15 + 10 * (4 - 3.0))) 380 | end 381 | end 382 | end 383 | describe 'data_rollup lasts the first 15 days and the rate is monthly' do 384 | let(:fixed_rate) { Money.new(11) } 385 | let(:variable_rate) { Money.new(7) } 386 | let(:rate) { FactoryBot.create(:rate, :CPU_number) } 387 | let(:data_rollup_hm) { FactoryBot.create(:data_rollup, :first_half_month, :with_vm_data) } 388 | let(:proration) { data_rollup_hm.time_span.to_f / data_rollup_hm.month_duration } 389 | let(:tier) do 390 | tier = rate.tiers.first 391 | tier.fixed_rate = fixed_rate 392 | tier.variable_rate = variable_rate 393 | tier.variable_rate_per_unit = "cores" 394 | tier.save 395 | tier 396 | end 397 | 398 | context 'empty #context' do 399 | it 'should data_view an data_rollup by occurrence' do 400 | tier 401 | rate.calculation = 'occurrence' 402 | expect(rate.rate(data_rollup_hm)).to eq(Money.new(11) + Money.new(7)) 403 | end 404 | 405 | it 'should data_view an data_rollup by duration' do 406 | tier 407 | data_rollup_hm.reload 408 | rate.calculation = 'duration' 409 | expect(rate.rate(data_rollup_hm)).to eq(Money.new(11 + 7 * 2) * proration) 410 | end 411 | 412 | it 'should data_view an data_rollup by quantity' do 413 | data_rollup_hm.reload 414 | tier 415 | rate.calculation = 'quantity' 416 | # Fixed is 11 per day, variable is 7 per CPU, data_rollup has 2 CPU 417 | expect(rate.rate(data_rollup_hm)).to eq(Money.new(11 + 7 * 2)) 418 | end 419 | end 420 | 421 | context 'empty #context, modified per_time' do 422 | it 'should data_view an data_rollup by occurrence' do 423 | data_rollup_hm.reload 424 | rate.calculation = 'occurrence' 425 | tier.fixed_rate_per_time = 'daily' 426 | tier.variable_rate_per_time = 'daily' 427 | tier.save 428 | days_in_month = Time.days_in_month(Time.current.month) 429 | expect(rate.rate(data_rollup_hm)).to eq(Money.new(days_in_month * (11 + 7))) 430 | end 431 | 432 | it 'should data_view an data_rollup by duration' do 433 | data_rollup_hm.reload 434 | rate.calculation = 'duration' 435 | tier.fixed_rate_per_time = 'daily' 436 | tier.variable_rate_per_time = 'daily' 437 | tier.save 438 | days_in_month = Time.days_in_month(Time.current.month) 439 | expect(rate.rate(data_rollup_hm)).to eq(Money.new(days_in_month * proration * (11 + 7 * 2))) 440 | end 441 | 442 | it 'should data_view an data_rollup by quantity' do 443 | data_rollup_hm.reload 444 | rate.calculation = 'quantity' 445 | tier.fixed_rate_per_time = 'daily' 446 | tier.variable_rate_per_time = 'daily' 447 | tier.save 448 | days_in_month = Time.days_in_month(Time.current.month) 449 | # Fixed is 11 per day, variable is 7 per CPU, data_rollup has 2 CPU 450 | expect(rate.rate(data_rollup_hm)).to eq(Money.new((days_in_month * 11) + (7 * 2))) 451 | end 452 | end 453 | 454 | context 'tiered on input value' do 455 | pending 'it should data_view an data_rollup by occurrence' 456 | pending 'it should data_view an data_rollup by duration' 457 | pending 'it should data_view an data_rollup by quantity' 458 | end 459 | 460 | context 'tiered on non-input value in #context' do 461 | pending 'it should data_view an data_rollup by occurrence' 462 | pending 'it should data_view an data_rollup by duration' 463 | pending 'it should data_view an data_rollup by quantity' 464 | end 465 | end 466 | 467 | describe 'data_rollup lasts 1 day for a weekly rate' do 468 | pending 'should data_view an data_rollup by occurrence' 469 | pending 'should data_view an data_rollup by duration' 470 | pending 'should data_view an data_rollup by quantity' 471 | end 472 | 473 | describe 'data_rollup lasts 1 week for a daily rate' do 474 | pending 'should data_view an data_rollup by occurrence' 475 | pending 'should data_view an data_rollup by duration' 476 | pending 'should data_view an data_rollup by quantity' 477 | end 478 | end 479 | end 480 | -------------------------------------------------------------------------------- /spec/models/tier_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'money-rails/test_helpers' 3 | 4 | RSpec.describe ManageIQ::Showback::Tier, :type => :model do 5 | describe 'model validations' do 6 | let(:rate) { FactoryBot.create(:rate) } 7 | let(:tier) { rate.tiers.first } 8 | 9 | it 'has a valid factory' do 10 | expect(tier).to be_valid 11 | end 12 | 13 | it 'returns a name' do 14 | expect(tier.name).to eq("#{rate.entity}:#{rate.group}:#{rate.field}:Tier:#{tier.tier_start_value}-#{tier.tier_end_value}") 15 | end 16 | 17 | it 'is not valid with a nil fixed_rate' do 18 | tier.fixed_rate_subunits = nil 19 | tier.valid? 20 | expect(tier.errors.details[:fixed_rate]).to include(:error => :not_a_number, :value => '') 21 | end 22 | 23 | it 'is not valid with a nil variable_rate' do 24 | rate 25 | tier.variable_rate_subunits = nil 26 | tier.valid? 27 | expect(tier.errors.details[:variable_rate]).to include(:error => :not_a_number, :value => '') 28 | end 29 | 30 | it 'has a Money fixed_rate' do 31 | expect(described_class).to monetize(:fixed_rate) 32 | tier.fixed_rate = Money.new(256_000_000, 'US8') 33 | expect(tier).to be_valid 34 | expect(tier.fixed_rate.exchange_to('USD').format).to eq('$2.56') 35 | end 36 | 37 | it 'has a Money variable_rate' do 38 | expect(described_class).to monetize(:variable_rate) 39 | tier.variable_rate = Money.new(675_000_000, 'US8') 40 | expect(tier).to be_valid 41 | expect(tier.variable_rate.exchange_to('USD').format).to eq('$6.75') 42 | end 43 | 44 | it '#fixed_rate_per_time included in VALID_INTERVAL_UNITS is valid' do 45 | ManageIQ::Showback::TimeConverterHelper::VALID_INTERVAL_UNITS.each do |interval| 46 | tier.fixed_rate_per_time = interval 47 | tier.valid? 48 | expect(tier).to be_valid 49 | end 50 | end 51 | 52 | it '#fixed_rate_per_time not included in VALID_INTERVAL_UNITS is not valid' do 53 | tier.fixed_rate_per_time = 'bad_interval' 54 | tier.valid? 55 | expect(tier.errors.details[:fixed_rate_per_time]).to include(:error => :inclusion, :value => 'bad_interval') 56 | end 57 | 58 | it '#variable_rate_per_time included in VALID_INTERVAL_UNITS is valid' do 59 | ManageIQ::Showback::TimeConverterHelper::VALID_INTERVAL_UNITS.each do |interval| 60 | tier.variable_rate_per_time = interval 61 | tier.valid? 62 | expect(tier).to be_valid 63 | end 64 | end 65 | 66 | it '#variable_rate_per_time not included in VALID_INTERVAL_UNITS is not valid' do 67 | tier.variable_rate_per_time = 'bad_interval' 68 | tier.valid? 69 | expect(tier.errors.details[:variable_rate_per_time]).to include(:error => :inclusion, :value => 'bad_interval') 70 | end 71 | 72 | it '#variable_rate_per_unit is valid with a non empty string' do 73 | tier.variable_rate_per_unit = 'Hz' 74 | tier.valid? 75 | expect(tier).to be_valid 76 | end 77 | 78 | it '#variable_rate_per_unit is valid with an empty string' do 79 | tier.variable_rate_per_unit = '' 80 | tier.valid? 81 | expect(tier).to be_valid 82 | end 83 | 84 | it '#variable_rate_per_unit is not valid when nil' do 85 | tier.variable_rate_per_unit = nil 86 | tier.valid? 87 | expect(tier.errors.details[:variable_rate_per_unit]).to include(:error => :exclusion, :value => nil) 88 | end 89 | 90 | context 'validate intervals' do 91 | it 'end_value is lower than start_value' do 92 | tier.tier_start_value = 15 93 | tier.tier_end_value = 10 94 | expect { tier.valid? }.to raise_error(RuntimeError, _("Start value of interval is greater than end value")) 95 | end 96 | it '#there is a showbackTier just defined with Float::INFINITY you cant add another in this interval' do 97 | st = FactoryBot.build(:tier, :rate => tier.rate, :tier_start_value => 5, :tier_end_value => 10) 98 | expect { st.valid? }.to raise_error(RuntimeError, _("Interval or subinterval is in a tier with Infinity at the end")) 99 | end 100 | it '#there is a showbackTier with Infinity' do 101 | st = FactoryBot.build(:tier, :rate => tier.rate, :tier_start_value => 5, :tier_end_value => Float::INFINITY) 102 | expect { st.valid? }.to raise_error(RuntimeError, _("Interval or subinterval is in a tier with Infinity at the end")) 103 | end 104 | it '#there is a showbackTier just defined in this interval' do 105 | tier.tier_start_value = 2 106 | tier.tier_end_value = 7 107 | tier.save 108 | st = FactoryBot.build(:tier, :rate => tier.rate, :tier_start_value => 5, :tier_end_value => 10) 109 | expect { st.valid? }.to raise_error(RuntimeError, _("Interval or subinterval is in another tier")) 110 | end 111 | end 112 | end 113 | 114 | describe 'tier methods' do 115 | let(:rate) { FactoryBot.create(:rate) } 116 | let(:tier) { rate.tiers.first } 117 | context 'interval methods' do 118 | it '#range return the range of tier_start_value and tier_end_value' do 119 | expect(tier.range).to eq(0..Float::INFINITY) 120 | end 121 | it '#includes? method' do 122 | tier.tier_start_value = 2 123 | tier.tier_end_value = 4 124 | expect(tier.includes?(3)).to be_truthy 125 | expect(tier.includes?(5)).to be_falsey 126 | tier.tier_end_value = Float::INFINITY 127 | expect(tier.includes?(500)).to be_truthy 128 | expect(tier.includes?(1)).to be_falsey 129 | end 130 | it '#set_range method' do 131 | tier.set_range(2, 4) 132 | expect(tier.range).to eq(2..4) 133 | tier.set_range(2, Float::INFINITY) 134 | expect(tier.range).to eq(2..Float::INFINITY) 135 | end 136 | it '#starts_with_zero? method' do 137 | tier.tier_start_value = 2 138 | expect(tier.starts_with_zero?).to be_falsey 139 | tier.tier_start_value = 0 140 | expect(tier.starts_with_zero?).to be_truthy 141 | end 142 | it '#ends_with_infinity?method' do 143 | expect(tier.ends_with_infinity?).to be_truthy 144 | tier.tier_end_value = 10 145 | expect(tier.ends_with_infinity?).to be_falsey 146 | end 147 | it '#free? method' do 148 | tier.fixed_rate = 0 149 | tier.variable_rate = 0 150 | expect(tier.free?).to be_truthy 151 | tier.fixed_rate = 10 152 | expect(tier.free?).to be_falsey 153 | tier.fixed_rate = 0 154 | tier.variable_rate = 10 155 | expect(tier.free?).to be_falsey 156 | tier.fixed_rate = 10 157 | tier.variable_rate = 10 158 | expect(tier.free?).to be_falsey 159 | end 160 | it 'to_float method' do 161 | tier.tier_start_value = 2 162 | tier.tier_end_value = Float::INFINITY 163 | expect(described_class.to_float(tier.tier_start_value)).to eq(2) 164 | expect(described_class.to_float(tier.tier_end_value)).to eq(Float::INFINITY) 165 | end 166 | it 'divide tier method' do 167 | expect(ManageIQ::Showback::Tier.where(:rate => tier.rate).count).to eq(1) 168 | tier.divide_tier(5) 169 | tier.reload 170 | expect(ManageIQ::Showback::Tier.to_float(tier.tier_end_value)).to eq(5) 171 | expect(ManageIQ::Showback::Tier.where(:rate => tier.rate).count).to eq(2) 172 | end 173 | end 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | require 'spec_helper' 3 | ENV['RAILS_ENV'] ||= 'test' 4 | require File.expand_path('manageiq/config/environment', __FILE__) 5 | # Prevent database truncation if the environment is production 6 | abort("The Rails environment is running in production mode!") if Rails.env.production? 7 | require 'rspec/rails' 8 | # Add additional requires below this line. Rails is not loaded until this point! 9 | 10 | # Requires supporting ruby files with custom matchers and macros, etc, in 11 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 12 | # run as spec files by default. This means that files in spec/support that end 13 | # in _spec.rb will both be required and run as specs, causing the specs to be 14 | # run twice. It is recommended that you do not name files matching this glob to 15 | # end with _spec.rb. You can configure this pattern with the --pattern 16 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 17 | # 18 | # The following line is provided for convenience purposes. It has the downside 19 | # of increasing the boot-up time by auto-requiring all files in the support 20 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 21 | # require only the support files necessary. 22 | # 23 | # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 24 | 25 | # Checks for pending migration and applies them before tests are run. 26 | # If you are not using ActiveRecord, you can remove this line. 27 | ActiveRecord::Migration.maintain_test_schema! 28 | 29 | RSpec.configure do |config| 30 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 31 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 32 | 33 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 34 | # examples within a transaction, remove the following line or assign false 35 | # instead of true. 36 | config.use_transactional_fixtures = true 37 | 38 | # RSpec Rails can automatically mix in different behaviours to your tests 39 | # based on their file location, for example enabling you to call `get` and 40 | # `post` in specs under `spec/controllers`. 41 | # 42 | # You can disable this behaviour by removing the line below, and instead 43 | # explicitly tag your specs with their type, e.g.: 44 | # 45 | # RSpec.describe UsersController, :type => :controller do 46 | # # ... 47 | # end 48 | # 49 | # The different available types are documented in the features, such as in 50 | # https://relishapp.com/rspec/rspec-rails/docs 51 | config.infer_spec_type_from_file_location! 52 | 53 | # Filter lines from Rails gems in backtraces. 54 | config.filter_rails_from_backtrace! 55 | # arbitrary gems may also be filtered via: 56 | # config.filter_gems_from_backtrace("gem name") 57 | I18n.locale = :en 58 | end 59 | -------------------------------------------------------------------------------- /spec/showback_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe ManageIQ::Showback do 2 | let(:version) { "0.0.1" } 3 | 4 | it "has a version number" do 5 | expect(ManageIQ::Showback::VERSION).not_to be nil 6 | end 7 | 8 | it "Version number is 0.0.1" do 9 | expect(ManageIQ::Showback::VERSION).to eq(version) 10 | end 11 | 12 | it "should have a version var" do 13 | expect(defined?(ManageIQ::Showback::VERSION)).to be == "constant" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['CI'] 2 | require 'simplecov' 3 | SimpleCov.start 4 | end 5 | 6 | Dir[Rails.root.join("spec/shared/**/*.rb")].each { |f| require f } 7 | Dir[File.join(__dir__, "support/**/*.rb")].each { |f| require f } 8 | 9 | require "manageiq/consumption" 10 | --------------------------------------------------------------------------------