├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ └── .keep │ ├── javascripts │ │ ├── application.js │ │ ├── bootstrap-table.min.js │ │ ├── bootstrap-tablo-export.min.js │ │ ├── bootstrap.js.coffee │ │ └── tableExport.min.js │ └── stylesheets │ │ ├── application.css │ │ ├── bootstrap-table.min.css │ │ └── bootstrap_and_overrides.css.less ├── controllers │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ ├── instances_controller.rb │ ├── reserved_instances_controller.rb │ ├── setup_controller.rb │ ├── summary_controller.rb │ └── tests_controller.rb ├── helpers │ └── application_helper.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── ami.rb │ ├── concerns │ │ └── .keep │ ├── instance.rb │ ├── modification.rb │ ├── recommendation.rb │ ├── recommendation_cache.rb │ ├── reserved_instance.rb │ ├── setup.rb │ └── summary.rb └── views │ ├── instances │ └── index.erb │ ├── layouts │ └── application.html.erb │ ├── reserved_instances │ └── index.erb │ ├── setup │ ├── clear_cache.erb │ └── index.erb │ ├── summary │ ├── apply_recommendations.erb │ ├── index.erb │ ├── log_recommendations.erb │ └── recommendations.erb │ └── tests │ └── index.erb ├── bin ├── bundle ├── rails ├── rake └── setup ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ ├── en.bootstrap.yml │ └── en.yml ├── routes.rb └── secrets.yml ├── cron.yaml ├── db ├── migrate │ ├── 20150609101847_create_setups.rb │ ├── 20150611114606_add_minutes_to_setup.rb │ ├── 20150612114153_add_next_to_setup.rb │ ├── 20150612115559_create_recommendations.rb │ ├── 20150615090544_add_password_to_setup.rb │ ├── 20150930092546_add_s3conf_to_setup.rb │ ├── 20150930110321_add_processed_to_setup.rb │ ├── 20150930131716_create_amis.rb │ ├── 20160531154327_create_instances.rb │ ├── 20160531154702_create_reserved_instances.rb │ ├── 20160531155006_create_modifications.rb │ ├── 20160601070400_add_affinity_to_setup.rb │ ├── 20160601093449_add_counts_to_recommendation.rb │ ├── 20160601145048_add_offering_to_reserved_instance.rb │ ├── 20160616131916_add_refresh_to_setup.rb │ ├── 20160616135351_create_summaries.rb │ └── 20160616142650_create_recommendation_caches.rb ├── schema.rb └── seeds.rb ├── lib ├── assets │ └── .keep ├── aws_common.rb └── tasks │ └── .keep ├── log └── .keep ├── public ├── 404.html ├── 422.html ├── 500.html ├── assets │ ├── .sprockets-manifest-063585aea31bebddb7106e9f3aa4a334.json │ ├── application-16bbbe34e9335a2d2f54086ea22499ea4ad5eab75357accd499959d1b147c540.js │ ├── application-94e001c2897085d55a9d51b0ee3e758cb1914f5f1d4448aefbd44544350e7a42.css │ ├── application-da073b160e591389edb8450c5a2b0862e1a7589d9fc7e727870c29c925805ecb.css │ ├── fontawesome-webfont-4f1f9ffe01469bbd03b254ec563c304dd92276514110364148cb7ffdd75d3297.svg │ ├── fontawesome-webfont-66db52b456efe7e29cec11fa09421d03cb09e37ed1b567307ec0444fd605ce31.woff │ ├── fontawesome-webfont-9f8288933d2c87ab3cdbdbda5c9fa1e1e139b01c7c1d1983f91a13be85173975.eot │ ├── fontawesome-webfont-c2a9333b008247abd42354df966498b4c2f1aa51a10b7e178a4f5df2edea4ce1.ttf │ ├── glyphicons-halflings-regular-62fcbc4796f99217282f30c654764f572d9bfd9df7de9ce1e37922fa3caf8124.eot │ ├── glyphicons-halflings-regular-63faf0af44a428f182686f0d924bb30e369a9549630c7b98a969394f58431067.woff │ ├── glyphicons-halflings-regular-cef3dffcef386be2c8d1307761717e2eb9f43c151f2da9f1647e9d454abf13a3.svg │ ├── glyphicons-halflings-regular-e27b969ef04fed3b39000b7b977e602d6e6a2b1c8c0d618bebf6dd875243ea3c.ttf │ └── twitter │ │ └── bootstrap │ │ ├── glyphicons-halflings-d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de.png │ │ └── glyphicons-halflings-white-f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4.png ├── favicon.ico ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ ├── glyphicons-halflings-regular.woff.1 │ └── glyphicons-halflings-regular.woff2 └── robots.txt ├── reservedinstances.cform ├── test ├── controllers │ └── .keep ├── fixtures │ ├── .keep │ ├── instances.yml │ ├── modifications.yml │ ├── recommendation_caches.yml │ ├── recommendations.yml │ ├── reserved_instances.yml │ ├── setups.yml │ └── summaries.yml ├── helpers │ └── .keep ├── integration │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── instance_test.rb │ ├── modification_test.rb │ ├── recommendation_cache_test.rb │ ├── recommendation_test.rb │ ├── reserved_instance_test.rb │ ├── setup_test.rb │ └── summary_test.rb └── test_helper.rb └── vendor └── assets ├── javascripts └── .keep └── stylesheets └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | !/log/.keep 17 | /tmp 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | 4 | gem 'aws-sdk', '~> 2.0.48' 5 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 6 | gem 'rails', '4.2.1' 7 | # Use sqlite3 as the database for Active Record 8 | gem 'sqlite3' 9 | gem 'mysql2', '~> 0.3.13' 10 | # Use SCSS for stylesheets 11 | gem 'sass-rails', '~> 5.0' 12 | # Use Uglifier as compressor for JavaScript assets 13 | gem 'uglifier', '>= 1.3.0' 14 | # Use CoffeeScript for .coffee assets and views 15 | gem 'coffee-rails', '~> 4.1.0' 16 | # See https://github.com/rails/execjs#readme for more supported runtimes 17 | gem 'therubyracer', platforms: :ruby 18 | gem 'less-rails' 19 | gem 'twitter-bootstrap-rails' 20 | gem 'bcrypt' 21 | 22 | gem 'rubyzip' 23 | 24 | # Use jquery as the JavaScript library 25 | gem 'jquery-rails' 26 | # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks 27 | gem 'turbolinks' 28 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 29 | gem 'jbuilder', '~> 2.0' 30 | # bundle exec rake doc:rails generates the API under doc/api. 31 | gem 'sdoc', '~> 0.4.0', group: :doc 32 | 33 | # Use ActiveModel has_secure_password 34 | # gem 'bcrypt', '~> 3.1.7' 35 | 36 | # Use Unicorn as the app server 37 | # gem 'unicorn' 38 | 39 | # Use Capistrano for deployment 40 | # gem 'capistrano-rails', group: :development 41 | 42 | group :development, :test do 43 | # Call 'debugger' anywhere in the code to stop execution and get a debugger console 44 | gem 'debugger' 45 | 46 | # Access an IRB console on exception pages or by using <%= console %> in views 47 | gem 'web-console', '~> 2.0' 48 | 49 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 50 | gem 'spring' 51 | end 52 | 53 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.1) 5 | actionpack (= 4.2.1) 6 | actionview (= 4.2.1) 7 | activejob (= 4.2.1) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.1) 11 | actionview (= 4.2.1) 12 | activesupport (= 4.2.1) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 17 | actionview (4.2.1) 18 | activesupport (= 4.2.1) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 23 | activejob (4.2.1) 24 | activesupport (= 4.2.1) 25 | globalid (>= 0.3.0) 26 | activemodel (4.2.1) 27 | activesupport (= 4.2.1) 28 | builder (~> 3.1) 29 | activerecord (4.2.1) 30 | activemodel (= 4.2.1) 31 | activesupport (= 4.2.1) 32 | arel (~> 6.0) 33 | activesupport (4.2.1) 34 | i18n (~> 0.7) 35 | json (~> 1.7, >= 1.7.7) 36 | minitest (~> 5.1) 37 | thread_safe (~> 0.3, >= 0.3.4) 38 | tzinfo (~> 1.1) 39 | arel (6.0.3) 40 | aws-sdk (2.0.48) 41 | aws-sdk-resources (= 2.0.48) 42 | aws-sdk-core (2.0.48) 43 | builder (~> 3.0) 44 | jmespath (~> 1.0) 45 | multi_json (~> 1.0) 46 | aws-sdk-resources (2.0.48) 47 | aws-sdk-core (= 2.0.48) 48 | bcrypt (3.1.10) 49 | binding_of_caller (0.7.2) 50 | debug_inspector (>= 0.0.1) 51 | builder (3.2.2) 52 | coffee-rails (4.1.0) 53 | coffee-script (>= 2.2.0) 54 | railties (>= 4.0.0, < 5.0) 55 | coffee-script (2.4.1) 56 | coffee-script-source 57 | execjs 58 | coffee-script-source (1.9.1.1) 59 | columnize (0.9.0) 60 | commonjs (0.2.7) 61 | debug_inspector (0.0.2) 62 | debugger (1.6.8) 63 | columnize (>= 0.3.1) 64 | debugger-linecache (~> 1.2.0) 65 | debugger-ruby_core_source (~> 1.3.5) 66 | debugger-linecache (1.2.0) 67 | debugger-ruby_core_source (1.3.8) 68 | erubis (2.7.0) 69 | execjs (2.6.0) 70 | globalid (0.3.6) 71 | activesupport (>= 4.1.0) 72 | i18n (0.7.0) 73 | jbuilder (2.3.2) 74 | activesupport (>= 3.0.0, < 5) 75 | multi_json (~> 1.2) 76 | jmespath (1.1.3) 77 | jquery-rails (4.0.5) 78 | rails-dom-testing (~> 1.0) 79 | railties (>= 4.2.0) 80 | thor (>= 0.14, < 2.0) 81 | json (1.8.3) 82 | less (2.6.0) 83 | commonjs (~> 0.2.7) 84 | less-rails (2.7.0) 85 | actionpack (>= 4.0) 86 | less (~> 2.6.0) 87 | sprockets (> 2, < 4) 88 | tilt 89 | libv8 (3.16.14.11) 90 | loofah (2.0.3) 91 | nokogiri (>= 1.5.9) 92 | mail (2.6.3) 93 | mime-types (>= 1.16, < 3) 94 | mime-types (2.6.2) 95 | mini_portile (0.6.2) 96 | minitest (5.8.1) 97 | multi_json (1.11.2) 98 | mysql2 (0.3.20) 99 | nokogiri (1.6.6.2) 100 | mini_portile (~> 0.6.0) 101 | rack (1.6.4) 102 | rack-test (0.6.3) 103 | rack (>= 1.0) 104 | rails (4.2.1) 105 | actionmailer (= 4.2.1) 106 | actionpack (= 4.2.1) 107 | actionview (= 4.2.1) 108 | activejob (= 4.2.1) 109 | activemodel (= 4.2.1) 110 | activerecord (= 4.2.1) 111 | activesupport (= 4.2.1) 112 | bundler (>= 1.3.0, < 2.0) 113 | railties (= 4.2.1) 114 | sprockets-rails 115 | rails-deprecated_sanitizer (1.0.3) 116 | activesupport (>= 4.2.0.alpha) 117 | rails-dom-testing (1.0.7) 118 | activesupport (>= 4.2.0.beta, < 5.0) 119 | nokogiri (~> 1.6.0) 120 | rails-deprecated_sanitizer (>= 1.0.1) 121 | rails-html-sanitizer (1.0.2) 122 | loofah (~> 2.0) 123 | railties (4.2.1) 124 | actionpack (= 4.2.1) 125 | activesupport (= 4.2.1) 126 | rake (>= 0.8.7) 127 | thor (>= 0.18.1, < 2.0) 128 | rake (10.4.2) 129 | rdoc (4.2.0) 130 | json (~> 1.4) 131 | ref (2.0.0) 132 | rubyzip (1.1.7) 133 | sass (3.4.18) 134 | sass-rails (5.0.4) 135 | railties (>= 4.0.0, < 5.0) 136 | sass (~> 3.1) 137 | sprockets (>= 2.8, < 4.0) 138 | sprockets-rails (>= 2.0, < 4.0) 139 | tilt (>= 1.1, < 3) 140 | sdoc (0.4.1) 141 | json (~> 1.7, >= 1.7.7) 142 | rdoc (~> 4.0) 143 | spring (1.4.0) 144 | sprockets (3.3.5) 145 | rack (> 1, < 3) 146 | sprockets-rails (2.3.3) 147 | actionpack (>= 3.0) 148 | activesupport (>= 3.0) 149 | sprockets (>= 2.8, < 4.0) 150 | sqlite3 (1.3.10) 151 | therubyracer (0.12.2) 152 | libv8 (~> 3.16.14.0) 153 | ref 154 | thor (0.19.1) 155 | thread_safe (0.3.5) 156 | tilt (2.0.1) 157 | turbolinks (2.5.3) 158 | coffee-rails 159 | twitter-bootstrap-rails (3.2.0) 160 | actionpack (~> 4.1) 161 | execjs (~> 2.2) 162 | rails (~> 4.1) 163 | railties (~> 4.1) 164 | tzinfo (1.2.2) 165 | thread_safe (~> 0.1) 166 | uglifier (2.7.2) 167 | execjs (>= 0.3.0) 168 | json (>= 1.8.0) 169 | web-console (2.2.1) 170 | activemodel (>= 4.0) 171 | binding_of_caller (>= 0.7.2) 172 | railties (>= 4.0) 173 | sprockets-rails (>= 2.0, < 4.0) 174 | 175 | PLATFORMS 176 | ruby 177 | 178 | DEPENDENCIES 179 | aws-sdk (~> 2.0.48) 180 | bcrypt 181 | coffee-rails (~> 4.1.0) 182 | debugger 183 | jbuilder (~> 2.0) 184 | jquery-rails 185 | less-rails 186 | mysql2 (~> 0.3.13) 187 | rails (= 4.2.1) 188 | rubyzip 189 | sass-rails (~> 5.0) 190 | sdoc (~> 0.4.0) 191 | spring 192 | sqlite3 193 | therubyracer 194 | turbolinks 195 | twitter-bootstrap-rails 196 | uglifier (>= 1.3.0) 197 | web-console (~> 2.0) 198 | 199 | BUNDLED WITH 200 | 1.10.3 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reserved Instances Tool 2 | ## Introduction 3 | This tool is to manage your Reserved Instances in AWS across all your linked accounts. The tool reads your live configuration (instances and reserved instances) in all your accounts using the AWS API, and produces a set of recommendations to optimize your RI usage. The tool can apply the modifications in your RIs (changing the availability zone, instance type or network allocation) for you, and you can configure it to apply all the recommendations automatically every so often. 4 | 5 | > **Note:** If you were using the version 2.1 or previous you should modify your IAM roles in all the accounts to add the permission "ec2:DescribeAvailabilityZones" to all of them. 6 | 7 | > **Note:** Now the tool supports the instance types "Windows with SQL Standard", "Windows with SQL Web", "Windows with SQL Enterprise", "RHEL" and "SLES". If you're using any of these instance types you should configure the DBR file. 8 | 9 | ## Installation 10 | To install the tool, you should create the necessary roles to let the tool run the AWS API calls. Then you can launch the tool using AWS Beanstalk, I've created a CloudFormation file to facilitate the deployment of the tool. 11 | 12 | You can use the tool with one account, or a group of accounts (linked accounts). 13 | 14 | If you have multiple accounts, you can deploy the tool in any of them (let's call the account where you're going to deploy the tool account1), then you should create a role in each account. Go to the AWS Console, and select the Identity & Access Management (IAM) service. Select Roles and create a new one. 15 | 16 | For each account where you're not going to deploy the tool (so for all but account1), name the role "reservedinstances", select the option "Role for Cross-Account Access"and select "Provide access between AWS accounts you own". Introduce the account id for account1. Create the role and then attach this policy to it: 17 | 18 | ```json 19 | { 20 | "Version": "2012-10-17", 21 | "Statement": [ 22 | { 23 | "Sid": "Stmt1433771637000", 24 | "Effect": "Allow", 25 | "Action": [ 26 | "ec2:DescribeInstanceStatus", 27 | "ec2:DescribeInstances", 28 | "ec2:DescribeReservedInstances", 29 | "ec2:DescribeReservedInstancesListings", 30 | "ec2:DescribeReservedInstancesModifications", 31 | "ec2:DescribeReservedInstancesOfferings", 32 | "ec2:DescribeAccountAttributes", 33 | "ec2:ModifyReservedInstances", 34 | "ec2:DescribeAvailabilityZones" 35 | ], 36 | "Resource": [ 37 | "*" 38 | ] 39 | } 40 | ] 41 | } 42 | ``` 43 | 44 | If you only have one account, or if you have multiple accounts in the account1, create a new role. Name the role "reservedinstances", select the option "Amazon EC2" in the "AWS Service Roles" list. Create the role and then attach two policies to it, the previous and this one: 45 | 46 | ```json 47 | { 48 | "Version": "2012-10-17", 49 | "Statement": [ 50 | { 51 | "Sid": "Stmt1433772347001", 52 | "Effect": "Allow", 53 | "Action": [ 54 | "iam:ListRolePolicies", 55 | "iam:GetRolePolicy" 56 | ], 57 | "Resource": [ 58 | "arn:aws:iam:::role/reservedinstances" 59 | ] 60 | }, 61 | { 62 | "Sid": "Stmt1433772347000", 63 | "Effect": "Allow", 64 | "Action": [ 65 | "sts:AssumeRole" 66 | ], 67 | "Resource": [ 68 | "arn:aws:iam:::role/reservedinstances", 69 | "arn:aws:iam:::role/reservedinstances", 70 | "arn:aws:iam:::role/reservedinstances", 71 | "arn:aws:iam:::role/reservedinstances", 72 | "arn:aws:iam:::role/reservedinstances" 73 | ] 74 | }, 75 | { 76 | "Sid": "QueueAccess", 77 | "Action": [ 78 | "sqs:ChangeMessageVisibility", 79 | "sqs:DeleteMessage", 80 | "sqs:ReceiveMessage", 81 | "sqs:SendMessage" 82 | ], 83 | "Effect": "Allow", 84 | "Resource": "arn:aws:sqs:::ritoolqueue" 85 | }, 86 | { 87 | "Sid": "MetricsAccess", 88 | "Action": [ 89 | "cloudwatch:PutMetricData" 90 | ], 91 | "Effect": "Allow", 92 | "Resource": "*" 93 | }, 94 | { 95 | "Sid": "BucketAccess", 96 | "Action": [ 97 | "s3:Get*", 98 | "s3:List*", 99 | "s3:PutObject" 100 | ], 101 | "Effect": "Allow", 102 | "Resource": [ 103 | "arn:aws:s3:::elasticbeanstalk-*-/*", 104 | "arn:aws:s3:::elasticbeanstalk-*--*/*" 105 | ] 106 | }, 107 | { 108 | "Sid": "DynamoPeriodicTasks", 109 | "Action": [ 110 | "dynamodb:BatchGetItem", 111 | "dynamodb:BatchWriteItem", 112 | "dynamodb:DeleteItem", 113 | "dynamodb:GetItem", 114 | "dynamodb:PutItem", 115 | "dynamodb:Query", 116 | "dynamodb:Scan", 117 | "dynamodb:UpdateItem" 118 | ], 119 | "Effect": "Allow", 120 | "Resource": [ 121 | "arn:aws:dynamodb:*::table/*-stack-AWSEBWorkerCronLeaderRegistry*" 122 | ] 123 | } 124 | ] 125 | } 126 | ``` 127 | 128 | You can add as many accounts as you need to the policy. You can use all your linked accounts or a subset of them. 129 | 130 | You also need: 131 | 132 | * 1 VPC 133 | * 2 Subnets 134 | * 1 KeyPair 135 | * 1 SSL Cert ARN (http://docs.aws.amazon.com/IAM/latest/UserGuide/ManagingServerCerts.html) 136 | * 1 Rails Secret Key - You can generate it in any computer with Ruby installed, just run: 137 | * $ irb 138 | * >> require 'securerandom' 139 | * >> SecureRandom.hex(64) 140 | * Download the CloudFormation template from here: https://raw.githubusercontent.com/jros2300/reservedinstances/master/reservedinstances.cform 141 | 142 | You need also this application in S3, you can download the last version and upload to any S3 bucket, or you can use the default values and use the one I maintain. 143 | 144 | Then you should go to the console in the account1, and select the service CloudFormation. Create a new stack and upload the template, complete all the parameters and create the stack. Once the stack is created you can access the application using the URL you can find in the Output of the stack. 145 | 146 | ## DBR 147 | 148 | If you have any instance of these types: "Windows with SQL Standard", "Windows with SQL Web", "Windows with SQL Enterprise", "RHEL" and "SLES", please continue reading this section and configure the system to read your DBR files. 149 | 150 | In first place you should configure the DBR (Detailed Billing Record) file generation following these instructions: http://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/detailed-billing-reports.html#turnonreports (selecting at least the "Detailed billing report with resources and tags" option). 151 | 152 | Add a new policy to the account1 role "reservedinstances" changing the bucket name for the bucket configured to store the DBR: 153 | 154 | ```json 155 | { 156 | "Version": "2012-10-17", 157 | "Statement": [ 158 | { 159 | "Effect": "Allow", 160 | "Action": [ 161 | "s3:Get*", 162 | "s3:List*" 163 | ], 164 | "Resource": [ 165 | "arn:aws:s3:::/*" 166 | ] 167 | }, 168 | { 169 | "Effect": "Allow", 170 | "Action": [ 171 | "s3:List*", 172 | "s3:Get*" 173 | ], 174 | "Resource": [ 175 | "arn:aws:s3:::" 176 | ] 177 | } 178 | ] 179 | } 180 | ``` 181 | 182 | Finally, you should configure the bucket name in the application in the Setup tab and select the checkbox to process the DBR. 183 | 184 | Wait two hours after the action to let the system process the first DBR file. 185 | 186 | ## Usage 187 | You can access the tool using the URL you can find in the output of the Stack. You should use the default password you put in the parameters of the Stack (you can leave the username blank). 188 | 189 | Once in the tool you should configure it: 190 | 191 | * Regions in use: Select all the regions you're using in all your accounts. You can select all the regions, but you can filter out the regions you're not using to improve the performance of the tool 192 | * Automatically apply recommendations each: If you set this to 0, then there is not going to be any automatic mofification of the RIs. If you set this to any other number (more than 30), the application is going to apply all the recommendations automatically periodically on that interval. 193 | * Change Password: You can introduce a new password for the tool 194 | 195 | The tool updates the information from the accounts using the API calls each 5 minutes (it can take up to 5 minutes until you're able to see the first analysis when you deploy the application). 196 | 197 | In the tool there are several options: 198 | 199 | * Instances: You can see all your running instances in all the accounts, you can search by any field 200 | * Reserved Instances: You can see all your reserved instances in all the accounts 201 | * Summary: You can see a summary of your instances and reserved instances. You can see where you have more instances than reserved instances (yellow), and where you have more RIs than instances (red) 202 | * Recommendations: You can see all the recommended modifications in your RIs, you can select all the modifications or a subset of them and apply the modifications to your RIs from the tool 203 | * Log: You'll see all the recommended modifications to your RIs applied by the tool, in the Recommendations option, or automatically by the periodic task 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require twitter/bootstrap 16 | //= require turbolinks 17 | //= require_tree . 18 | -------------------------------------------------------------------------------- /app/assets/javascripts/bootstrap-table.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * bootstrap-table - v1.8.1 - 2015-05-29 3 | * https://github.com/wenzhixin/bootstrap-table 4 | * Copyright (c) 2015 zhixin wen 5 | * Licensed MIT License 6 | */ 7 | !function(a){"use strict";var b=37,c=null,d="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==",e="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7XQMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC",f="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ",g=function(a){var b=arguments,c=!0,d=1;return a=a.replace(/%s/g,function(){var a=b[d++];return"undefined"==typeof a?(c=!1,""):a}),c?a:""},h=function(b,c,d,e){var f="";return a.each(b,function(a,b){return b[c]===e?(f=b[d],!1):!0}),f},i=function(b,c){var d=-1;return a.each(b,function(a,b){return b.field===c?(d=a,!1):!0}),d},j=function(){if(null===c){var b,d,e=a("

").addClass("fixed-table-scroll-inner"),f=a("

").addClass("fixed-table-scroll-outer");f.append(e),a("body").append(f),b=e[0].offsetWidth,f.css("overflow","scroll"),d=e[0].offsetWidth,b===d&&(d=f[0].clientWidth),f.remove(),c=b-d}return c},k=function(b,c,d,e){var f=c;if("string"==typeof c){var h=c.split(".");h.length>1?(f=window,a.each(h,function(a,b){f=f[b]})):f=window[c]}return"object"==typeof f?f:"function"==typeof f?f.apply(b,d):!f&&"string"==typeof c&&g.apply(this,[c].concat(d))?g.apply(this,[c].concat(d)):e},l=function(a){return"string"==typeof a?a.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"):a},m=function(b){var c=0;return b.children().each(function(){c','
',"top"===this.options.paginationVAlign||"both"===this.options.paginationVAlign?'
':"",'
','
','
','
',this.options.formatLoadingMessage(),"
","
",'',"bottom"===this.options.paginationVAlign||"both"===this.options.paginationVAlign?'
':"","
","
"].join("")),this.$container.insertAfter(this.$el),this.$tableContainer=this.$container.find(".fixed-table-container"),this.$tableHeader=this.$container.find(".fixed-table-header"),this.$tableBody=this.$container.find(".fixed-table-body"),this.$tableLoading=this.$container.find(".fixed-table-loading"),this.$tableFooter=this.$container.find(".fixed-table-footer"),this.$toolbar=this.$container.find(".fixed-table-toolbar"),this.$pagination=this.$container.find(".fixed-table-pagination"),this.$tableBody.append(this.$el),this.$container.after('
'),this.$el.addClass(this.options.classes),this.options.striped&&this.$el.addClass("table-striped"),-1!==a.inArray("table-no-bordered",this.options.classes.split(" "))&&this.$tableContainer.addClass("table-no-bordered")},o.prototype.initTable=function(){var b=this,c=[],d=[];this.$header=this.$el.find("thead"),this.$header.length||(this.$header=a("").appendTo(this.$el)),this.$header.find("tr").length||this.$header.append(""),this.$header.find("th").each(function(){var b=a.extend({},{title:a(this).html(),"class":a(this).attr("class")},a(this).data());c.push(b)}),this.options.columns=a.extend(!0,[],c,this.options.columns),a.each(this.options.columns,function(c,d){b.options.columns[c]=a.extend({},o.COLUMN_DEFAULTS,{field:c},d)}),this.options.data.length||(this.$el.find("tbody tr").each(function(){var c={};c._id=a(this).attr("id"),c._class=a(this).attr("class"),c._data=n(a(this).data()),a(this).find("td").each(function(d){var e=b.options.columns[d].field;c[e]=a(this).html(),c["_"+e+"_id"]=a(this).attr("id"),c["_"+e+"_class"]=a(this).attr("class"),c["_"+e+"_rowspan"]=a(this).attr("rowspan"),c["_"+e+"_data"]=n(a(this).data())}),d.push(c)}),this.options.data=d)},o.prototype.initHeader=function(){var c=this,d=[],e=[];this.header={fields:[],styles:[],classes:[],formatters:[],events:[],sorters:[],sortNames:[],cellStyles:[],clickToSelects:[],searchables:[]},!this.options.cardView&&this.options.detailView&&(e.push('
'),d.push({})),a.each(this.options.columns,function(a,b){var f="",h="",i="",j="",k=g(' class="%s"',b["class"]),l=(c.options.sortOrder||b.order,"px"),m=b.width;return b.visible?void((!c.options.cardView||b.cardVisible)&&(void 0===b.width||c.options.cardView||"string"==typeof b.width&&-1!==b.width.indexOf("%")&&(l="%"),b.width&&"string"==typeof b.width&&(m=b.width.replace("%","").replace("px","")),h=g("text-align: %s; ",b.halign?b.halign:b.align),i=g("text-align: %s; ",b.align),j=g("vertical-align: %s; ",b.valign),j+=g("width: %s%s; ",b.checkbox||b.radio?36:m,l),d.push(b),c.header.fields.push(b.field),c.header.styles.push(i+j),c.header.classes.push(k),c.header.formatters.push(b.formatter),c.header.events.push(b.events),c.header.sorters.push(b.sorter),c.header.sortNames.push(b.sortName),c.header.cellStyles.push(b.cellStyle),c.header.clickToSelects.push(b.clickToSelect),c.header.searchables.push(b.searchable),e.push(""),e.push(g('
',c.options.sortable&&b.sortable?"sortable":"")),f=b.title,b.checkbox&&(!c.options.singleSelect&&c.options.checkboxHeader&&(f=''),c.header.stateField=b.field),b.radio&&(f="",c.header.stateField=b.field,c.options.singleSelect=!0),e.push(f),e.push("
"),e.push('
'),e.push(""),e.push(""))):void(b.field===c.options.sortName&&c.header.fields.push(b.field))}),this.$header.find("tr").html(e.join("")),this.$header.find("th").each(function(b){a(this).data(d[b])}),this.$container.off("click",".th-inner").on("click",".th-inner",function(b){c.options.sortable&&a(this).parent().data().sortable&&c.onSort(b)}),!this.options.showHeader||this.options.cardView?(this.$header.hide(),this.$tableHeader.hide(),this.$tableLoading.css("top",0)):(this.$header.show(),this.$tableHeader.show(),this.$tableLoading.css("top",b+"px"),this.getCaretHtml()),this.$selectAll=this.$header.find('[name="btSelectAll"]'),this.$container.off("click",'[name="btSelectAll"]').on("click",'[name="btSelectAll"]',function(){var b=a(this).prop("checked");c[b?"checkAll":"uncheckAll"]()})},o.prototype.initFooter=function(){!this.options.showFooter||this.options.cardView?this.$tableFooter.hide():this.$tableFooter.show()},o.prototype.initData=function(a,b){this.data="append"===b?this.data.concat(a):"prepend"===b?[].concat(a).concat(this.data):a||this.options.data,this.options.data="append"===b?this.options.data.concat(a):"prepend"===b?[].concat(a).concat(this.options.data):this.data,"server"!==this.options.sidePagination&&this.initSort()},o.prototype.initSort=function(){var b=this,c=this.options.sortName,d="desc"===this.options.sortOrder?-1:1,e=a.inArray(this.options.sortName,this.header.fields);-1!==e&&this.data.sort(function(f,g){b.header.sortNames[e]&&(c=b.header.sortNames[e]);var h=f[c],i=g[c],j=k(b.header,b.header.sorters[e],[h,i]);return void 0!==j?d*j:((void 0===h||null===h)&&(h=""),(void 0===i||null===i)&&(i=""),a.isNumeric(h)&&a.isNumeric(i)?(h=parseFloat(h),i=parseFloat(i),i>h?-1*d:d):h===i?0:("string"!=typeof h&&(h=h.toString()),-1===h.localeCompare(i)?-1*d:d))})},o.prototype.onSort=function(b){var c=a(b.currentTarget).parent(),d=this.$header.find("th").eq(c.index());return this.$header.add(this.$header_).find("span.order").remove(),this.options.sortName===c.data("field")?this.options.sortOrder="asc"===this.options.sortOrder?"desc":"asc":(this.options.sortName=c.data("field"),this.options.sortOrder="asc"===c.data("order")?"desc":"asc"),this.trigger("sort",this.options.sortName,this.options.sortOrder),c.add(d).data("order",this.options.sortOrder),this.getCaretHtml(),"server"===this.options.sidePagination?void this.initServer():(this.initSort(),void this.initBody())},o.prototype.initToolbar=function(){var b,c,d=this,e=[],f=0,h=0;this.$toolbar.html(""),"string"==typeof this.options.toolbar&&a(g('
',this.options.toolbarAlign)).appendTo(this.$toolbar).append(a(this.options.toolbar)),e=[g('
',this.options.buttonsAlign,this.options.buttonsAlign)],"string"==typeof this.options.icons&&(this.options.icons=k(null,this.options.icons)),this.options.showPaginationSwitch&&e.push(g('"),this.options.showRefresh&&e.push(g('"),this.options.showToggle&&e.push(g('"),this.options.showColumns&&(e.push(g('
',this.options.formatColumns()),'",'","
")),e.push("
"),(this.showToolbar||e.length>2)&&this.$toolbar.append(e.join("")),this.options.showPaginationSwitch&&this.$toolbar.find('button[name="paginationSwitch"]').off("click").on("click",a.proxy(this.togglePagination,this)),this.options.showRefresh&&this.$toolbar.find('button[name="refresh"]').off("click").on("click",a.proxy(this.refresh,this)),this.options.showToggle&&this.$toolbar.find('button[name="toggle"]').off("click").on("click",function(){d.toggleView()}),this.options.showColumns&&(b=this.$toolbar.find(".keep-open"),h<=this.options.minimumCountColumns&&b.find("input").prop("disabled",!0),b.find("li").off("click").on("click",function(a){a.stopImmediatePropagation()}),b.find("input").off("click").on("click",function(){var b=a(this);d.toggleColumn(i(d.options.columns,a(this).data("field")),b.prop("checked"),!1),d.trigger("column-switch",a(this).data("field"),b.prop("checked"))})),this.options.search&&(e=[],e.push('"),this.$toolbar.append(e.join("")),c=this.$toolbar.find(".search input"),c.off("keyup drop").on("keyup drop",function(a){clearTimeout(f),f=setTimeout(function(){d.onSearch(a)},d.options.searchTimeOut)}),""!==this.options.searchText&&(c.val(this.options.searchText),clearTimeout(f),f=setTimeout(function(){c.trigger("keyup")},d.options.searchTimeOut)))},o.prototype.onSearch=function(b){var c=a.trim(a(b.currentTarget).val());this.options.trimOnSearch&&a(b.currentTarget).val()!==c&&a(b.currentTarget).val(c),c!==this.searchText&&(this.searchText=c,this.options.pageNumber=1,this.initSearch(),this.updatePagination(),this.trigger("search",c))},o.prototype.initSearch=function(){var b=this;if("server"!==this.options.sidePagination){var c=this.searchText&&this.searchText.toLowerCase(),d=a.isEmptyObject(this.filterColumns)?null:this.filterColumns;this.data=d?a.grep(this.options.data,function(a){for(var b in d)if(a[b]!==d[b])return!1;return!0}):this.options.data,this.data=c?a.grep(this.data,function(d,e){for(var f in d){f=a.isNumeric(f)?parseInt(f,10):f;var g=d[f],h=b.options.columns[i(b.options.columns,f)],j=a.inArray(f,b.header.fields);g=k(h,b.header.formatters[j],[g,d,e],g);var l=a.inArray(f,b.header.fields);if(-1!==l&&b.header.searchables[l]&&("string"==typeof g||"number"==typeof g)&&-1!==(g+"").toLowerCase().indexOf(c))return!0}return!1}):this.data}},o.prototype.initPagination=function(){if(!this.options.pagination)return void this.$pagination.hide();this.$pagination.show();var b,c,d,e,f,h,i,j,k,l=this,m=[],n=!1,o=this.getData();if("server"!==this.options.sidePagination&&(this.options.totalRows=o.length),this.totalPages=0,this.options.totalRows){if(this.options.pageSize===this.options.formatAllRows())this.options.pageSize=this.options.totalRows,n=!0;else if(this.options.pageSize===this.options.totalRows){var p="string"==typeof this.options.pageList?this.options.pageList.replace("[","").replace("]","").replace(/ /g,"").toLowerCase().split(","):this.options.pageList;p.indexOf(this.options.formatAllRows().toLowerCase())>-1&&(n=!0)}this.totalPages=~~((this.options.totalRows-1)/this.options.pageSize)+1,this.options.totalPages=this.totalPages}this.totalPages>0&&this.options.pageNumber>this.totalPages&&(this.options.pageNumber=this.totalPages),this.pageFrom=(this.options.pageNumber-1)*this.options.pageSize+1,this.pageTo=this.options.pageNumber*this.options.pageSize,this.pageTo>this.options.totalRows&&(this.pageTo=this.options.totalRows),m.push('
','',this.options.formatShowingRows(this.pageFrom,this.pageTo,this.options.totalRows),""),m.push('');var q=[g('',"top"===this.options.paginationVAlign||"both"===this.options.paginationVAlign?"dropdown":"dropup"),'",'"),m.push(this.options.formatRecordsPerPage(q.join(""))),m.push(""),m.push("
",'"),this.$pagination.html(m.join("")),e=this.$pagination.find(".page-list a"),f=this.$pagination.find(".page-first"),h=this.$pagination.find(".page-pre"),i=this.$pagination.find(".page-next"),j=this.$pagination.find(".page-last"),k=this.$pagination.find(".page-number"),this.options.pageNumber<=1&&(f.addClass("disabled"),h.addClass("disabled")),this.options.pageNumber>=this.totalPages&&(i.addClass("disabled"),j.addClass("disabled")),this.options.smartDisplay&&(this.totalPages<=1&&this.$pagination.find("div.pagination").hide(),(r.length<2||this.options.totalRows<=r[0])&&this.$pagination.find("span.page-list").hide(),this.$pagination[this.getData().length?"show":"hide"]()),n&&(this.options.pageSize=this.options.formatAllRows()),e.off("click").on("click",a.proxy(this.onPageListChange,this)),f.off("click").on("click",a.proxy(this.onPageFirst,this)),h.off("click").on("click",a.proxy(this.onPagePre,this)),i.off("click").on("click",a.proxy(this.onPageNext,this)),j.off("click").on("click",a.proxy(this.onPageLast,this)),k.off("click").on("click",a.proxy(this.onPageNumber,this))},o.prototype.updatePagination=function(b){b&&a(b.currentTarget).hasClass("disabled")||(this.options.maintainSelected||this.resetRows(),this.initPagination(),"server"===this.options.sidePagination?this.initServer():this.initBody(),this.trigger("page-change",this.options.pageNumber,this.options.pageSize))},o.prototype.onPageListChange=function(b){var c=a(b.currentTarget);c.parent().addClass("active").siblings().removeClass("active"),this.options.pageSize=c.text().toUpperCase()===this.options.formatAllRows().toUpperCase()?this.options.formatAllRows():+c.text(),this.$toolbar.find(".page-size").text(this.options.pageSize),this.updatePagination(b)},o.prototype.onPageFirst=function(a){this.options.pageNumber=1,this.updatePagination(a)},o.prototype.onPagePre=function(a){this.options.pageNumber--,this.updatePagination(a)},o.prototype.onPageNext=function(a){this.options.pageNumber++,this.updatePagination(a)},o.prototype.onPageLast=function(a){this.options.pageNumber=this.totalPages,this.updatePagination(a)},o.prototype.onPageNumber=function(b){this.options.pageNumber!==+a(b.currentTarget).text()&&(this.options.pageNumber=+a(b.currentTarget).text(),this.updatePagination(b))},o.prototype.initBody=function(b){var c=this,d=[],e=this.getData();this.trigger("pre-body",e),this.$body=this.$el.find("tbody"),this.$body.length||(this.$body=a("").appendTo(this.$el)),this.options.pagination&&"server"!==this.options.sidePagination||(this.pageFrom=1,this.pageTo=e.length);for(var f=this.pageFrom-1;f"),this.options.cardView&&d.push(g('',this.header.fields.length)),!this.options.cardView&&this.options.detailView&&d.push("",'','',"",""),a.each(this.header.fields,function(b,e){var j="",l=m[e],p="",q={},r="",s=c.header.classes[b],t="",u="",v=c.options.columns[i(c.options.columns,e)];if(n=g('style="%s"',o.concat(c.header.styles[b]).join("; ")),l=k(v,c.header.formatters[b],[l,m,f],l),m["_"+e+"_id"]&&(r=g(' id="%s"',m["_"+e+"_id"])),m["_"+e+"_class"]&&(s=g(' class="%s"',m["_"+e+"_class"])),m["_"+e+"_rowspan"]&&(u=g(' rowspan="%s"',m["_"+e+"_rowspan"])),q=k(c.header,c.header.cellStyles[b],[l,m,f],q),q.classes&&(s=g(' class="%s"',q.classes)),q.css){var w=[];for(var x in q.css)w.push(x+": "+q.css[x]);n=g('style="%s"',w.concat(c.header.styles[b]).join("; "))}m["_"+e+"_data"]&&!a.isEmptyObject(m["_"+e+"_data"])&&a.each(m["_"+e+"_data"],function(a,b){"index"!==a&&(t+=g(' data-%s="%s"',a,b))}),v.checkbox||v.radio?(p=v.checkbox?"checkbox":p,p=v.radio?"radio":p,j=[c.options.cardView?'
':'',"",c.options.cardView?"
":""].join(""),m[c.header.stateField]=l===!0||l&&l.checked):(l="undefined"==typeof l||null===l?c.options.undefinedText:l,j=c.options.cardView?['
',c.options.showHeader?g('%s',n,h(c.options.columns,"field","title",e)):"",g('%s',l),"
"].join(""):[g("",r,s,n,t,u),l,""].join(""),c.options.cardView&&c.options.smartDisplay&&""===l&&(j="")),d.push(j)}),this.options.cardView&&d.push(""),d.push("")}d.length||d.push('',g('%s',this.$header.find("th").length,this.options.formatNoMatches()),""),this.$body.html(d.join("")),b||this.scrollTo(0),this.$body.find("> tr > td").off("click").on("click",function(){var b=a(this),d=b.parent(),e=c.data[d.data("index")],f=b[0].cellIndex,h=c.$header.find("th:eq("+f+")"),i=h.data("field"),j=e[i];c.trigger("click-cell",i,j,e,b),c.trigger("click-row",e,d),c.options.clickToSelect&&c.header.clickToSelects[d.children().index(a(this))]&&d.find(g('[name="%s"]',c.options.selectItemName))[0].click()}),this.$body.find("> tr > td").off("dblclick").on("dblclick",function(){var b=a(this),d=b.parent(),e=c.data[d.data("index")],f=b[0].cellIndex,g=c.$header.find("th:eq("+f+")"),h=g.data("field"),i=e[h];c.trigger("dbl-click-cell",h,i,e,b),c.trigger("dbl-click-row",e,d)}),this.$body.find("> tr > td > .detail-icon").off("click").on("click",function(){var b=a(this),d=b.parent().parent(),e=d.data("index"),f=c.options.data[e];d.next().is("tr.detail-view")?(b.find("i").attr("class","glyphicon glyphicon-plus icon-plus"),d.next().remove(),c.trigger("collapse-row",e,f)):(b.find("i").attr("class","glyphicon glyphicon-minus icon-minus"),d.after(g('%s',d.find("td").length,k(c.options,c.options.detailFormatter,[e,f],""))),c.trigger("expand-row",e,f,d.next().find("td"))),c.resetView()}),this.$selectItem=this.$body.find(g('[name="%s"]',this.options.selectItemName)),this.$selectItem.off("click").on("click",function(b){b.stopImmediatePropagation();var d=a(this).prop("checked"),e=c.data[a(this).data("index")];e[c.header.stateField]=d,c.options.singleSelect&&(c.$selectItem.not(this).each(function(){c.data[a(this).data("index")][c.header.stateField]=!1}),c.$selectItem.filter(":checked").not(this).prop("checked",!1)),c.updateSelected(),c.trigger(d?"check":"uncheck",e)}),a.each(this.header.events,function(b,d){if(d){"string"==typeof d&&(d=k(null,d)),!c.options.cardView&&c.options.detailView&&(b+=1);for(var e in d)c.$body.find("tr").each(function(){var f=a(this),g=f.find(c.options.cardView?".card-view":"td").eq(b),h=e.indexOf(" "),i=e.substring(0,h),j=e.substring(h+1),k=d[e];g.find(j).off(i).on(i,function(a){var d=f.data("index"),e=c.data[d],g=e[c.header.fields[b]];k.apply(this,[a,g,e,d])})})}}),this.updateSelected(),this.resetView(),this.trigger("post-body")},o.prototype.initServer=function(b,c){var d,e=this,f={},g={pageSize:this.options.pageSize===this.options.formatAllRows()?this.options.totalRows:this.options.pageSize,pageNumber:this.options.pageNumber,searchText:this.searchText,sortName:this.options.sortName,sortOrder:this.options.sortOrder};(this.options.url||this.options.ajax)&&("limit"===this.options.queryParamsType&&(g={search:g.searchText,sort:g.sortName,order:g.sortOrder},this.options.pagination&&(g.limit=this.options.pageSize===this.options.formatAllRows()?this.options.totalRows:this.options.pageSize,g.offset=this.options.pageSize===this.options.formatAllRows()?0:this.options.pageSize*(this.options.pageNumber-1))),a.isEmptyObject(this.filterColumnsPartial)||(g.filter=JSON.stringify(this.filterColumnsPartial,null)),f=k(this.options,this.options.queryParams,[g],f),a.extend(f,c||{}),f!==!1&&(b||this.$tableLoading.show(),d=a.extend({},k(null,this.options.ajaxOptions),{type:this.options.method,url:this.options.url,data:"application/json"===this.options.contentType&&"post"===this.options.method?JSON.stringify(f):f,cache:this.options.cache,contentType:this.options.contentType,dataType:this.options.dataType,success:function(a){a=k(e.options,e.options.responseHandler,[a],a),e.load(a),e.trigger("load-success",a)},error:function(a){e.trigger("load-error",a.status)},complete:function(){b||e.$tableLoading.hide()}}),this.options.ajax?k(this,this.options.ajax,[d],null):a.ajax(d)))},o.prototype.getCaretHtml=function(){var b=this;a.each(this.$header.find("th"),function(c,g){a(g).data("field")===b.options.sortName?a(g).find(".sortable").css("background-image","url("+("desc"===b.options.sortOrder?f:d)+")"):a(g).find(".sortable").css("background-image","url("+e+")")})},o.prototype.updateSelected=function(){var b=this.$selectItem.filter(":enabled").length===this.$selectItem.filter(":enabled").filter(":checked").length;this.$selectAll.add(this.$selectAll_).prop("checked",b),this.$selectItem.each(function(){a(this).parents("tr")[a(this).prop("checked")?"addClass":"removeClass"]("selected")})},o.prototype.updateRows=function(){var b=this;this.$selectItem.each(function(){b.data[a(this).data("index")][b.header.stateField]=a(this).prop("checked")})},o.prototype.resetRows=function(){var b=this;a.each(this.data,function(a,c){b.$selectAll.prop("checked",!1),b.$selectItem.prop("checked",!1),c[b.header.stateField]=!1})},o.prototype.trigger=function(b){var c=Array.prototype.slice.call(arguments,1);b+=".bs.table",this.options[o.EVENTS[b]].apply(this.options,c),this.$el.trigger(a.Event(b),c),this.options.onAll(b,c),this.$el.trigger(a.Event("all.bs.table"),[b,c])},o.prototype.resetHeader=function(){clearTimeout(this.timeoutId_),this.timeoutId_=setTimeout(a.proxy(this.fitHeader,this),this.$el.is(":hidden")?100:0)},o.prototype.fitHeader=function(){var b,c,d=this;return d.$el.is(":hidden")?void(d.timeoutFooter_=setTimeout(a.proxy(d.fitHeader,d),100)):(b=this.$tableBody.get(0),c=b.scrollWidth>b.clientWidth&&b.scrollHeight>b.clientHeight+this.$header.height()?j():0,this.$el.css("margin-top",-this.$header.height()),this.$header_=this.$header.clone(!0,!0),this.$selectAll_=this.$header_.find('[name="btSelectAll"]'),this.$tableHeader.css({"margin-right":c}).find("table").css("width",this.$el.css("width")).html("").attr("class",this.$el.attr("class")).append(this.$header_),this.$header.find("th").each(function(b){d.$header_.find("th").eq(b).data(a(this).data())}),this.$body.find("tr:first-child:not(.no-records-found) > *").each(function(b){d.$header_.find("div.fht-cell").eq(b).width(a(this).innerWidth()) 8 | }),this.$tableBody.off("scroll").on("scroll",function(){d.$tableHeader.scrollLeft(a(this).scrollLeft())}),void d.trigger("post-header"))},o.prototype.resetFooter=function(){var b=this,c=b.getData(),d=[];this.options.showFooter&&!this.options.cardView&&(!this.options.cardView&&this.options.detailView&&d.push(""),a.each(this.options.columns,function(a,e){var f="",h="",i=g(' class="%s"',e["class"]);e.visible&&(!b.options.cardView||e.cardVisible)&&(f=g("text-align: %s; ",e.falign?e.falign:e.align),h=g("vertical-align: %s; ",e.valign),d.push(""),d.push(k(e,e.footerFormatter,[c]," ")||" "),d.push(""))}),this.$tableFooter.find("tr").html(d.join("")),clearTimeout(this.timeoutFooter_),this.timeoutFooter_=setTimeout(a.proxy(this.fitFooter,this),this.$el.is(":hidden")?100:0))},o.prototype.fitFooter=function(){var b,c,d;return clearTimeout(this.timeoutFooter_),this.$el.is(":hidden")?void(this.timeoutFooter_=setTimeout(a.proxy(this.fitFooter,this),100)):(c=this.$el.css("width"),d=c>this.$tableBody.width()?j():0,this.$tableFooter.css({"margin-right":d}).find("table").css("width",c).attr("class",this.$el.attr("class")),b=this.$tableFooter.find("td"),void this.$tableBody.find("tbody tr:first-child:not(.no-records-found) > td").each(function(c){b.eq(c).outerWidth(a(this).outerWidth())}))},o.prototype.toggleColumn=function(a,b,c){if(-1!==a&&(this.options.columns[a].visible=b,this.initHeader(),this.initSearch(),this.initPagination(),this.initBody(),this.options.showColumns)){var d=this.$toolbar.find(".keep-open input").prop("disabled",!1);c&&d.filter(g('[value="%s"]',a)).prop("checked",b),d.filter(":checked").length<=this.options.minimumCountColumns&&d.filter(":checked").prop("disabled",!0)}},o.prototype.toggleRow=function(b,c,d){-1!==b&&a(this.$body[0]).children().filter(g(c?'[data-uniqueid="%s"]':'[data-index="%s"]',b))[d?"show":"hide"]()},o.prototype.resetView=function(a){var c=0;if(a&&a.height&&(this.options.height=a.height),this.$selectAll.prop("checked",this.$selectItem.length>0&&this.$selectItem.length===this.$selectItem.filter(":checked").length),this.options.height){var d=m(this.$toolbar),e=m(this.$pagination),f=this.options.height-d-e;this.$tableContainer.css("height",f+"px")}return this.options.cardView?(this.$el.css("margin-top","0"),void this.$tableContainer.css("padding-bottom","0")):(this.options.showHeader&&this.options.height?(this.$tableHeader.show(),this.resetHeader(),c+=b):(this.$tableHeader.hide(),this.trigger("post-header")),this.options.showFooter&&(this.resetFooter(),this.options.height&&(c+=b)),this.getCaretHtml(),void this.$tableContainer.css("padding-bottom",c+"px"))},o.prototype.getData=function(b){return!this.searchText&&a.isEmptyObject(this.filterColumns)&&a.isEmptyObject(this.filterColumnsPartial)?b?this.options.data.slice(this.pageFrom-1,this.pageTo):this.options.data:b?this.data.slice(this.pageFrom-1,this.pageTo):this.data},o.prototype.load=function(b){var c=!1;"server"===this.options.sidePagination?(this.options.totalRows=b.total,c=b.fixedScroll,b=b.rows):a.isArray(b)||(c=b.fixedScroll,b=b.data),this.initData(b),this.initSearch(),this.initPagination(),this.initBody(c)},o.prototype.append=function(a){this.initData(a,"append"),this.initSearch(),this.initPagination(),this.initBody(!0)},o.prototype.prepend=function(a){this.initData(a,"prepend"),this.initSearch(),this.initPagination(),this.initBody(!0)},o.prototype.remove=function(b){var c,d,e=this.options.data.length;if(b.hasOwnProperty("field")&&b.hasOwnProperty("values")){for(c=e-1;c>=0;c--)d=this.options.data[c],d.hasOwnProperty(b.field)&&-1!==a.inArray(d[b.field],b.values)&&this.options.data.splice(c,1);e!==this.options.data.length&&(this.initSearch(),this.initPagination(),this.initBody(!0))}},o.prototype.removeAll=function(){this.options.data.length>0&&(this.options.data.splice(0,this.options.data.length),this.initSearch(),this.initPagination(),this.initBody(!0))},o.prototype.removeByUniqueId=function(a){var b,c,d=this.options.uniqueId,e=this.options.data.length;for(b=e-1;b>=0;b--)c=this.options.data[b],c.hasOwnProperty(d)&&("string"==typeof c[d]?a=a.toString():"number"==typeof c[d]&&(Number(c[d])===c[d]&&c[d]%1===0?a=parseInt(a):c[d]===Number(c[d])&&0!==c[d]&&(a=parseFloat(a))),c[d]===a&&this.options.data.splice(b,1));e!==this.options.data.length&&(this.initSearch(),this.initPagination(),this.initBody(!0))},o.prototype.insertRow=function(a){a.hasOwnProperty("index")&&a.hasOwnProperty("row")&&(this.data.splice(a.index,0,a.row),this.initSearch(),this.initPagination(),this.initSort(),this.initBody(!0))},o.prototype.updateRow=function(b){b.hasOwnProperty("index")&&b.hasOwnProperty("row")&&(a.extend(this.data[b.index],b.row),this.initSort(),this.initBody(!0))},o.prototype.showRow=function(a){a.hasOwnProperty("index")&&this.toggleRow(a.index,void 0===a.isIdField?!1:!0,!0)},o.prototype.hideRow=function(a){a.hasOwnProperty("index")&&this.toggleRow(a.index,void 0===a.isIdField?!1:!0,!1)},o.prototype.getRowsHidden=function(b){var c=a(this.$body[0]).children().filter(":hidden"),d=0;if(b)for(;de||0>f||e>=this.data.length)){for(c=e;e+g>c;c++)for(d=f;f+h>d;d++)i.eq(c).find("td").eq(d).hide();j.attr("rowspan",g).attr("colspan",h).show()}},o.prototype.updateCell=function(a){a.hasOwnProperty("rowIndex")&&a.hasOwnProperty("fieldName")&&a.hasOwnProperty("fieldValue")&&(this.data[a.rowIndex][a.fieldName]=a.fieldValue,this.initSort(),this.initBody(!0))},o.prototype.getOptions=function(){return this.options},o.prototype.getSelections=function(){var b=this;return a.grep(this.data,function(a){return a[b.header.stateField]})},o.prototype.getAllSelections=function(){var b=this;return a.grep(this.options.data,function(a){return a[b.header.stateField]})},o.prototype.checkAll=function(){this.checkAll_(!0)},o.prototype.uncheckAll=function(){this.checkAll_(!1)},o.prototype.checkAll_=function(a){var b;a||(b=this.getSelections()),this.$selectItem.filter(":enabled").prop("checked",a),this.updateRows(),this.updateSelected(),a&&(b=this.getSelections()),this.trigger(a?"check-all":"uncheck-all",b)},o.prototype.check=function(a){this.check_(!0,a)},o.prototype.uncheck=function(a){this.check_(!1,a)},o.prototype.check_=function(a,b){this.$selectItem.filter(g('[data-index="%s"]',b)).prop("checked",a),this.data[b][this.header.stateField]=a,this.updateSelected(),this.trigger(a?"check":"uncheck",this.data[b])},o.prototype.checkBy=function(a){this.checkBy_(!0,a)},o.prototype.uncheckBy=function(a){this.checkBy_(!1,a)},o.prototype.checkBy_=function(b,c){if(c.hasOwnProperty("field")&&c.hasOwnProperty("values")){var d=this,e=[];a.each(this.options.data,function(f,h){return h.hasOwnProperty(c.field)?void(-1!==a.inArray(h[c.field],c.values)&&(d.$selectItem.filter(g('[data-index="%s"]',f)).prop("checked",b),h[d.header.stateField]=b,e.push(h),d.trigger(b?"check":"uncheck",h))):!1}),this.updateSelected(),this.trigger(b?"check-some":"uncheck-some",e)}},o.prototype.destroy=function(){this.$el.insertBefore(this.$container),a(this.options.toolbar).insertBefore(this.$el),this.$container.next().remove(),this.$container.remove(),this.$el.html(this.$el_.html()).css("margin-top","0").attr("class",this.$el_.attr("class")||"")},o.prototype.showLoading=function(){this.$tableLoading.show()},o.prototype.hideLoading=function(){this.$tableLoading.hide()},o.prototype.togglePagination=function(){this.options.pagination=!this.options.pagination;var a=this.$toolbar.find('button[name="paginationSwitch"] i');this.options.pagination?a.attr("class",this.options.iconsPrefix+" "+this.options.icons.paginationSwitchDown):a.attr("class",this.options.iconsPrefix+" "+this.options.icons.paginationSwitchUp),this.updatePagination()},o.prototype.refresh=function(a){a&&a.url&&(this.options.url=a.url,this.options.pageNumber=1),this.initServer(a&&a.silent,a&&a.query)},o.prototype.resetWidth=function(){this.options.showHeader&&this.options.height&&this.fitHeader(),this.options.showFooter&&this.fitFooter()},o.prototype.showColumn=function(a){this.toggleColumn(i(this.options.columns,a),!0,!0)},o.prototype.hideColumn=function(a){this.toggleColumn(i(this.options.columns,a),!1,!0)},o.prototype.filterBy=function(b){this.filterColumns=a.isEmptyObject(b)?{}:b,this.options.pageNumber=1,this.initSearch(),this.updatePagination()},o.prototype.scrollTo=function(a){return"string"==typeof a&&(a="bottom"===a?this.$tableBody[0].scrollHeight:0),"number"==typeof a&&this.$tableBody.scrollTop(a),"undefined"==typeof a?this.$tableBody.scrollTop():void 0},o.prototype.getScrollPosition=function(){return this.scrollTo()},o.prototype.selectPage=function(a){a>0&&a<=this.options.totalPages&&(this.options.pageNumber=a,this.updatePagination())},o.prototype.prevPage=function(){this.options.pageNumber>1&&(this.options.pageNumber--,this.updatePagination())},o.prototype.nextPage=function(){this.options.pageNumber.btn-group"),f=e.find("div.export");if(!f.length){f=a(['
','",'","
"].join("")).appendTo(e);var g=f.find(".dropdown-menu"),h=this.options.exportTypes;if("string"==typeof this.options.exportTypes){var i=this.options.exportTypes.slice(1,-1).replace(/ /g,"").split(",");h=[],a.each(i,function(a,b){h.push(b.slice(1,-1))})}a.each(h,function(a,c){b.hasOwnProperty(c)&&g.append(['
  • ','',b[c],"","
  • "].join(""))}),g.find("li").click(function(){c.$el.tableExport(a.extend({},c.options.exportOptions,{type:a(this).data("type"),escape:!1}))})}}}}(jQuery); 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/bootstrap.js.coffee: -------------------------------------------------------------------------------- 1 | jQuery -> 2 | $("a[rel~=popover], .has-popover").popover() 3 | $("a[rel~=tooltip], .has-tooltip").tooltip() 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/tableExport.min.js: -------------------------------------------------------------------------------- 1 | (function(d){d.fn.extend({tableExport:function(k){function n(l,g,b,e){d(l).filter(":visible").find(g).each(function(l,g){"none"!=d(this).css("display")&&"none"!=d(this).data("tableexport-display")&&-1==a.ignoreColumn.indexOf(l)&&"function"===typeof e&&e(this,b,l)})}function x(l){!0===a.consoleLog&&console.log(l.output());if("string"==a.outputMode)return l.output();if("base64"==a.outputMode)return r(l.output());try{var g=l.output("blob");saveAs(g,a.fileName+".pdf")}catch(b){t(a.fileName+".pdf","data:application/pdf;base64,"+ 2 | r(l.output()))}}function y(l,g,b){var e="";g=p(l,g,b);l=null===g||""==g?"":g.toString();if(g instanceof Date)e=a.csvEnclosure+g.toLocaleString()+a.csvEnclosure;else if(g=a.csvEnclosure+a.csvEnclosure,e=l.replace(new RegExp(a.csvEnclosure.replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1"),"g"),g),0<=e.indexOf(a.csvSeparator)||/[\r\n ]/g.test(e))e=a.csvEnclosure+e+a.csvEnclosure;return e}function p(l,g,b){l=d(l);content_data=!0===a.htmlContent?l.html().trim():l.text().trim().replace(/\u00AD/g,"");!0===a.escape&& 3 | (content_data=escape(content_data));"function"===typeof a.onCellData&&(content_data=a.onCellData(l,g,b,content_data));return content_data}function F(a,g,b){return g+"-"+b.toLowerCase()}function z(a,g,b){window.getComputedStyle?(g=g.replace(/([a-z])([A-Z])/,F),g=window.getComputedStyle(a,null).getPropertyValue(g)):g=a.currentStyle?a.currentStyle[g]:a.style[g];g=g.match(/\d+/);if(null!==g){g=g[0];var e=document.createElement("div");e.style.overflow="hidden";e.style.visibility="hidden";a.parentElement.appendChild(e); 4 | e.style.width=100+b;b=100/e.offsetWidth;a.parentElement.removeChild(e);return g*b}return 0}function t(a,b){var c=document.createElement("a");if(c){document.body.appendChild(c);c.style="display: none";c.download=a;c.href=b;if(document.createEvent)null==u&&(u=document.createEvent("MouseEvents")),u.initEvent("click",!0,!1),c.dispatchEvent(u);else if(document.createEventObject)c.fireEvent("onclick");else if("function"==typeof c.onclick)c.onclick();document.body.removeChild(c)}}function r(a){var b="", 5 | c,e,d,f,h,k,m=0;a=a.replace(/\x0d\x0a/g,"\n");e="";for(d=0;df?e+=String.fromCharCode(f):(127f?e+=String.fromCharCode(f>>6|192):(e+=String.fromCharCode(f>>12|224),e+=String.fromCharCode(f>>6&63|128)),e+=String.fromCharCode(f&63|128));for(a=e;m>2,c=(c&3)<<4|e>>4,h=(e&15)<<2|d>>6,k=d&63,isNaN(e)?h=k=64:isNaN(d)&&(k=64),b=b+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(f)+ 6 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(c)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(h)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(k);return b}var a={csvSeparator:",",csvEnclosure:'"',onCellData:null,ignoreColumn:[],displayTableName:!1,theadSelector:"tr",tbodySelector:"tr",tableName:"myTableName",worksheetName:"xlsWorksheetName",type:"csv",jspdf:{orientation:"p",unit:"pt",format:"a4",margins:{left:20, 7 | right:10,top:10,bottom:10},autotable:{padding:2,lineHeight:12,fontSize:8}},escape:!1,htmlContent:!1,consoleLog:!1,outputMode:"file",fileName:"tableExport",excelstyles:["border-bottom","border-top","border-left","border-right"]};k=d.extend(!0,a,k);var u=null;if("csv"==a.type||"txt"==a.type){var c="",h=0;d(this).find("thead").find(a.theadSelector).each(function(){c+="\n";n(this,"th,td",h,function(b,g,d){c+=y(b,g,d)+a.csvSeparator});h++;c=d.trim(c);c=d.trim(c).substring(0,c.length-1)});d(this).find("tbody").find(a.tbodySelector).each(function(){trData= 8 | "";n(this,"td",h,function(b,c,d){trData+=y(b,c,d)+a.csvSeparator});trData.length>a.csvSeparator.length&&(c+="\n"+trData);h++;c=d.trim(c).substring(0,c.length-1)});c+="\n";!0===a.consoleLog&&console.log(c);if("string"==a.outputMode)return c;if("base64"==a.outputMode)return r(c);try{var q=new Blob([("csv"==a.type?"\ufeff":"")+c],{type:"text/"+("csv"==a.type?"csv":"plain")+";charset=utf-8"});saveAs(q,a.fileName+"."+a.type)}catch(G){t(a.fileName+"."+a.type,"data:text/"+("csv"==a.type?"csv":"plain")+";charset=utf-8,"+ 9 | ("csv"==a.type?"\ufeff":"")+encodeURIComponent(c))}}else if("sql"==a.type){h=0;c="INSERT INTO `"+a.tableName+"` (";d(this).find("thead").find(a.theadSelector).each(function(){n(this,"th,td",h,function(a,b,d){c+="'"+p(a,b,d)+"',"});h++;c=d.trim(c);c=d.trim(c).substring(0,c.length-1)});c+=") VALUES ";d(this).find("tbody").find(a.tbodySelector).each(function(){trData="";n(this,"td",h,function(a,b,c){trData+="'"+p(a,b,c)+"',"});3',m=m+"";d(this).find("thead").find(a.theadSelector).each(function(){n(this,"th,td",h,function(a,b,c){m+=""+p(a,b,c)+""});h++});var m=m+"",v=1;d(this).find("tbody").find(a.tbodySelector).each(function(){var a=1,b="";n(this,"td",h,function(c,e,d){b+=""+p(c,e,d)+"";a++});""!=b&&(m+=''+b+"",v++);h++});m+=""; 13 | !0===a.consoleLog&&console.log(m);if("string"==a.outputMode)return m;k=r(m);if("base64"==a.outputMode)return k;try{q=new Blob([m],{type:"application/xml;charset=utf-8"}),saveAs(q,a.fileName+".xml")}catch(J){t(a.fileName+".xml","data:application/xml;charset=utf-8;base64,"+k)}}else if("excel"==a.type||"doc"==a.type){var h=0,f="";d(this).find("thead").find(a.theadSelector).each(function(){f+="";n(this,"th,td",h,function(b,c,h){f+=""});h++;f+=""});v=1;d(this).find("tbody").find(a.tbodySelector).each(function(){f+="";n(this,"td",h,function(b,c,h){f+=""});v++;h++;f+=""});a.displayTableName&&(f+="");f+="
    "+p(b,c,h)+"
    "+p(b,c,h)+"
    "+ 15 | p(d("

    "+a.tableName+"

    "))+"
    ";!0===a.consoleLog&&console.log(f);b="";b+='';b=b+'';b+="";"excel"==a.type&&(b+="\x3c!--[if gte mso 9]>", 16 | b+="",b+="",b+="",b+="",b+="",b+=a.worksheetName,b+="",b+="",b+="",b+="",b+="",b+="",b+="",b+="",b+="";b+="";b+=f;b+="";b+="";if("string"==a.outputMode)return b;k=r(b);if("base64"==a.outputMode)return k;var C="excel"==a.type?"xls":"doc";try{q=new Blob([b], 17 | {type:"application/vnd.ms-"+a.type}),saveAs(q,a.fileName+"."+C)}catch(K){t(a.fileName+"."+C,"data:application/vnd.ms-"+a.type+";base64,"+k)}}else if("png"==a.type)html2canvas(d(this),{onrendered:function(b){b=b.toDataURL();b=b.substring(22);for(var c=atob(b),d=new ArrayBuffer(c.length),e=new Uint8Array(d),f=0;ftbody>tr>td,.bootstrap-table .table>tbody>tr>th,.bootstrap-table .table>tfoot>tr>td,.bootstrap-table .table>tfoot>tr>th,.bootstrap-table .table>thead>tr>td{padding:8px!important}.bootstrap-table .table.table-no-bordered>tbody>tr>td,.bootstrap-table .table.table-no-bordered>thead>tr>th{border-right:2px solid transparent}.fixed-table-container{position:relative;clear:both;border:1px solid #ddd;border-radius:4px;-webkit-border-radius:4px;-moz-border-radius:4px}.fixed-table-container.table-no-bordered{border:1px solid transparent}.fixed-table-footer,.fixed-table-header{height:37px;overflow:hidden;border-radius:4px 4px 0 0;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0}.fixed-table-header{border-bottom:1px solid #ddd}.fixed-table-footer{border-top:1px solid #ddd}.fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.fixed-table-container table{width:100%}.fixed-table-container thead th{height:0;padding:0;margin:0;border-left:1px solid #ddd}.fixed-table-container thead th:first-child{border-left:none;border-top-left-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px}.fixed-table-container thead th .th-inner{padding:8px;line-height:24px;vertical-align:top;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fixed-table-container thead th .sortable{cursor:pointer;background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC');background-position:right;background-repeat:no-repeat;padding-right:30px}.fixed-table-container th.detail{width:30px}.fixed-table-container tbody td{border-left:1px solid #ddd}.fixed-table-container tbody tr:first-child td{border-top:none}.fixed-table-container tbody td:first-child{border-left:none}.fixed-table-container tbody .selected td{background-color:#f5f5f5}.fixed-table-container .bs-checkbox{text-align:center}.fixed-table-container .bs-checkbox .th-inner{padding:8px 0}.fixed-table-container input[type=radio],.fixed-table-container input[type=checkbox]{margin:0 auto!important}.fixed-table-container .no-records-found{text-align:center}.fixed-table-pagination .pagination-detail,.fixed-table-pagination div.pagination{margin-top:10px;margin-bottom:10px}.fixed-table-pagination div.pagination .pagination{margin:0}.fixed-table-pagination .pagination a{padding:6px 12px;line-height:1.428571429}.fixed-table-pagination .pagination-info{line-height:34px;margin-right:5px}.fixed-table-pagination .btn-group{position:relative;display:inline-block;vertical-align:middle}.fixed-table-pagination .dropup .dropdown-menu{margin-bottom:0}.fixed-table-pagination .page-list{display:inline-block}.fixed-table-toolbar .columns-left{margin-right:5px}.fixed-table-toolbar .columns-right{margin-left:5px}.fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429}.fixed-table-toolbar .bars,.fixed-table-toolbar .columns,.fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px;line-height:34px}.fixed-table-pagination li.disabled a{pointer-events:none;cursor:default}.fixed-table-loading{display:none;position:absolute;top:42px;right:0;bottom:0;left:0;z-index:99;background-color:#fff;text-align:center}.fixed-table-body .card-view .title{font-weight:700;display:inline-block;min-width:30%;text-align:left!important}.fixed-table-body thead th .th-inner{box-sizing:border-box}.table td,.table th{vertical-align:middle;box-sizing:border-box}.fixed-table-toolbar .dropdown-menu{text-align:left;max-height:300px;overflow:auto}.fixed-table-toolbar .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.fixed-table-toolbar .btn-group>.btn-group>.btn{border-radius:0}.fixed-table-toolbar .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.fixed-table-toolbar .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.bootstrap-table .table thead>tr>th{padding:0;margin:0}.pull-right .dropdown-menu{right:0;left:auto}p.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden} -------------------------------------------------------------------------------- /app/assets/stylesheets/bootstrap_and_overrides.css.less: -------------------------------------------------------------------------------- 1 | @import "twitter/bootstrap/bootstrap"; 2 | 3 | // Set the correct sprite paths 4 | @iconSpritePath: image-url("twitter/bootstrap/glyphicons-halflings.png"); 5 | @iconWhiteSpritePath: image-url("twitter/bootstrap/glyphicons-halflings-white.png"); 6 | 7 | // Set the Font Awesome (Font Awesome is default. You can disable by commenting below lines) 8 | @fontAwesomeEotPath: font-url("fontawesome-webfont.eot"); 9 | @fontAwesomeEotPath_iefix: font-url("fontawesome-webfont.eot?#iefix"); 10 | @fontAwesomeWoffPath: font-url("fontawesome-webfont.woff"); 11 | @fontAwesomeTtfPath: font-url("fontawesome-webfont.ttf"); 12 | @fontAwesomeSvgPath: font-url("fontawesome-webfont.svg#fontawesomeregular"); 13 | 14 | @icon-font-path: "/fonts/"; 15 | 16 | // Font Awesome 17 | @import "fontawesome/font-awesome"; 18 | 19 | // Glyphicons 20 | @import "twitter/bootstrap/glyphicons.less"; 21 | 22 | // Your custom LESS stylesheets goes here 23 | // 24 | // Since bootstrap was imported above you have access to its mixins which 25 | // you may use and inherit here 26 | // 27 | // If you'd like to override bootstrap's own variables, you can do so here as well 28 | // See http://twitter.github.com/bootstrap/customize.html#variables for their names and documentation 29 | // 30 | // Example: 31 | // @link-color: #ff0000; 32 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | 6 | before_filter :authenticate 7 | 8 | private 9 | 10 | def authenticate 11 | authenticate_or_request_with_http_basic do |username, password| 12 | Setup.test_password(password) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/instances_controller.rb: -------------------------------------------------------------------------------- 1 | class InstancesController < ApplicationController 2 | include AwsCommon 3 | 4 | def index 5 | @instances = Instance.all 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/reserved_instances_controller.rb: -------------------------------------------------------------------------------- 1 | class ReservedInstancesController < ApplicationController 2 | include AwsCommon 3 | 4 | def index 5 | @reserved_instances = ReservedInstance.all 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/setup_controller.rb: -------------------------------------------------------------------------------- 1 | class SetupController < ApplicationController 2 | 3 | include AwsCommon 4 | 5 | def index 6 | @regions = Setup.get_regions 7 | @minutes = Setup.get_minutes 8 | @minutesrefresh = Setup.get_minutesrefresh 9 | @importdbr = Setup.get_importdbr 10 | @s3bucket = Setup.get_s3bucket 11 | @processed = Setup.get_processed 12 | @affinity = Setup.get_affinity 13 | end 14 | 15 | def change 16 | if !params[:regions].blank? 17 | Setup.put_regions params[:regions] 18 | end 19 | if !params[:minutes].blank? 20 | Setup.put_minutes params[:minutes] if params[:minutes].to_i >= 30 || params[:minutes].to_i == 0 21 | end 22 | if !params[:minutesrefresh].blank? 23 | Setup.put_minutesrefresh params[:minutesrefresh] if params[:minutesrefresh].to_i >= 5 24 | end 25 | Setup.put_password params[:password] if !params[:password].blank? 26 | Setup.put_importdbr !params[:importdbr].blank? 27 | Setup.put_affinity !params[:affinity].blank? 28 | Setup.put_s3bucket params[:s3bucket] 29 | redirect_to action: 'index' 30 | end 31 | 32 | def clear_cache 33 | populatedb_data() 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /app/controllers/summary_controller.rb: -------------------------------------------------------------------------------- 1 | class SummaryController < ApplicationController 2 | skip_before_filter :verify_authenticity_token, :only => [:periodic_worker, :health, :s3importer, :populatedb] 3 | skip_before_filter :authenticate, :only => [:periodic_worker, :health, :s3importer, :populatedb] 4 | before_filter :authenticate_local, :only => [:periodic_worker, :s3importer, :populatedb] 5 | 6 | include AwsCommon 7 | def index 8 | instances = Instance.all 9 | reserved_instances = ReservedInstance.all 10 | @summary = Summary.all 11 | end 12 | 13 | def health 14 | render :nothing => true, :status => 200, :content_type => 'text/html' 15 | end 16 | 17 | def recommendations 18 | @recommendations = [] 19 | RecommendationCache.all.each do |recommenation| 20 | @recommendations << Marshal.load(recommenation.object) 21 | end 22 | end 23 | 24 | def apply_recommendations 25 | recommendations = JSON.parse(params[:recommendations_original], :symbolize_names => true) 26 | selected = params[:recommendations].split(",") 27 | selected_recommendations = [] 28 | selected.each do |index| 29 | selected_recommendations << recommendations[index.to_i] 30 | end 31 | apply_recommendation(selected_recommendations) 32 | end 33 | 34 | def periodic_worker 35 | if Setup.now_after_nextrefresh 36 | Setup.update_nextrefresh 37 | populatedb_data() 38 | end 39 | 40 | if Setup.now_after_next 41 | Setup.update_next 42 | recommendations 43 | apply_recommendation(@recommendations) 44 | end 45 | render :nothing => true, :status => 200, :content_type => 'text/html' 46 | end 47 | 48 | def s3importer 49 | if Setup.get_importdbr 50 | bucket = Setup.get_s3bucket 51 | object = get_dbr_last_date(bucket, Setup.get_processed) 52 | if !object.nil? 53 | file_path = download_to_temp(bucket, object) 54 | file_path_unzip = File.join(Dir.tmpdir, Dir::Tmpname.make_tmpname('dbru',nil)) 55 | Zip::File.open(file_path) do |zip_file| 56 | zip_file.each do |entry| 57 | entry.extract(file_path_unzip) 58 | #content = entry.get_input_stream.read 59 | end 60 | end 61 | f = File.open(file_path_unzip) 62 | headers = f.gets 63 | f.close 64 | regular_account = true 65 | regular_account = false if headers.include? "BlendedRate" 66 | file_path_unzip_grep = File.join(Dir.tmpdir, Dir::Tmpname.make_tmpname('dbrug',nil)) 67 | system('grep "RunInstances:" ' + file_path_unzip + ' > ' + file_path_unzip_grep) 68 | list_instances = {} 69 | CSV.foreach(file_path_unzip_grep, {headers: true}) do |row| 70 | # row[10] -> Operation 71 | # row[21] -> ResourceId (19 for regular accounts) 72 | # row[2] -> AccountId 73 | # row[11] -> AZ 74 | if !row[10].nil? && row[10].start_with?('RunInstances:') && row[10] != 'RunInstances:0002' 75 | if regular_account 76 | list_instances[row[19]] = [row[10], row[2], row[11]] 77 | else 78 | list_instances[row[21]] = [row[10], row[2], row[11]] 79 | end 80 | end 81 | end 82 | amis = get_amis(list_instances, get_account_ids) 83 | 84 | amis.each do |ami_id, operation| 85 | if Ami.find_by(ami:ami_id).nil? 86 | new_ami = Ami.new 87 | new_ami.ami = ami_id 88 | new_ami.operation = operation 89 | new_ami.save 90 | end 91 | end 92 | 93 | File::unlink(file_path) 94 | File::unlink(file_path_unzip) 95 | File::unlink(file_path_unzip_grep) 96 | Setup.put_processed(object.last_modified) if ENV['MOCK_DATA'].blank? 97 | end 98 | end 99 | render :nothing => true, :status => 200, :content_type => 'text/html' 100 | end 101 | 102 | def log_recommendations 103 | @recommendations = Recommendation.all 104 | @failed_recommendations = Modification.all 105 | end 106 | 107 | def populatedb 108 | populatedb_data() 109 | 110 | render :nothing => true, :status => 200, :content_type => 'text/html' 111 | end 112 | 113 | private 114 | 115 | def authenticate_local 116 | render :nothing => true, :status => :unauthorized if !Socket.ip_address_list.map {|x| x.ip_address}.include?(request.remote_ip) 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /app/controllers/tests_controller.rb: -------------------------------------------------------------------------------- 1 | class TestsController < ApplicationController 2 | 3 | def index 4 | account_ids = get_account_ids 5 | end 6 | 7 | private 8 | 9 | def get_account_ids 10 | iam = Aws::IAM::Client.new(region: 'eu-west-1') 11 | metadata_endpoint = 'http://169.254.169.254/latest/meta-data/' 12 | iam_data = Net::HTTP.get( URI.parse( metadata_endpoint + 'iam/info' ) ) 13 | role_name = JSON.parse(iam_data)["InstanceProfileArn"].split("/")[-1] 14 | pages_policies = iam.list_role_policies({role_name: role_name}) 15 | account_ids = [JSON.parse(iam_data)["InstanceProfileArn"].split(":")[4]] 16 | pages_policies.each do |role_policies| 17 | role_policies.policy_names.each do |policy_name| 18 | pages_policy_data = iam.get_role_policy({role_name: role_name, policy_name: policy_name}) 19 | pages_policy_data.each do |policy_data| 20 | account_ids += get_account_ids_from_policy(CGI::unescape(policy_data.policy_document)) 21 | end 22 | end 23 | end 24 | #Rails.logger.debug(account_ids) 25 | return account_ids 26 | end 27 | 28 | def get_account_ids_from_policy(policy_document) 29 | policy = JSON.parse(policy_document) 30 | account_ids = [] 31 | policy["Statement"].each do |statement| 32 | if statement["Action"].include?("sts:AssumeRole") 33 | statement["Resource"].each do |arn| 34 | account_ids << arn.split(":")[4] 35 | end 36 | end 37 | end 38 | return account_ids 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/app/mailers/.keep -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/app/models/.keep -------------------------------------------------------------------------------- /app/models/ami.rb: -------------------------------------------------------------------------------- 1 | class Ami < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/instance.rb: -------------------------------------------------------------------------------- 1 | class Instance < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/models/modification.rb: -------------------------------------------------------------------------------- 1 | class Modification < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/models/recommendation.rb: -------------------------------------------------------------------------------- 1 | class Recommendation < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/models/recommendation_cache.rb: -------------------------------------------------------------------------------- 1 | class RecommendationCache < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/models/reserved_instance.rb: -------------------------------------------------------------------------------- 1 | class ReservedInstance < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/models/setup.rb: -------------------------------------------------------------------------------- 1 | class Setup < ActiveRecord::Base 2 | require 'bcrypt' 3 | 4 | def self.get_minutes 5 | minutes = 0 6 | setup = Setup.first 7 | if !setup.nil? 8 | minutes = setup.minutes if !setup.minutes.nil? 9 | end 10 | return minutes 11 | end 12 | 13 | def self.put_minutes(minutes) 14 | setup = Setup.first 15 | setup = Setup.new if setup.nil? 16 | 17 | setup.minutes = minutes 18 | setup.save 19 | update_next 20 | end 21 | 22 | def self.get_minutesrefresh 23 | minutesrefresh = 5 24 | setup = Setup.first 25 | if !setup.nil? 26 | minutesrefresh = setup.minutesrefresh if !setup.minutesrefresh.nil? 27 | end 28 | return minutesrefresh 29 | end 30 | 31 | def self.put_minutesrefresh(minutes) 32 | setup = Setup.first 33 | setup = Setup.new if setup.nil? 34 | 35 | setup.minutesrefresh = minutes 36 | setup.save 37 | update_nextrefresh 38 | end 39 | 40 | def self.get_password 41 | password = BCrypt::Password.create(ENV['DEFAULT_PASSWORD']) 42 | setup = Setup.first 43 | if !setup.nil? && !setup.password.nil? 44 | password = setup.password 45 | end 46 | return password 47 | end 48 | 49 | def self.put_password(password) 50 | setup = Setup.first 51 | setup = Setup.new if setup.nil? 52 | 53 | setup.password = BCrypt::Password.create(password) 54 | setup.save 55 | end 56 | 57 | def self.get_affinity 58 | setup = Setup.first 59 | affinity = false 60 | if !setup.nil? && !setup.affinity.nil? 61 | affinity = setup.affinity 62 | end 63 | return affinity 64 | end 65 | 66 | def self.put_affinity(affinity) 67 | setup = Setup.first 68 | setup = Setup.new if setup.nil? 69 | 70 | setup.affinity = affinity 71 | setup.save 72 | end 73 | 74 | def self.get_importdbr 75 | setup = Setup.first 76 | importdbr = false 77 | if !setup.nil? && !setup.importdbr.nil? 78 | importdbr = setup.importdbr 79 | end 80 | return importdbr 81 | end 82 | 83 | def self.put_importdbr(importdbr) 84 | setup = Setup.first 85 | setup = Setup.new if setup.nil? 86 | 87 | setup.importdbr = importdbr 88 | setup.save 89 | end 90 | 91 | def self.get_s3bucket 92 | setup = Setup.first 93 | s3bucket = '' 94 | if !setup.nil? && !setup.s3bucket.nil? 95 | s3bucket = setup.s3bucket 96 | end 97 | return s3bucket 98 | end 99 | 100 | def self.put_s3bucket(s3bucket) 101 | setup = Setup.first 102 | setup = Setup.new if setup.nil? 103 | 104 | setup.s3bucket = s3bucket 105 | setup.save 106 | end 107 | 108 | def self.get_processed 109 | setup = Setup.first 110 | s3bucket = '' 111 | if !setup.nil? && !setup.processed.nil? 112 | processed = setup.processed 113 | end 114 | return processed 115 | end 116 | 117 | def self.put_processed(processed) 118 | setup = Setup.first 119 | setup = Setup.new if setup.nil? 120 | 121 | setup.processed = processed 122 | setup.save 123 | end 124 | 125 | def self.test_password(password) 126 | return BCrypt::Password.new(get_password).is_password? password 127 | end 128 | 129 | def self.update_next 130 | minutes = 0 131 | setup = Setup.first 132 | if !setup.nil? 133 | minutes = setup.minutes if !setup.minutes.nil? 134 | end 135 | 136 | if minutes > 0 137 | setup.nextrun = Time.current + minutes.minutes 138 | setup.save 139 | end 140 | end 141 | 142 | def self.now_after_next 143 | after_next = false 144 | setup = Setup.first 145 | if !setup.nil? && !setup.nextrun.nil? 146 | return false if setup.minutes.nil? || setup.minutes==0 147 | after_next = (setup.nextrun < Time.current) 148 | end 149 | return after_next 150 | end 151 | 152 | def self.update_nextrefresh 153 | minutesrefresh = 5 154 | setup = Setup.first 155 | if !setup.nil? 156 | minutesrefresh = setup.minutesrefresh if !setup.minutesrefresh.nil? 157 | end 158 | 159 | setup.nextrefresh = Time.current + minutesrefresh.minutes 160 | setup.save 161 | end 162 | 163 | def self.now_after_nextrefresh 164 | ###### DELETE THIS ####### 165 | return true 166 | ###### DELETE THIS ####### 167 | after_nextrefresh = true 168 | setup = Setup.first 169 | if !setup.nil? && !setup.nextrefresh.nil? 170 | after_nextrefresh = (setup.nextrefresh < Time.current) 171 | end 172 | return after_nextrefresh 173 | end 174 | 175 | 176 | def self.get_regions 177 | regions = {"eu-west-1"=> false, "us-east-1"=> false, "eu-central-1"=> false, "us-west-1"=> false, "us-west-2"=> false, "ap-southeast-1"=> false, "ap-southeast-2"=> false, "ap-northeast-1"=> false, "sa-east-1"=> false} 178 | setup = Setup.first 179 | if !setup.nil? && !setup.regions.nil? 180 | regions_text = setup.regions 181 | regions_list = regions_text.split "," 182 | regions_list.each do |region| 183 | regions[region] = true if !regions[region].nil? 184 | end 185 | end 186 | return regions 187 | end 188 | 189 | def self.put_regions(regions) 190 | regions_list = [] 191 | regions.each do |region, value| 192 | regions_list << region if value 193 | end 194 | setup = Setup.first 195 | setup = Setup.new if setup.nil? 196 | 197 | setup.regions = regions_list.join "," 198 | setup.save 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /app/models/summary.rb: -------------------------------------------------------------------------------- 1 | class Summary < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/views/instances/index.erb: -------------------------------------------------------------------------------- 1 |

    Instances

    2 | > 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% @instances.each do |instance| %> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% end %> 26 | 27 |
    Account IDInstance IDInstance typeAvailability ZoneTenancyPlatformNetwork
    <%= instance.accountid %><%= instance.instanceid %><%= instance.instancetype %><%= instance.az %><%= instance.tenancy %><%= instance.platform %><%= instance.network %>
    28 | 29 | 34 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= content_for?(:title) ? yield(:title) : "Reserved Instances" %> 8 | <%= csrf_meta_tags %> 9 | 10 | 11 | 14 | 15 | <%= stylesheet_link_tag "application", :media => "all" %> 16 | 17 | 18 | 19 | <%= favicon_link_tag 'apple-touch-icon-144x144-precomposed.png', :rel => 'apple-touch-icon-precomposed', :type => 'image/png', :sizes => '144x144' %> 20 | 21 | 22 | 23 | <%= favicon_link_tag 'apple-touch-icon-114x114-precomposed.png', :rel => 'apple-touch-icon-precomposed', :type => 'image/png', :sizes => '114x114' %> 24 | 25 | 26 | 27 | <%= favicon_link_tag 'apple-touch-icon-72x72-precomposed.png', :rel => 'apple-touch-icon-precomposed', :type => 'image/png', :sizes => '72x72' %> 28 | 29 | 30 | 31 | <%= favicon_link_tag 'apple-touch-icon-precomposed.png', :rel => 'apple-touch-icon-precomposed', :type => 'image/png' %> 32 | 33 | 34 | 35 | <%= favicon_link_tag 'favicon.ico', :rel => 'shortcut icon', :href => '/favicon.ico' %> 36 | 37 | <%= javascript_include_tag "application" %> 38 | 39 | 40 | 41 | 62 | 63 |
    64 |
    65 | <%= bootstrap_flash %> 66 | <%= yield %> 67 |
    68 | 69 |
    70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /app/views/reserved_instances/index.erb: -------------------------------------------------------------------------------- 1 |

    Reserved Instances

    2 | 3 | > 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <% @reserved_instances.each do |ri| %> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | <% end %> 33 | 34 |
    Account IDReservation IDInstance typeAvailability ZoneTenancyPlatformNetworkCountEnd DateStatus
    <%= ri.accountid %><%= ri.reservationid %><%= ri.instancetype %><%= ri.az %><%= ri.tenancy %><%= ri.platform %><%= ri.network %><%= ri.count %><%= ri.enddate %><%= ri.status %>
    35 | 36 | 41 | -------------------------------------------------------------------------------- /app/views/setup/clear_cache.erb: -------------------------------------------------------------------------------- 1 |

    The cache has been cleared

    2 | -------------------------------------------------------------------------------- /app/views/setup/index.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Setup

    3 | <%= form_tag(change_setup_path, :onsubmit => "return validateform()") do %> 4 |
    5 |
    6 | <% @regions.keys.each do |region| %> 7 | 11 | <% end %> 12 |
    13 |
    14 |
    15 |
    <%= number_field_tag "minutes", @minutes.to_s, min:0, id: 'minutestext', class: 'form-control' %>
    minutes (0 to stop, minimum 30) 16 |
    17 |
    18 |
    19 |
    <%= number_field_tag "minutesrefresh", @minutesrefresh.to_s, min:5, id: 'minutesrefreshtext', class: 'form-control' %>
    minutesrefresh (minimum 5) 20 |
    21 |
    22 | 26 |
    27 |
    28 |
    29 | <%= password_field_tag 'password', nil, class: 'form-control' %> 30 |
    31 |
    32 | 36 |
    37 |
    38 | 39 | <%= text_field_tag "s3bucket", @s3bucket, id: 's3bucket', class: 'form-control' %> 40 |
    41 |
    42 | Last processed time: <%= @processed.blank? ? "Never" : @processed.to_s %> 43 |
    44 | <%= submit_tag("Save", class: 'btn btn-default') %> 45 | <% end %> 46 | 47 |
    48 | 49 | 58 | 59 | -------------------------------------------------------------------------------- /app/views/summary/apply_recommendations.erb: -------------------------------------------------------------------------------- 1 |

    All the selected recommendations have been applied

    2 |

    Please wait until the modifications have been applied, and then clear the cache, before you continue

    3 | -------------------------------------------------------------------------------- /app/views/summary/index.erb: -------------------------------------------------------------------------------- 1 |

    Summary

    2 | 3 | > 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% @summary.each do |s| %> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% end %> 25 | 26 |
    Instance typeAvailability ZonePlatformTenancyTotal InstancesTotal Reservations
    <%= s.instancetype %><%= s.az %><%= s.platform %><%= s.tenancy %><%= s.total %><%= s.reservations %>
    27 | 28 |

    Legend

    29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
    Description
    There are more instances than reserved instances. Opportunity to move other reserved instances to this class or buy new RIs
    There are more reserved instances than instances. Opportunity to move reserved instances to other class
    44 | 45 | 64 | -------------------------------------------------------------------------------- /app/views/summary/log_recommendations.erb: -------------------------------------------------------------------------------- 1 |

    General log

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% @recommendations.each do |recommendation| %> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% end %> 26 | 27 |
    Account IDReservation IDCountNew typeNew availability zoneTime appliedResult
    <%= recommendation.accountid %><%= recommendation.rid %><%= recommendation.counts %><%= recommendation.instancetype %><%= recommendation.az %><%= recommendation.timestamp %><%= @failed_recommendations.find_by(modificationid: recommendation.rid) ? ''.html_safe : ''.html_safe %>
    28 | 29 | 34 | -------------------------------------------------------------------------------- /app/views/summary/recommendations.erb: -------------------------------------------------------------------------------- 1 |

    Recommendations

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% i= 0 %> 17 | <% @recommendations.each do |recommendation| %> 18 | <% recommendation_ids = [] 19 | recommendation_counts = [] 20 | recommendation_orig_counts = [] 21 | recommendation_types = [] 22 | recommendation_azs = [] 23 | recommendation_vpcs = [] 24 | recommendation.each do |element| 25 | recommendation_ids << "#{element[:rid]}" 26 | recommendation_counts << element[:count] 27 | recommendation_orig_counts << element[:orig_count] 28 | recommendation_types << element[:type] 29 | recommendation_azs << element[:az] 30 | recommendation_vpcs << element[:vpc] 31 | end %> 32 | 33 | 34 | 35 | 36 | <% i += 1 %> 37 | 38 | 39 | 40 | 41 | 42 | 43 | <% end %> 44 | 45 |
    Change numberReservation IDOrig CountNew CountNew typeNew availability zone
    <%= i %><%= recommendation_ids.join(",").html_safe() %><%= recommendation_orig_counts.join(",") %><%= recommendation_counts.join(",") %><%= recommendation_types.join(",") %><%= recommendation_azs.join(",") %>
    46 |
    47 | 48 | 49 | <%= form_tag(apply_recommendations_path, id: 'send_data') do %> 50 | 51 | 52 | <% end %> 53 | 54 | 71 | 72 | -------------------------------------------------------------------------------- /app/views/tests/index.erb: -------------------------------------------------------------------------------- 1 | Hello world! 2 | 3 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Reservedinstances 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | 23 | # Do not swallow errors in after_commit/after_rollback callbacks. 24 | config.active_record.raise_in_transactional_callbacks = true 25 | config.autoload_paths += %W(#{config.root}/lib) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | adapter: mysql2 25 | encoding: utf8 26 | pool: 5 27 | host: <%= ENV['RDS_HOSTNAME'] %> 28 | database: <%= ENV['RDS_DB_NAME'] %> 29 | username: <%= ENV['RDS_USERNAME'] %> 30 | password: <%= ENV['RDS_PASSWORD'] %> 31 | 32 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_reservedinstances_session' 4 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.bootstrap.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | breadcrumbs: 6 | application: 7 | root: "Index" 8 | pages: 9 | pages: "Pages" 10 | helpers: 11 | actions: "Actions" 12 | links: 13 | back: "Back" 14 | cancel: "Cancel" 15 | confirm: "Are you sure?" 16 | destroy: "Delete" 17 | new: "New" 18 | edit: "Edit" 19 | titles: 20 | edit: "Edit %{model}" 21 | save: "Save %{model}" 22 | new: "New %{model}" 23 | delete: "Delete %{model}" 24 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # The priority is based upon order of creation: first created -> highest priority. 3 | # See how all your routes lay out with "rake routes". 4 | 5 | # You can have the root of your site routed with "root" 6 | # root 'welcome#index' 7 | root 'setup#index' 8 | 9 | get '/setup' => 'setup#index' 10 | post '/setup' => 'setup#change', as: 'change_setup' 11 | get '/clearcache' => 'setup#clear_cache' 12 | 13 | get '/instances' => 'instances#index' 14 | get '/reservedinstances' => 'reserved_instances#index' 15 | get '/summary' => 'summary#index' 16 | get '/recommendations' => 'summary#recommendations' 17 | post '/recommendations' => 'summary#apply_recommendations', as: 'apply_recommendations' 18 | get '/logrecommendations' => 'summary#log_recommendations' 19 | get '/health' => 'summary#health' 20 | 21 | post '/periodicworker' => 'summary#periodic_worker' 22 | post '/s3importer' => 'summary#s3importer' 23 | post '/populatedb' => 'summary#populatedb' 24 | 25 | 26 | # Example of regular route: 27 | # get 'products/:id' => 'catalog#view' 28 | 29 | # Example of named route that can be invoked with purchase_url(id: product.id) 30 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 31 | 32 | # Example resource route (maps HTTP verbs to controller actions automatically): 33 | # resources :products 34 | 35 | # Example resource route with options: 36 | # resources :products do 37 | # member do 38 | # get 'short' 39 | # post 'toggle' 40 | # end 41 | # 42 | # collection do 43 | # get 'sold' 44 | # end 45 | # end 46 | 47 | # Example resource route with sub-resources: 48 | # resources :products do 49 | # resources :comments, :sales 50 | # resource :seller 51 | # end 52 | 53 | # Example resource route with more complex sub-resources: 54 | # resources :products do 55 | # resources :comments 56 | # resources :sales do 57 | # get 'recent', on: :collection 58 | # end 59 | # end 60 | 61 | # Example resource route with concerns: 62 | # concern :toggleable do 63 | # post 'toggle' 64 | # end 65 | # resources :posts, concerns: :toggleable 66 | # resources :photos, concerns: :toggleable 67 | 68 | # Example resource route within a namespace: 69 | # namespace :admin do 70 | # # Directs /admin/products/* to Admin::ProductsController 71 | # # (app/controllers/admin/products_controller.rb) 72 | # resources :products 73 | # end 74 | end 75 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: f4780821d2d7c86550bcd1d5e106871174874310837d9fe6293de251a4a6d34b185719012b6cc37aa289508eb2e380e9648e957548d26d5154c5c1fa52a88665 15 | 16 | test: 17 | secret_key_base: 4f83d03a2b24a0aa21aa2101e953211b51540798ee363c6d57e80f73f35e4210c54aca2af9aefe2abbf2060571c995f54f0b234350eedead751ab4b2eceaac3b 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /cron.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | cron: 3 | - name: "apply-recommendations-job" 4 | url: "/periodicworker" 5 | schedule: "*/5 * * * *" 6 | - name: "read-s3-dbr" 7 | url: "/s3importer" 8 | schedule: "* */2 * * *" 9 | -------------------------------------------------------------------------------- /db/migrate/20150609101847_create_setups.rb: -------------------------------------------------------------------------------- 1 | class CreateSetups < ActiveRecord::Migration 2 | def change 3 | create_table :setups do |t| 4 | t.text :regions 5 | 6 | t.timestamps null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20150611114606_add_minutes_to_setup.rb: -------------------------------------------------------------------------------- 1 | class AddMinutesToSetup < ActiveRecord::Migration 2 | def change 3 | add_column :setups, :minutes, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150612114153_add_next_to_setup.rb: -------------------------------------------------------------------------------- 1 | class AddNextToSetup < ActiveRecord::Migration 2 | def change 3 | add_column :setups, :nextrun, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150612115559_create_recommendations.rb: -------------------------------------------------------------------------------- 1 | class CreateRecommendations < ActiveRecord::Migration 2 | def change 3 | create_table :recommendations do |t| 4 | t.string :rid 5 | t.string :az 6 | t.string :instancetype 7 | t.string :vpc 8 | t.integer :count 9 | t.datetime :timestamp 10 | t.string :accountid 11 | 12 | t.timestamps null: false 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20150615090544_add_password_to_setup.rb: -------------------------------------------------------------------------------- 1 | class AddPasswordToSetup < ActiveRecord::Migration 2 | def change 3 | add_column :setups, :password, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150930092546_add_s3conf_to_setup.rb: -------------------------------------------------------------------------------- 1 | class AddS3confToSetup < ActiveRecord::Migration 2 | def change 3 | add_column :setups, :importdbr, :boolean 4 | add_column :setups, :s3bucket, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20150930110321_add_processed_to_setup.rb: -------------------------------------------------------------------------------- 1 | class AddProcessedToSetup < ActiveRecord::Migration 2 | def change 3 | add_column :setups, :processed, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150930131716_create_amis.rb: -------------------------------------------------------------------------------- 1 | class CreateAmis < ActiveRecord::Migration 2 | def change 3 | create_table :amis do |t| 4 | t.string :ami 5 | t.string :operation 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20160531154327_create_instances.rb: -------------------------------------------------------------------------------- 1 | class CreateInstances < ActiveRecord::Migration 2 | def change 3 | create_table :instances do |t| 4 | t.string :accountid 5 | t.string :instanceid 6 | t.string :instancetype 7 | t.string :az 8 | t.string :tenancy 9 | t.string :platform 10 | t.string :network 11 | 12 | t.timestamps null: false 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20160531154702_create_reserved_instances.rb: -------------------------------------------------------------------------------- 1 | class CreateReservedInstances < ActiveRecord::Migration 2 | def change 3 | create_table :reserved_instances do |t| 4 | t.string :accountid 5 | t.string :reservationid 6 | t.string :instancetype 7 | t.string :az 8 | t.string :tenancy 9 | t.string :platform 10 | t.string :network 11 | t.integer :count 12 | t.datetime :enddate 13 | t.string :status 14 | t.string :rolearn 15 | 16 | t.timestamps null: false 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20160531155006_create_modifications.rb: -------------------------------------------------------------------------------- 1 | class CreateModifications < ActiveRecord::Migration 2 | def change 3 | create_table :modifications do |t| 4 | t.string :modificationid 5 | t.string :status 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20160601070400_add_affinity_to_setup.rb: -------------------------------------------------------------------------------- 1 | class AddAffinityToSetup < ActiveRecord::Migration 2 | def change 3 | add_column :setups, :affinity, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160601093449_add_counts_to_recommendation.rb: -------------------------------------------------------------------------------- 1 | class AddCountsToRecommendation < ActiveRecord::Migration 2 | def change 3 | add_column :recommendations, :counts, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160601145048_add_offering_to_reserved_instance.rb: -------------------------------------------------------------------------------- 1 | class AddOfferingToReservedInstance < ActiveRecord::Migration 2 | def change 3 | add_column :reserved_instances, :offering, :string 4 | add_column :reserved_instances, :duration, :integer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20160616131916_add_refresh_to_setup.rb: -------------------------------------------------------------------------------- 1 | class AddRefreshToSetup < ActiveRecord::Migration 2 | def change 3 | add_column :setups, :nextrefresh, :datetime 4 | add_column :setups, :minutesrefresh, :integer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20160616135351_create_summaries.rb: -------------------------------------------------------------------------------- 1 | class CreateSummaries < ActiveRecord::Migration 2 | def change 3 | create_table :summaries do |t| 4 | t.string :instancetype 5 | t.string :az 6 | t.string :tenancy 7 | t.string :platform 8 | t.integer :total 9 | t.integer :reservations 10 | 11 | t.timestamps null: false 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20160616142650_create_recommendation_caches.rb: -------------------------------------------------------------------------------- 1 | class CreateRecommendationCaches < ActiveRecord::Migration 2 | def change 3 | create_table :recommendation_caches do |t| 4 | t.text :object 5 | 6 | t.timestamps null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20160616151823) do 15 | 16 | create_table "amis", force: :cascade do |t| 17 | t.string "ami" 18 | t.string "operation" 19 | end 20 | 21 | create_table "instances", force: :cascade do |t| 22 | t.string "accountid" 23 | t.string "instanceid" 24 | t.string "instancetype" 25 | t.string "az" 26 | t.string "tenancy" 27 | t.string "platform" 28 | t.string "network" 29 | t.datetime "created_at", null: false 30 | t.datetime "updated_at", null: false 31 | end 32 | 33 | create_table "modifications", force: :cascade do |t| 34 | t.string "modificationid" 35 | t.string "status" 36 | t.datetime "created_at", null: false 37 | t.datetime "updated_at", null: false 38 | end 39 | 40 | create_table "recommendation_caches", force: :cascade do |t| 41 | t.text "object" 42 | t.datetime "created_at", null: false 43 | t.datetime "updated_at", null: false 44 | end 45 | 46 | create_table "recommendations", force: :cascade do |t| 47 | t.string "rid" 48 | t.string "az" 49 | t.string "instancetype" 50 | t.string "vpc" 51 | t.integer "count" 52 | t.datetime "timestamp" 53 | t.string "accountid" 54 | t.datetime "created_at", null: false 55 | t.datetime "updated_at", null: false 56 | t.string "counts" 57 | end 58 | 59 | create_table "reserved_instances", force: :cascade do |t| 60 | t.string "accountid" 61 | t.string "reservationid" 62 | t.string "instancetype" 63 | t.string "az" 64 | t.string "tenancy" 65 | t.string "platform" 66 | t.string "network" 67 | t.integer "count" 68 | t.datetime "enddate" 69 | t.string "status" 70 | t.string "rolearn" 71 | t.datetime "created_at", null: false 72 | t.datetime "updated_at", null: false 73 | t.string "offering" 74 | t.integer "duration" 75 | end 76 | 77 | create_table "setups", force: :cascade do |t| 78 | t.text "regions" 79 | t.datetime "created_at", null: false 80 | t.datetime "updated_at", null: false 81 | t.integer "minutes" 82 | t.datetime "nextrun" 83 | t.string "password" 84 | t.boolean "importdbr" 85 | t.string "s3bucket" 86 | t.datetime "processed" 87 | t.boolean "affinity" 88 | t.datetime "nextrefresh" 89 | t.integer "minutesrefresh" 90 | end 91 | 92 | create_table "summaries", force: :cascade do |t| 93 | t.string "instancetype" 94 | t.string "az" 95 | t.string "tenancy" 96 | t.string "platform" 97 | t.integer "total" 98 | t.integer "reservations" 99 | t.datetime "created_at", null: false 100 | t.datetime "updated_at", null: false 101 | end 102 | 103 | create_table "zones", force: :cascade do |t| 104 | t.string "accountid" 105 | t.string "az" 106 | t.datetime "created_at", null: false 107 | t.datetime "updated_at", null: false 108 | end 109 | 110 | end 111 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/lib/assets/.keep -------------------------------------------------------------------------------- /lib/aws_common.rb: -------------------------------------------------------------------------------- 1 | module AwsCommon 2 | require 'csv' 3 | require 'zip' 4 | 5 | METADATA_ENDPOINT = 'http://169.254.169.254/latest/meta-data/' 6 | PLATFORMS = {'RunInstances:000g' => 'SUSE Linux', 'RunInstances:0006' => 'Windows with SQL Server Standard', 'RunInstances:0202' => 'Windows with SQL Server Web', 'RunInstances:0010' => 'Red Hat Enterprise Linux', 'RunInstances:0102' => 'Windows with SQL Server Enterprise'} 7 | 8 | def get_current_account_id 9 | iam_data = Net::HTTP.get( URI.parse( METADATA_ENDPOINT + 'iam/info' ) ) 10 | return JSON.parse(iam_data)["InstanceProfileArn"].split(":")[4] 11 | end 12 | 13 | def get_account_ids 14 | return [] if !ENV['MOCK_DATA'].blank? 15 | iam = Aws::IAM::Client.new(region: 'eu-west-1') 16 | iam_data = Net::HTTP.get( URI.parse( METADATA_ENDPOINT + 'iam/info' ) ) 17 | role_name = JSON.parse(iam_data)["InstanceProfileArn"].split("/")[-1] 18 | pages_policies = iam.list_role_policies({role_name: role_name}) 19 | account_ids = [[get_current_account_id,""]] 20 | pages_policies.each do |role_policies| 21 | role_policies.policy_names.each do |policy_name| 22 | pages_policy_data = iam.get_role_policy({role_name: role_name, policy_name: policy_name}) 23 | pages_policy_data.each do |policy_data| 24 | account_ids += get_account_ids_from_policy(CGI::unescape(policy_data.policy_document)) 25 | end 26 | end 27 | end 28 | return account_ids 29 | end 30 | 31 | def get_account_ids_from_policy(policy_document) 32 | policy = JSON.parse(policy_document) 33 | account_ids = [] 34 | policy["Statement"].each do |statement| 35 | if statement["Action"].include?("sts:AssumeRole") 36 | statement["Resource"].each do |arn| 37 | account_ids << [arn.split(":")[4], arn] 38 | end 39 | end 40 | end 41 | return account_ids 42 | end 43 | 44 | def is_marketplace(product_codes) 45 | return false if product_codes.blank? 46 | product_codes.each do |product_code| 47 | return true if product_code.product_code_type == 'marketplace' 48 | end 49 | return false 50 | end 51 | 52 | def get_instances(regions, account_ids) 53 | return get_mock_instances if !ENV['MOCK_DATA'].blank? 54 | instances = {} 55 | current_account_id = get_current_account_id 56 | 57 | amis = {} 58 | Ami.all.each do |ami| 59 | amis[ami.ami] = ami.operation 60 | end 61 | 62 | account_ids.each do |account_id| 63 | regions.select {|key, value| value }.keys.each do |region| 64 | if account_id[0] == current_account_id 65 | ec2 = Aws::EC2::Resource.new(client: Aws::EC2::Client.new(region: region)) 66 | else 67 | role_credentials = Aws::AssumeRoleCredentials.new( client: Aws::STS::Client.new(region: region), role_arn: account_id[1], role_session_name: "reserved_instances" ) 68 | ec2 = Aws::EC2::Resource.new(client: Aws::EC2::Client.new(region: region, credentials: role_credentials)) 69 | end 70 | ec2.instances.each do |instance| 71 | if !is_marketplace(instance.product_codes) 72 | platform = instance.platform.blank? ? "Linux/UNIX" : "Windows" 73 | platform = PLATFORMS[amis[instance.image_id]] if !amis[instance.image_id].nil? && !PLATFORMS[amis[instance.image_id]].nil? 74 | 75 | instances[instance.id] = {type: instance.instance_type, az: instance.placement.availability_zone, tenancy: instance.placement.tenancy, platform: platform, account_id: account_id[0], vpc: instance.vpc_id.blank? ? "EC2 Classic" : "VPC", ami: instance.image_id} if instance.state.name == 'running' and instance.instance_lifecycle != 'spot' 76 | end 77 | end 78 | end 79 | end 80 | instances 81 | end 82 | 83 | def get_zones(regions, account_ids) 84 | return get_mock_zones if !ENV['MOCK_DATA'].blank? 85 | zones = {} 86 | current_account_id = get_current_account_id 87 | account_ids.each do |account_id| 88 | zones[account_id[0]] = [] 89 | regions.select {|key, value| value }.keys.each do |region| 90 | if account_id[0] == current_account_id 91 | ec2 = Aws::EC2::Client.new(region: region) 92 | else 93 | role_credentials = Aws::AssumeRoleCredentials.new( client: Aws::STS::Client.new(region: region), role_arn: account_id[1], role_session_name: "reserved_instances" ) 94 | ec2 = Aws::EC2::Client.new(region: region, credentials: role_credentials) 95 | end 96 | resp = ec2.describe_availability_zones() 97 | resp.availability_zones.each do |az| 98 | zones[account_id[0]] << az.zone_name if az.state == 'available' 99 | end 100 | end 101 | end 102 | zones 103 | end 104 | 105 | def get_mock_zones() 106 | zones = {} 107 | CSV.foreach('/tmp/zones.csv', headers: true) do |row| 108 | zones[row[1]] = [] if !zones[row[1]] 109 | zones[row[1]] << row[2] 110 | end 111 | return zones 112 | end 113 | 114 | def get_failed_modifications(regions, account_ids) 115 | return [] if !ENV['MOCK_DATA'].blank? 116 | failed_modifications = [] 117 | current_account_id = get_current_account_id 118 | 119 | account_ids.each do |account_id| 120 | regions.select {|key, value| value }.keys.each do |region| 121 | if account_id[0] == current_account_id 122 | ec2 = Aws::EC2::Client.new(region: region) 123 | else 124 | role_credentials = Aws::AssumeRoleCredentials.new( client: Aws::STS::Client.new(region: region), role_arn: account_id[1], role_session_name: "reserved_instances" ) 125 | ec2 = Aws::EC2::Client.new(region: region, credentials: role_credentials) 126 | end 127 | modifications = ec2.describe_reserved_instances_modifications({filters: [ {name: 'status', values: ['failed'] } ] }) 128 | modifications.reserved_instances_modifications.each do |modification| 129 | failed_modifications << modification.reserved_instances_ids[0].reserved_instances_id 130 | end 131 | end 132 | end 133 | 134 | failed_modifications 135 | end 136 | 137 | def get_mock_instances 138 | instances = {} 139 | amis = {} 140 | Ami.all.each do |ami| 141 | amis[ami.ami] = ami.operation 142 | end 143 | CSV.foreach('/tmp/instances.csv', headers: true) do |row| 144 | platform = row[5].blank? ? "Linux/UNIX" : "Windows" 145 | platform = PLATFORMS[amis[row[9]]] if !amis[row[9]].nil? && !PLATFORMS[amis[row[9]]].nil? 146 | 147 | instances[row[1]] = {type: row[2], az: row[3], tenancy: row[4], platform: platform, account_id: row[6], vpc: row[7].blank? ? "EC2 Classic" : "VPC", ami: row[9]} if row[8] == 'running' 148 | end 149 | return instances 150 | end 151 | 152 | def get_reserved_instances(regions, account_ids) 153 | return get_mock_reserved_instances if !ENV['MOCK_DATA'].blank? 154 | instances = {} 155 | current_account_id = get_current_account_id 156 | 157 | supported_platforms = {} 158 | 159 | account_ids.each do |account_id| 160 | regions.select {|key, value| value }.keys.each do |region| 161 | if account_id[0] == current_account_id 162 | ec2 = Aws::EC2::Client.new(region: region) 163 | else 164 | role_credentials = Aws::AssumeRoleCredentials.new( client: Aws::STS::Client.new(region: region), role_arn: account_id[1], role_session_name: "reserved_instances" ) 165 | ec2 = Aws::EC2::Client.new(region: region, credentials: role_credentials) 166 | end 167 | if supported_platforms[account_id[0]].nil? 168 | platforms = ec2.describe_account_attributes(attribute_names: ["supported-platforms"]) 169 | platforms.each do |platform| 170 | platform.account_attributes.each do |attribute| 171 | supported_platforms[account_id[0]] = attribute.attribute_values.size > 1 ? "Classic" : "VPC" if attribute.attribute_name == 'supported-platforms' 172 | end 173 | end 174 | end 175 | 176 | reserved_instances = ec2.describe_reserved_instances 177 | reserved_instances.each do |reserved_instance| 178 | reserved_instance.reserved_instances.each do |ri| 179 | if ri.state == 'active' 180 | Rails.logger.debug ri 181 | instances[ri.reserved_instances_id] = {type: ri.instance_type, az: ri.availability_zone, tenancy: ri.instance_tenancy, account_id: account_id[0], count: ri.instance_count, description: ri.product_description, role_arn: account_id[1], end: ri.end, status: 'active', offering: ri.offering_type, duration: ri.duration} 182 | if supported_platforms[account_id[0]] == 'Classic' 183 | instances[ri.reserved_instances_id][:vpc] = ri.product_description.include?("Amazon VPC") ? 'VPC' : 'EC2 Classic' 184 | else 185 | instances[ri.reserved_instances_id][:vpc] = 'VPC' 186 | end 187 | instances[ri.reserved_instances_id][:platform] = ri.product_description.sub(' (Amazon VPC)','') 188 | end 189 | end 190 | end 191 | modifications = ec2.describe_reserved_instances_modifications({filters: [ {name: 'status', values: ['processing'] } ] }) 192 | modifications.reserved_instances_modifications.each do |modification| 193 | if !instances[modification.reserved_instances_ids[0].reserved_instances_id].nil? 194 | instances[modification.reserved_instances_ids[0].reserved_instances_id][:status] = 'processing' 195 | instances[modification.modification_results[0].reserved_instances_id][:status] = 'creating' if !instances[modification.modification_results[0].reserved_instances_id].nil? 196 | end 197 | end 198 | end 199 | end 200 | 201 | instances 202 | end 203 | 204 | def get_mock_reserved_instances 205 | instances = {} 206 | platforms = {} 207 | CSV.foreach('/tmp/platforms.csv', headers: true) do |row| 208 | platforms[row[2]] = row[1] 209 | end 210 | CSV.foreach('/tmp/ri.csv', headers: true) do |row| 211 | if row[8] == 'active' 212 | instances[row[1]] = {type: row[2], az: row[3], tenancy: row[4], account_id: row[5], count: row[6].to_i, description: row[7], role_arn: '', status: 'active', end: 2.months.from_now, offering: row[9], duration: row[10]} 213 | if platforms[row[5]] == 'Classic' 214 | instances[row[1]][:vpc] = row[7].include?("Amazon VPC") ? 'VPC' : 'EC2 Classic' 215 | else 216 | instances[row[1]][:vpc] = 'VPC' 217 | end 218 | 219 | instances[row[1]][:platform] = row[7].sub(' (Amazon VPC)','') 220 | end 221 | end 222 | return instances 223 | end 224 | 225 | def get_related_recommendations(list_recommendations, recommendations, recommendation_full) 226 | list_recommendations << recommendation_full 227 | recommendations2 = recommendations.dup 228 | recommendations2.delete_at(recommendations2.index(recommendation_full)) 229 | Rails.logger.debug "BB" 230 | Rails.logger.debug recommendations2 231 | 232 | recommendation_full.each do |recommendation| 233 | recommendations2.each do |recommendation_full2| 234 | recommendation_full2.each do |recommendation2| 235 | if recommendation[:rid] == recommendation2[:rid] 236 | Rails.logger.debug "CC" 237 | Rails.logger.debug recommendation[:rid] 238 | list_recommendations = get_related_recommendations(list_recommendations, recommendations2, recommendation_full2) 239 | break 240 | end 241 | end 242 | end 243 | end 244 | 245 | return list_recommendations 246 | end 247 | 248 | def apply_recommendation(recommendations) 249 | Rails.logger.debug "Recomm" 250 | Rails.logger.debug recommendations 251 | modified_ris = [] 252 | recommendations.each do |recommendation_full| 253 | Rails.logger.debug "Recommendation proc" 254 | Rails.logger.debug recommendation_full 255 | next if modified_ris.include? recommendation_full[0][:rid] 256 | all_confs = [] 257 | reserved_instance_ids = [] 258 | ri = ReservedInstance.find_by(reservationid: recommendation_full[0][:rid]) 259 | accountid = ri.accountid 260 | region = ri.az[0..-2] 261 | role_arn = ri.rolearn 262 | list_recommendations = [] 263 | list_recommendations = get_related_recommendations(list_recommendations, recommendations, recommendation_full) 264 | Rails.logger.debug "AA" 265 | Rails.logger.debug list_recommendations 266 | list_recommendations.each do |recommendation_elem| 267 | recommendation = recommendation_elem[0] 268 | ri = ReservedInstance.find_by(reservationid: recommendation[:rid]) 269 | recommendation_elem.each do |r| 270 | reserved_instance_ids << r[:rid] if !reserved_instance_ids.include? r[:rid] 271 | modified_ris << r[:rid] if !modified_ris.include? r[:rid] 272 | end 273 | conf = {} 274 | conf[:availability_zone] = recommendation[:az].nil? ? ri.az : recommendation[:az] 275 | conf[:platform] = recommendation[:vpc].nil? ? (ri.network == 'VPC' ? 'EC2-VPC' : 'EC2-Classic') : (recommendation[:vpc] == 'VPC' ? 'EC2-VPC' : 'EC2-Classic') 276 | conf[:instance_count] = recommendation[:count] 277 | conf[:instance_type] = recommendation[:type].nil? ? ri.instancetype : recommendation[:type] 278 | all_confs << conf 279 | end 280 | 281 | reserved_instance_ids.each do |ri_id| 282 | ri = ReservedInstance.find_by(reservationid: ri_id) 283 | elements_moved = 0 284 | list_recommendations.each do |recommendation_full2| 285 | recommendation_full2.each do |recommendation2| 286 | if recommendation2[:rid] == ri_id 287 | elements_moved += recommendation2[:orig_count] 288 | end 289 | end 290 | end 291 | if ri.count - elements_moved > 0 292 | rest_conf = {} 293 | rest_conf[:availability_zone] = ri.az 294 | rest_conf[:platform] = ri.network == 'VPC' ? 'EC2-VPC' : 'EC2-Classic' 295 | rest_conf[:instance_count] = ri.count - elements_moved 296 | rest_conf[:instance_type] = ri.instancetype 297 | all_confs << rest_conf 298 | end 299 | end 300 | 301 | if ENV['MOCK_DATA'].blank? 302 | if accountid == get_current_account_id 303 | ec2 = Aws::EC2::Client.new(region: region) 304 | else 305 | role_credentials = Aws::AssumeRoleCredentials.new( client: Aws::STS::Client.new(region: region), role_arn: role_arn, role_session_name: "reserved_instances" ) 306 | ec2 = Aws::EC2::Client.new(region: region, credentials: role_credentials) 307 | end 308 | Rails.logger.debug "Confs" 309 | Rails.logger.debug all_confs 310 | new_confs = [] 311 | all_confs.each do |conf| 312 | if new_confs.index {|c| c[:availability_zone]==conf[:availability_zone] && c[:platform] == conf[:platform] && c[:instance_type] == conf[:instance_type]}.nil? 313 | new_conf = {:availability_zone => conf[:availability_zone], :platform => conf[:platform], :instance_type => conf[:instance_type]} 314 | total_count = 0 315 | all_confs.each do |c| 316 | if c[:availability_zone]==conf[:availability_zone] && c[:platform] == conf[:platform] && c[:instance_type] == conf[:instance_type] 317 | total_count += c[:instance_count] 318 | end 319 | end 320 | new_conf[:instance_count] = total_count 321 | new_confs << new_conf 322 | end 323 | end 324 | Rails.logger.debug "New Confs" 325 | Rails.logger.debug new_confs 326 | Rails.logger.debug "RIS" 327 | Rails.logger.debug reserved_instance_ids 328 | ec2.modify_reserved_instances(reserved_instances_ids: reserved_instance_ids, target_configurations: new_confs) 329 | end 330 | 331 | list_recommendations.each do |recommendation_full| 332 | recommendation_ids = [] 333 | recommendation_counts = [] 334 | recommendation_types = [] 335 | recommendation_azs = [] 336 | recommendation_vpcs = [] 337 | account_id = "" 338 | recommendation_full.each do |element| 339 | ri = ReservedInstance.find_by(reservationid: element[:rid]) 340 | region = ri.az[0..-2] 341 | account_id = ri.accountid 342 | recommendation_ids << element[:rid] 343 | recommendation_counts << element[:count] 344 | recommendation_types << element[:type] 345 | recommendation_azs << element[:az] 346 | recommendation_vpcs << element[:vpc] 347 | end 348 | 349 | if recommendation_ids.size > 0 350 | log = Recommendation.new 351 | log.accountid = account_id 352 | log.rid = recommendation_ids.join(",") 353 | log.az = recommendation_azs.join(",") 354 | log.vpc = recommendation_vpcs.join(",") 355 | log.instancetype = recommendation_types.join(",") 356 | log.counts = recommendation_counts.join(",") 357 | log.timestamp = DateTime.now 358 | log.save 359 | end 360 | end 361 | end 362 | end 363 | 364 | def get_s3_resource_for_bucket(bucket) 365 | s3 = Aws::S3::Client.new(region: 'us-east-1') 366 | begin 367 | location = s3.get_bucket_location({bucket: bucket}) 368 | s3 = Aws::S3::Client.new(region: location.location_constraint) if location.location_constraint != 'us-east-1' 369 | rescue 370 | return nil 371 | end 372 | return Aws::S3::Resource.new(client: s3) 373 | end 374 | 375 | def get_dbr_last_date(bucket, last_processed) 376 | return 1 if !ENV['MOCK_DATA'].blank? 377 | last_processed = Time.new(1000) if last_processed.blank? 378 | s3 = get_s3_resource_for_bucket(bucket) 379 | Rails.logger.debug "Error" if s3.nil? 380 | return nil if s3.nil? 381 | bucket = s3.bucket(bucket) 382 | last_modified = Time.new(1000) 383 | last_object = nil 384 | bucket.objects.each do |object| 385 | if object.key.include? 'aws-billing-detailed-line-items-with-resources-and-tags' 386 | if object.last_modified > last_modified && object.last_modified > last_processed 387 | last_modified = object.last_modified 388 | last_object = object 389 | end 390 | end 391 | end 392 | 393 | return last_object 394 | end 395 | 396 | def download_to_temp(bucket, object) 397 | file_path = File.join(Dir.tmpdir, Dir::Tmpname.make_tmpname('dbr',nil)) 398 | 399 | if !ENV['MOCK_DATA'].blank? 400 | FileUtils.cp '/tmp/dbr.csv.zip', file_path 401 | return file_path 402 | end 403 | 404 | s3 = get_s3_resource_for_bucket(bucket) 405 | return nil if s3.nil? 406 | object.get({response_target: file_path}) 407 | return file_path 408 | end 409 | 410 | def get_amis(list_instances, account_ids) 411 | return get_mock_amis(list_instances) if !ENV['MOCK_DATA'].blank? 412 | amis = {} 413 | current_account_id = get_current_account_id 414 | list_instances.each do |instance_id, values| 415 | # values -> [Operation, AccountId, AZ] 416 | account_id = nil 417 | account_ids.each do |acc| 418 | if acc[0] == values[1] 419 | account_id = acc 420 | break 421 | end 422 | end 423 | 424 | if !account_id.nil? 425 | region = values[2][0..-2] 426 | if account_id[0] == current_account_id 427 | ec2 = Aws::EC2::Resource.new(client: Aws::EC2::Client.new(region: region)) 428 | else 429 | role_credentials = Aws::AssumeRoleCredentials.new( client: Aws::STS::Client.new(region: region), role_arn: account_id[1], role_session_name: "reserved_instances" ) 430 | ec2 = Aws::EC2::Resource.new(client: Aws::EC2::Client.new(region: region, credentials: role_credentials)) 431 | end 432 | ec2.instances({instance_ids: [instance_id]}).each do |instance| 433 | amis[instance.image_id] = values[0] 434 | end 435 | end 436 | end 437 | return amis 438 | end 439 | 440 | def get_mock_amis(list_instances) 441 | amis = {} 442 | instances = get_mock_instances 443 | list_instances.each do |instance_id, values| 444 | # values -> [Operation, AccountId, AZ] 445 | if !instances[instance_id].nil? && !instances[instance_id][:ami].nil? 446 | amis[instances[instance_id][:ami]] = values[0] 447 | end 448 | end 449 | return amis 450 | end 451 | 452 | def get_factor(type) 453 | size = type.split(".")[1] 454 | return case size 455 | when "nano" 456 | 0.25 457 | when "micro" 458 | 0.5 459 | when "small" 460 | 1 461 | when "medium" 462 | 2 463 | when "large" 464 | 4 465 | when "xlarge" 466 | 8 467 | when "2xlarge" 468 | 16 469 | when "4xlarge" 470 | 32 471 | when "8xlarge" 472 | 64 473 | when "10xlarge" 474 | 80 475 | else 476 | 0 477 | end 478 | end 479 | 480 | def populatedb_data 481 | instances = get_instances(Setup.get_regions, get_account_ids) 482 | reserved_instances = get_reserved_instances(Setup.get_regions, get_account_ids) 483 | failed_modifications = get_failed_modifications(Setup.get_regions, get_account_ids) 484 | summary = get_summary(instances, reserved_instances) 485 | zones = get_zones(Setup.get_regions, get_account_ids) 486 | 487 | continue_iteration = true 488 | recommendations = [] 489 | instances2 = Marshal.load(Marshal.dump(instances)) 490 | reserved_instances2 = Marshal.load(Marshal.dump(reserved_instances)) 491 | summary2 = Marshal.load(Marshal.dump(summary)) 492 | while continue_iteration do 493 | excess = {} 494 | # Excess of Instances and Reserved Instances per set of interchangable types 495 | calculate_excess(summary2, excess) 496 | continue_iteration = iterate_recommendation(excess, instances2, summary2, reserved_instances2, recommendations, zones) 497 | end 498 | 499 | ActiveRecord::Base.transaction do 500 | Instance.delete_all 501 | ReservedInstance.delete_all 502 | Modification.delete_all 503 | Summary.delete_all 504 | RecommendationCache.delete_all 505 | instances.each do |instance_id, instance| 506 | new_instance = Instance.new 507 | new_instance.accountid = instance[:account_id] 508 | new_instance.instanceid = instance_id 509 | new_instance.instancetype = instance[:type] 510 | new_instance.az = instance[:az] 511 | new_instance.tenancy = instance[:tenancy] 512 | new_instance.platform = instance[:platform] 513 | new_instance.network = instance[:vpc] 514 | new_instance.save 515 | end 516 | 517 | reserved_instances.each do |ri_id, ri| 518 | new_ri = ReservedInstance.new 519 | new_ri.accountid = ri[:account_id] 520 | new_ri.reservationid = ri_id 521 | new_ri.instancetype = ri[:type] 522 | new_ri.az = ri[:az] 523 | new_ri.tenancy = ri[:tenancy] 524 | new_ri.platform = ri[:platform] 525 | new_ri.network = ri[:vpc] 526 | new_ri.count = ri[:count] 527 | new_ri.enddate = ri[:end] 528 | new_ri.status = ri[:status] 529 | new_ri.rolearn = ri[:role_arn] 530 | new_ri.offering = ri[:offering] 531 | new_ri.duration = ri[:duration] 532 | 533 | new_ri.save 534 | end 535 | 536 | failed_modifications.each do |modification| 537 | new_modification = Modification.new 538 | new_modification.modificationid = modification 539 | new_modification.status = 'failed' 540 | new_modification.save 541 | end 542 | 543 | summary.each do |type, elem1| 544 | elem1.each do |az, elem2| 545 | elem2.each do |platform, elem3| 546 | elem3.each do |tenancy, total| 547 | new_summary = Summary.new 548 | new_summary.instancetype = type 549 | new_summary.az = az 550 | new_summary.tenancy = tenancy 551 | new_summary.platform = platform 552 | new_summary.total = total[0] 553 | new_summary.reservations = total[1] 554 | new_summary.save 555 | end 556 | end 557 | end 558 | end 559 | recommendations.each do |recommendation| 560 | new_recommendation = RecommendationCache.new 561 | new_recommendation.object = Marshal.dump(recommendation) 562 | new_recommendation.save 563 | end 564 | end 565 | end 566 | 567 | def get_summary(instances, reserved_instances) 568 | summary = {} 569 | 570 | instances.each do |instance_id, instance| 571 | summary[instance[:type]] = {} if summary[instance[:type]].nil? 572 | summary[instance[:type]][instance[:az]] = {} if summary[instance[:type]][instance[:az]].nil? 573 | summary[instance[:type]][instance[:az]][instance[:platform]] = {} if summary[instance[:type]][instance[:az]][instance[:platform]].nil? 574 | summary[instance[:type]][instance[:az]][instance[:platform]][instance[:tenancy]] = [0,0] if summary[instance[:type]][instance[:az]][instance[:platform]][instance[:tenancy]].nil? 575 | summary[instance[:type]][instance[:az]][instance[:platform]][instance[:tenancy]][0] += 1 576 | end 577 | 578 | reserved_instances.each do |ri_id, ri| 579 | if ri[:status] == 'active' 580 | summary[ri[:type]] = {} if summary[ri[:type]].nil? 581 | summary[ri[:type]][ri[:az]] = {} if summary[ri[:type]][ri[:az]].nil? 582 | summary[ri[:type]][ri[:az]][ri[:platform]] = {} if summary[ri[:type]][ri[:az]][ri[:platform]].nil? 583 | summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]] = [0,0] if summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]].nil? 584 | summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][1] += ri[:count] 585 | end 586 | end 587 | 588 | return summary 589 | end 590 | def calculate_excess(summary, excess) 591 | # Group the excess of RIs and instances per family and region 592 | # For example, for m3 in eu-west-1, it calculate the total RIs not used and the total instances not assigned to an RI (in any family type and AZ) 593 | summary.each do |type, elem1| 594 | elem1.each do |az, elem2| 595 | elem2.each do |platform, elem3| 596 | elem3.each do |tenancy, total| 597 | if total[0] != total[1] 598 | family = type.split(".")[0] 599 | region = az[0..-2] 600 | excess[family] = {} if excess[family].nil? 601 | excess[family][region] = {} if excess[family][region].nil? 602 | excess[family][region][platform] = {} if excess[family][region][platform].nil? 603 | excess[family][region][platform][tenancy] = [0,0] if excess[family][region][platform][tenancy].nil? 604 | factor = get_factor(type) 605 | if total[0] > total[1] 606 | # [0] -> Total of instances without a reserved instance 607 | excess[family][region][platform][tenancy][0] += (total[0]-total[1])*factor 608 | else 609 | # [1] -> Total of reserved instances not used 610 | excess[family][region][platform][tenancy][1] += (total[1]-total[0])*factor 611 | end 612 | end 613 | end 614 | end 615 | end 616 | end 617 | end 618 | 619 | def iterate_recommendation(excess, instances, summary, reserved_instances, recommendations, zones) 620 | excess.each do |family, elem1| 621 | elem1.each do |region, elem2| 622 | elem2.each do |platform, elem3| 623 | elem3.each do |tenancy, total| 624 | if total[1] > 0 && total[0] > 0 625 | # There are reserved instances not used and instances on-demand 626 | if Setup.get_affinity 627 | return true if calculate_recommendation(instances, family, region, platform, tenancy, summary, reserved_instances, recommendations, true, zones) 628 | end 629 | return true if calculate_recommendation(instances, family, region, platform, tenancy, summary, reserved_instances, recommendations, false, zones) 630 | end 631 | end 632 | end 633 | end 634 | end 635 | return false 636 | end 637 | 638 | def calculate_recommendation(instances, family, region, platform, tenancy, summary, reserved_instances, recommendations, affinity, zones) 639 | excess_instance = [] 640 | 641 | instances.each do |instance_id, instance| 642 | if instance[:type].split(".")[0] == family && instance[:az][0..-2] == region && instance[:platform] == platform && instance[:tenancy] == tenancy 643 | # This instance is of the usable type 644 | if summary[instance[:type]][instance[:az]][instance[:platform]][instance[:tenancy]][0] > summary[instance[:type]][instance[:az]][instance[:platform]][instance[:tenancy]][1] 645 | # If for this instance type we have excess of instances 646 | excess_instance << instance_id 647 | end 648 | end 649 | end 650 | 651 | # First look for AZ changes 652 | reserved_instances.each do |ri_id, ri| 653 | if !ri.nil? && ri[:type].split(".")[0] == family && ri[:az][0..-2] == region && ri[:platform] == platform && ri[:tenancy] == tenancy && ri[:status] == 'active' 654 | # This reserved instance is of the usable type 655 | if summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][1] > summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][0] 656 | # If for this reservation type we have excess of RIs 657 | # I'm going to look for an instance which can use this reservation 658 | excess_instance.each do |instance_id| 659 | # Change with the same type 660 | if instances[instance_id][:type] == ri[:type] && (!affinity || instances[instance_id][:account_id] == ri[:account_id]) && (instances[instance_id][:az] != ri[:az]) && zones[ri[:account_id]].include?(instances[instance_id][:az]) 661 | recommendation = {rid: ri_id, count: 1, orig_count: 1} 662 | recommendation[:az] = instances[instance_id][:az] 663 | #Rails.logger.debug("Change in the RI #{ri_id}, to az #{instances[instance_id][:az]}") 664 | summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][1] -= 1 665 | summary[ri[:type]][instances[instance_id][:az]][ri[:platform]][ri[:tenancy]][1] += 1 666 | reserved_instances[ri_id][:count] -= 1 667 | reserved_instances[ri_id] = nil if reserved_instances[ri_id][:count] == 0 668 | recommendations << [recommendation] 669 | return true 670 | end 671 | end 672 | end 673 | end 674 | end 675 | 676 | # Now I look for type changes 677 | # Only for Linux instances 678 | if platform == 'Linux/UNIX' 679 | reserved_instances.each do |ri_id, ri| 680 | if !ri.nil? && ri[:type].split(".")[0] == family && ri[:az][0..-2] == region && ri[:platform] == platform && ri[:tenancy] == tenancy && ri[:status] == 'active' 681 | # This reserved instance is of the usable type 682 | if summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][1] > summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][0] 683 | # If for this reservation type we have excess of RIs 684 | # I'm going to look for an instance which can use this reservation 685 | excess_instance.each do |instance_id| 686 | if instances[instance_id][:type] != ri[:type] && (!affinity || instances[instance_id][:account_id] == ri[:account_id]) && zones[ri[:account_id]].include?(instances[instance_id][:az]) 687 | factor_instance = get_factor(instances[instance_id][:type]) 688 | factor_ri = get_factor(ri[:type]) 689 | recommendation = {rid: ri_id} 690 | recommendation[:type] = instances[instance_id][:type] 691 | recommendation[:az] = instances[instance_id][:az] if instances[instance_id][:az] != ri[:az] 692 | #recommendation[:vpc] = instances[instance_id][:vpc] if instances[instance_id][:vpc] != ri[:vpc] 693 | if factor_ri > factor_instance 694 | # Split the RI 695 | new_instances = factor_ri / factor_instance 696 | recommendation[:count] = new_instances.to_i 697 | recommendation[:orig_count] = 1 698 | #Rails.logger.debug("Change in the RI #{ri_id}, split in #{new_instances} to type #{instances[instance_id][:type]}") 699 | 700 | summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][1] -= 1 701 | summary[instances[instance_id][:type]][instances[instance_id][:az]][ri[:platform]][ri[:tenancy]][1] += new_instances 702 | reserved_instances[ri_id][:count] -= 1 703 | reserved_instances[ri_id] = nil if reserved_instances[ri_id][:count] == 0 704 | recommendations << [recommendation] 705 | return true 706 | else 707 | # Join the RI, I need more RIs to complete the needed factor of the instance 708 | ri_needed = factor_instance / factor_ri 709 | if (ri[:count] > ri_needed) && (summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][1]-ri_needed) >= summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][0] 710 | # We only need join part of this RI to reach to the needed number of instances 711 | recommendation[:count] = 1 712 | recommendation[:orig_count] = ri_needed 713 | summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][1] -= ri_needed 714 | summary[instances[instance_id][:type]][instances[instance_id][:az]][ri[:platform]][ri[:tenancy]][1] += 1 715 | reserved_instances[ri_id][:count] -= ri_needed 716 | reserved_instances[ri_id] = nil if reserved_instances[ri_id][:count] == 0 717 | recommendations << [recommendation] 718 | return true 719 | else 720 | # We need to find more RIs to join with this one 721 | list_ris = [ri] 722 | list_ri_ids = [ri_id] 723 | count_ri = [(summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][1]-summary[ri[:type]][ri[:az]][ri[:platform]][ri[:tenancy]][0]), ri[:count]].min 724 | list_ri_counts = [count_ri] 725 | factor_ri_needed = factor_instance - (factor_ri*count_ri) 726 | 727 | reserved_instances.each do |ri_id2, ri2| 728 | if !ri2.nil? && ri2[:type].split(".")[0] == family && ri2[:az][0..-2] == region && ri2[:platform] == platform && ri2[:tenancy] == tenancy && ri2[:status] == 'active' && ri2[:account_id] == ri[:account_id] && !list_ri_ids.include?(ri_id2) && ri[:end].change(:min => 0) == ri2[:end].change(:min => 0) && ri[:offering] == ri2[:offering] && ri[:duration] == ri2[:duration] 729 | if summary[ri2[:type]][ri2[:az]][ri2[:platform]][ri2[:tenancy]][1] > summary[ri2[:type]][ri2[:az]][ri2[:platform]][ri2[:tenancy]][0] 730 | factor_ri2 = get_factor(ri2[:type]) 731 | if factor_ri2 < factor_instance 732 | list_ris << ri2 733 | list_ri_ids << ri_id2 734 | count_ri = [(summary[ri2[:type]][ri2[:az]][ri2[:platform]][ri2[:tenancy]][1]-summary[ri2[:type]][ri2[:az]][ri2[:platform]][ri2[:tenancy]][0]), ri2[:count]].min 735 | if (factor_ri2*count_ri) > factor_ri_needed 736 | count_ri = factor_ri_needed/factor_ri2 737 | list_ri_counts << count_ri 738 | factor_ri_needed -= factor_ri2*count_ri 739 | break 740 | else 741 | list_ri_counts << count_ri 742 | factor_ri_needed -= factor_ri2*count_ri 743 | end 744 | end 745 | end 746 | end 747 | end 748 | if factor_ri_needed == 0 749 | recommendation_complex = [] 750 | summary[instances[instance_id][:type]][instances[instance_id][:az]][instances[instance_id][:platform]][instances[instance_id][:tenancy]][1] += 1 751 | list_ris.each_index do |i| 752 | recommendation = {rid: list_ri_ids[i]} 753 | recommendation[:type] = instances[instance_id][:type] 754 | recommendation[:az] = instances[instance_id][:az] if instances[instance_id][:az] != list_ris[i][:az] 755 | recommendation[:count] = 1 756 | recommendation[:orig_count] = list_ri_counts[i] 757 | summary[list_ris[i][:type]][list_ris[i][:az]][list_ris[i][:platform]][list_ris[i][:tenancy]][1] -= list_ri_counts[i] 758 | reserved_instances[list_ri_ids[i]][:count] -= list_ri_counts[i] 759 | reserved_instances[list_ri_ids[i]] = nil if reserved_instances[list_ri_ids[i]][:count] == 0 760 | recommendation_complex << recommendation 761 | end 762 | recommendations << recommendation_complex 763 | return true 764 | end 765 | end 766 | end 767 | end 768 | end 769 | end 770 | end 771 | end 772 | end 773 | 774 | return false 775 | 776 | end 777 | 778 | end 779 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/lib/tasks/.keep -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/log/.keep -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The page you were looking for doesn't exist.

    62 |

    You may have mistyped the address or the page may have moved.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The change you wanted was rejected.

    62 |

    Maybe you tried to change something you didn't have access to.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    We're sorry, but something went wrong.

    62 |
    63 |

    If you are the application owner check the logs for more information.

    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /public/assets/.sprockets-manifest-063585aea31bebddb7106e9f3aa4a334.json: -------------------------------------------------------------------------------- 1 | {"files":{"application-16bbbe34e9335a2d2f54086ea22499ea4ad5eab75357accd499959d1b147c540.js":{"logical_path":"application.js","mtime":"2015-06-18T16:25:57+00:00","size":436800,"digest":"16bbbe34e9335a2d2f54086ea22499ea4ad5eab75357accd499959d1b147c540","integrity":"sha256-Fru+NOkzWi0vVAhuoiSZ6krV6rdTV6zNSZlZ0bFHxUA="},"application-da073b160e591389edb8450c5a2b0862e1a7589d9fc7e727870c29c925805ecb.css":{"logical_path":"application.css","mtime":"2015-06-18T16:25:57+00:00","size":163911,"digest":"da073b160e591389edb8450c5a2b0862e1a7589d9fc7e727870c29c925805ecb","integrity":"sha256-2gc7Fg5ZE4ntuEUMWisIYuGnWJ2fx+cnhwwpySWAXss="},"fontawesome-webfont-9f8288933d2c87ab3cdbdbda5c9fa1e1e139b01c7c1d1983f91a13be85173975.eot":{"logical_path":"fontawesome-webfont.eot","mtime":"2015-06-09T07:55:54+00:00","size":72449,"digest":"9f8288933d2c87ab3cdbdbda5c9fa1e1e139b01c7c1d1983f91a13be85173975","integrity":"sha256-n4KIkz0sh6s829vaXJ+h4eE5sBx8HRmD+RoTvoUXOXU="},"fontawesome-webfont-4f1f9ffe01469bbd03b254ec563c304dd92276514110364148cb7ffdd75d3297.svg":{"logical_path":"fontawesome-webfont.svg","mtime":"2015-06-09T07:55:54+00:00","size":253487,"digest":"4f1f9ffe01469bbd03b254ec563c304dd92276514110364148cb7ffdd75d3297","integrity":"sha256-Tx+f/gFGm70DslTsVjwwTdkidlFBEDZBSMt//dddMpc="},"fontawesome-webfont-c2a9333b008247abd42354df966498b4c2f1aa51a10b7e178a4f5df2edea4ce1.ttf":{"logical_path":"fontawesome-webfont.ttf","mtime":"2015-06-09T07:55:54+00:00","size":141564,"digest":"c2a9333b008247abd42354df966498b4c2f1aa51a10b7e178a4f5df2edea4ce1","integrity":"sha256-wqkzOwCCR6vUI1TflmSYtMLxqlGhC34Xik9d8u3qTOE="},"fontawesome-webfont-66db52b456efe7e29cec11fa09421d03cb09e37ed1b567307ec0444fd605ce31.woff":{"logical_path":"fontawesome-webfont.woff","mtime":"2015-06-09T07:55:54+00:00","size":83760,"digest":"66db52b456efe7e29cec11fa09421d03cb09e37ed1b567307ec0444fd605ce31","integrity":"sha256-ZttStFbv5+Kc7BH6CUIdA8sJ437RtWcwfsBET9YFzjE="},"glyphicons-halflings-regular-62fcbc4796f99217282f30c654764f572d9bfd9df7de9ce1e37922fa3caf8124.eot":{"logical_path":"glyphicons-halflings-regular.eot","mtime":"2015-06-09T07:55:54+00:00","size":20290,"digest":"62fcbc4796f99217282f30c654764f572d9bfd9df7de9ce1e37922fa3caf8124","integrity":"sha256-Yvy8R5b5khcoLzDGVHZPVy2b/Z333pzh43ki+jyvgSQ="},"glyphicons-halflings-regular-cef3dffcef386be2c8d1307761717e2eb9f43c151f2da9f1647e9d454abf13a3.svg":{"logical_path":"glyphicons-halflings-regular.svg","mtime":"2015-06-09T07:55:54+00:00","size":62850,"digest":"cef3dffcef386be2c8d1307761717e2eb9f43c151f2da9f1647e9d454abf13a3","integrity":"sha256-zvPf/O84a+LI0TB3YXF+Lrn0PBUfLanxZH6dRUq/E6M="},"glyphicons-halflings-regular-e27b969ef04fed3b39000b7b977e602d6e6a2b1c8c0d618bebf6dd875243ea3c.ttf":{"logical_path":"glyphicons-halflings-regular.ttf","mtime":"2015-06-09T07:55:54+00:00","size":41236,"digest":"e27b969ef04fed3b39000b7b977e602d6e6a2b1c8c0d618bebf6dd875243ea3c","integrity":"sha256-4nuWnvBP7Ts5AAt7l35gLW5qKxyMDWGL6/bdh1JD6jw="},"glyphicons-halflings-regular-63faf0af44a428f182686f0d924bb30e369a9549630c7b98a969394f58431067.woff":{"logical_path":"glyphicons-halflings-regular.woff","mtime":"2015-06-09T07:55:54+00:00","size":23292,"digest":"63faf0af44a428f182686f0d924bb30e369a9549630c7b98a969394f58431067","integrity":"sha256-Y/rwr0SkKPGCaG8NkkuzDjaalUljDHuYqWk5T1hDEGc="},"twitter/bootstrap/glyphicons-halflings-white-f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4.png":{"logical_path":"twitter/bootstrap/glyphicons-halflings-white.png","mtime":"2015-06-09T07:55:54+00:00","size":8777,"digest":"f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4","integrity":"sha256-8ODZWpyKvN+r9GNI4tQoWCm7BJH19q8OBa9Sv/tjJMQ="},"twitter/bootstrap/glyphicons-halflings-d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de.png":{"logical_path":"twitter/bootstrap/glyphicons-halflings.png","mtime":"2015-06-09T07:55:54+00:00","size":12799,"digest":"d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de","integrity":"sha256-2Z4/oyxkEDLwgUmRSyjC3GrPLsYvcJh/Ilnqu/p/wN4="},"application-94e001c2897085d55a9d51b0ee3e758cb1914f5f1d4448aefbd44544350e7a42.css":{"logical_path":"application.css","mtime":"2015-06-18T16:34:28+00:00","size":138409,"digest":"94e001c2897085d55a9d51b0ee3e758cb1914f5f1d4448aefbd44544350e7a42","integrity":"sha256-lOABwolwhdVanVGw7j51jLGRT18dREiu+9RFRDUOekI="}},"assets":{"application.js":"application-16bbbe34e9335a2d2f54086ea22499ea4ad5eab75357accd499959d1b147c540.js","application.css":"application-94e001c2897085d55a9d51b0ee3e758cb1914f5f1d4448aefbd44544350e7a42.css","fontawesome-webfont.eot":"fontawesome-webfont-9f8288933d2c87ab3cdbdbda5c9fa1e1e139b01c7c1d1983f91a13be85173975.eot","fontawesome-webfont.svg":"fontawesome-webfont-4f1f9ffe01469bbd03b254ec563c304dd92276514110364148cb7ffdd75d3297.svg","fontawesome-webfont.ttf":"fontawesome-webfont-c2a9333b008247abd42354df966498b4c2f1aa51a10b7e178a4f5df2edea4ce1.ttf","fontawesome-webfont.woff":"fontawesome-webfont-66db52b456efe7e29cec11fa09421d03cb09e37ed1b567307ec0444fd605ce31.woff","glyphicons-halflings-regular.eot":"glyphicons-halflings-regular-62fcbc4796f99217282f30c654764f572d9bfd9df7de9ce1e37922fa3caf8124.eot","glyphicons-halflings-regular.svg":"glyphicons-halflings-regular-cef3dffcef386be2c8d1307761717e2eb9f43c151f2da9f1647e9d454abf13a3.svg","glyphicons-halflings-regular.ttf":"glyphicons-halflings-regular-e27b969ef04fed3b39000b7b977e602d6e6a2b1c8c0d618bebf6dd875243ea3c.ttf","glyphicons-halflings-regular.woff":"glyphicons-halflings-regular-63faf0af44a428f182686f0d924bb30e369a9549630c7b98a969394f58431067.woff","twitter/bootstrap/glyphicons-halflings-white.png":"twitter/bootstrap/glyphicons-halflings-white-f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4.png","twitter/bootstrap/glyphicons-halflings.png":"twitter/bootstrap/glyphicons-halflings-d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de.png"}} -------------------------------------------------------------------------------- /public/assets/fontawesome-webfont-66db52b456efe7e29cec11fa09421d03cb09e37ed1b567307ec0444fd605ce31.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/assets/fontawesome-webfont-66db52b456efe7e29cec11fa09421d03cb09e37ed1b567307ec0444fd605ce31.woff -------------------------------------------------------------------------------- /public/assets/fontawesome-webfont-9f8288933d2c87ab3cdbdbda5c9fa1e1e139b01c7c1d1983f91a13be85173975.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/assets/fontawesome-webfont-9f8288933d2c87ab3cdbdbda5c9fa1e1e139b01c7c1d1983f91a13be85173975.eot -------------------------------------------------------------------------------- /public/assets/fontawesome-webfont-c2a9333b008247abd42354df966498b4c2f1aa51a10b7e178a4f5df2edea4ce1.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/assets/fontawesome-webfont-c2a9333b008247abd42354df966498b4c2f1aa51a10b7e178a4f5df2edea4ce1.ttf -------------------------------------------------------------------------------- /public/assets/glyphicons-halflings-regular-62fcbc4796f99217282f30c654764f572d9bfd9df7de9ce1e37922fa3caf8124.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/assets/glyphicons-halflings-regular-62fcbc4796f99217282f30c654764f572d9bfd9df7de9ce1e37922fa3caf8124.eot -------------------------------------------------------------------------------- /public/assets/glyphicons-halflings-regular-63faf0af44a428f182686f0d924bb30e369a9549630c7b98a969394f58431067.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/assets/glyphicons-halflings-regular-63faf0af44a428f182686f0d924bb30e369a9549630c7b98a969394f58431067.woff -------------------------------------------------------------------------------- /public/assets/glyphicons-halflings-regular-e27b969ef04fed3b39000b7b977e602d6e6a2b1c8c0d618bebf6dd875243ea3c.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/assets/glyphicons-halflings-regular-e27b969ef04fed3b39000b7b977e602d6e6a2b1c8c0d618bebf6dd875243ea3c.ttf -------------------------------------------------------------------------------- /public/assets/twitter/bootstrap/glyphicons-halflings-d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/assets/twitter/bootstrap/glyphicons-halflings-d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de.png -------------------------------------------------------------------------------- /public/assets/twitter/bootstrap/glyphicons-halflings-white-f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/assets/twitter/bootstrap/glyphicons-halflings-white-f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- 1 | aa 2 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/fonts/glyphicons-halflings-regular.woff.1 -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /reservedinstances.cform: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Reserved Instances Management Tool", 4 | "Parameters": { 5 | "DBUser": { 6 | "Type": "String", 7 | "Default": "riuser", 8 | "Description": "Name of DB username", 9 | "MinLength": "1", 10 | "MaxLength": "16", 11 | "AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*", 12 | "ConstraintDescription": "must begin with a letter and contain only alphanumeric characters." 13 | }, 14 | "DBPassword": { 15 | "Type": "String", 16 | "Description": "Database password", 17 | "NoEcho": "true", 18 | "MinLength": "8", 19 | "MaxLength": "41", 20 | "AllowedPattern": "[a-zA-Z0-9]*", 21 | "ConstraintDescription": "must contain only alphanumeric characters." 22 | }, 23 | "RailsSecretKey": { 24 | "Type": "String", 25 | "Description": "Rails secret key, generated with 'rake secret'", 26 | "NoEcho": "true" 27 | }, 28 | "VPC": { 29 | "Type": "AWS::EC2::VPC::Id", 30 | "Description": "VPC to install the application" 31 | }, 32 | "DBSubnet1": { 33 | "Type": "AWS::EC2::Subnet::Id", 34 | "Description": "You need to select two subnets in the VPC selected for the DB" 35 | }, 36 | "DBSubnet2": { 37 | "Type": "AWS::EC2::Subnet::Id", 38 | "Description": "Second subnet in the VPC" 39 | }, 40 | "EC2Subnet": { 41 | "Type": "AWS::EC2::Subnet::Id", 42 | "Description": "Subnet in the VPC for the web server" 43 | }, 44 | "S3Bucket": { 45 | "Type": "String", 46 | "Description": "S3 Bucket with the source code", 47 | "Default": "amzsup" 48 | }, 49 | "S3Key": { 50 | "Type": "String", 51 | "Description": "S3 Key with the source code", 52 | "Default": "reservedinstances-master.zip" 53 | }, 54 | "KeyName": { 55 | "Description": "Name of an existing EC2 KeyPair to enable SSH access to the Elastic Beanstalk hosts", 56 | "Type": "AWS::EC2::KeyPair::KeyName", 57 | "ConstraintDescription": "must be the name of an existing EC2 KeyPair." 58 | }, 59 | "IamInstanceProfile": { 60 | "Description": "Instance profile created to allow the access to all the linked accounts", 61 | "Type": "String", 62 | "Default": "reservedinstances" 63 | }, 64 | "DefaultPassword": { 65 | "Type": "String", 66 | "Description": "Application default password, you can change it later in the tool", 67 | "NoEcho": "true" 68 | }, 69 | "SSLCertARN": { 70 | "Type": "String", 71 | "Description": "ARN of a SSL Cert (use 'aws iam list-server-certificates')" 72 | }, 73 | "WorkerInstanceType": { 74 | "Type": "String", 75 | "Default": "t2.micro", 76 | "AllowedValues": [ 77 | "t2.micro", 78 | "t2.small", 79 | "t2.medium", 80 | "t2.large", 81 | "m4.xlarge" 82 | ], 83 | "Description": "Increase the instance type for the worker if you have a big DBR file and you're going to use it." 84 | }, 85 | "BeanstalkStackName": { 86 | "Description": "Name of the beankstalk Stack (Ruby 2.2 with Passenger): http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/concepts.platforms.html#concepts.platforms.ruby", 87 | "Type": "String", 88 | "Default": "64bit Amazon Linux 2016.03 v2.1.2 running Ruby 2.2 (Passenger Standalone)" 89 | } 90 | }, 91 | "Resources": { 92 | "WebServerSecurityGroup": { 93 | "Type": "AWS::EC2::SecurityGroup", 94 | "Properties": { 95 | "GroupDescription": "Enable access via SSH", 96 | "SecurityGroupIngress": [ 97 | { 98 | "IpProtocol": "tcp", 99 | "FromPort": "22", 100 | "ToPort": "22", 101 | "CidrIp": "0.0.0.0/0" 102 | } 103 | ], 104 | "VpcId": { 105 | "Ref": "VPC" 106 | } 107 | } 108 | }, 109 | "DbSecurityByEC2SecurityGroup": { 110 | "Type": "AWS::RDS::DBSecurityGroup", 111 | "Properties": { 112 | "GroupDescription": "Ingress for Amazon Reserved Instaces DB", 113 | "EC2VpcId": { 114 | "Ref": "VPC" 115 | }, 116 | "DBSecurityGroupIngress": { 117 | "EC2SecurityGroupId": { 118 | "Ref": "WebServerSecurityGroup" 119 | } 120 | } 121 | } 122 | }, 123 | "MyDBSubnetGroup": { 124 | "Type": "AWS::RDS::DBSubnetGroup", 125 | "Properties": { 126 | "DBSubnetGroupDescription": "DB Subnet for Reserved Instances tool", 127 | "SubnetIds": [ 128 | { 129 | "Ref": "DBSubnet1" 130 | }, 131 | { 132 | "Ref": "DBSubnet2" 133 | } 134 | ] 135 | } 136 | }, 137 | "myDB": { 138 | "Type": "AWS::RDS::DBInstance", 139 | "Properties": { 140 | "AllocatedStorage": "5", 141 | "DBInstanceClass": "db.t2.micro", 142 | "Engine": "MySQL", 143 | "MasterUsername": { 144 | "Ref": "DBUser" 145 | }, 146 | "MasterUserPassword": { 147 | "Ref": "DBPassword" 148 | }, 149 | "StorageType": "standard", 150 | "DBName": "ritooldb", 151 | "DBSecurityGroups": [ 152 | { 153 | "Ref": "DbSecurityByEC2SecurityGroup" 154 | } 155 | ], 156 | "DBSubnetGroupName": { 157 | "Ref": "MyDBSubnetGroup" 158 | } 159 | } 160 | }, 161 | "RIApplication": { 162 | "Type": "AWS::ElasticBeanstalk::Application", 163 | "Properties": { 164 | "Description": "Reserved Instances Tool Application" 165 | } 166 | }, 167 | "RIApplicationVersion": { 168 | "Type": "AWS::ElasticBeanstalk::ApplicationVersion", 169 | "Properties": { 170 | "Description": "Version 1.0", 171 | "ApplicationName": { 172 | "Ref": "RIApplication" 173 | }, 174 | "SourceBundle": { 175 | "S3Bucket": { 176 | "Ref": "S3Bucket" 177 | }, 178 | "S3Key": { 179 | "Ref": "S3Key" 180 | } 181 | } 182 | } 183 | }, 184 | "RIEnvironment": { 185 | "Type": "AWS::ElasticBeanstalk::Environment", 186 | "Properties": { 187 | "ApplicationName": { 188 | "Ref": "RIApplication" 189 | }, 190 | "Description": "Reserved Instances Tool Application Web Server", 191 | "SolutionStackName": { "Ref": "BeanstalkStackName" }, 192 | "VersionLabel": { 193 | "Ref": "RIApplicationVersion" 194 | }, 195 | "OptionSettings": [ 196 | { 197 | "Namespace": "aws:autoscaling:launchconfiguration", 198 | "OptionName": "SecurityGroups", 199 | "Value": { 200 | "Ref": "WebServerSecurityGroup" 201 | } 202 | }, 203 | { 204 | "Namespace": "aws:autoscaling:launchconfiguration", 205 | "OptionName": "EC2KeyName", 206 | "Value": { 207 | "Ref": "KeyName" 208 | } 209 | }, 210 | { 211 | "Namespace": "aws:autoscaling:launchconfiguration", 212 | "OptionName": "InstanceType", 213 | "Value": "t2.micro" 214 | }, 215 | { 216 | "Namespace": "aws:ec2:vpc", 217 | "OptionName": "VPCId", 218 | "Value": { 219 | "Ref": "VPC" 220 | } 221 | }, 222 | { 223 | "Namespace": "aws:ec2:vpc", 224 | "OptionName": "Subnets", 225 | "Value": { 226 | "Ref": "EC2Subnet" 227 | } 228 | }, 229 | { 230 | "Namespace": "aws:ec2:vpc", 231 | "OptionName": "ELBSubnets", 232 | "Value": { 233 | "Ref": "EC2Subnet" 234 | } 235 | }, 236 | { 237 | "Namespace": "aws:ec2:vpc", 238 | "OptionName": "AssociatePublicIpAddress", 239 | "Value": "true" 240 | }, 241 | { 242 | "Namespace": "aws:autoscaling:asg", 243 | "OptionName": "MinSize", 244 | "Value": "1" 245 | }, 246 | { 247 | "Namespace": "aws:autoscaling:asg", 248 | "OptionName": "MaxSize", 249 | "Value": "1" 250 | }, 251 | { 252 | "Namespace": "aws:elasticbeanstalk:environment", 253 | "OptionName": "EnvironmentType", 254 | "Value": "LoadBalanced" 255 | }, 256 | { 257 | "Namespace": "aws:elb:loadbalancer", 258 | "OptionName": "LoadBalancerHTTPPort", 259 | "Value": "OFF" 260 | }, 261 | { 262 | "Namespace": "aws:elb:loadbalancer", 263 | "OptionName": "LoadBalancerHTTPSPort", 264 | "Value": "443" 265 | }, 266 | { 267 | "Namespace": "aws:elb:loadbalancer", 268 | "OptionName": "SSLCertificateId", 269 | "Value": { 270 | "Ref": "SSLCertARN" 271 | } 272 | }, 273 | { 274 | "Namespace": "aws:elasticbeanstalk:application:environment", 275 | "OptionName": "RDS_DB_NAME", 276 | "Value": "ritooldb" 277 | }, 278 | { 279 | "Namespace": "aws:elasticbeanstalk:application:environment", 280 | "OptionName": "RDS_HOSTNAME", 281 | "Value": { 282 | "Fn::GetAtt": [ 283 | "myDB", 284 | "Endpoint.Address" 285 | ] 286 | } 287 | }, 288 | { 289 | "Namespace": "aws:elasticbeanstalk:application:environment", 290 | "OptionName": "RDS_PASSWORD", 291 | "Value": { 292 | "Ref": "DBPassword" 293 | } 294 | }, 295 | { 296 | "Namespace": "aws:elasticbeanstalk:application:environment", 297 | "OptionName": "RDS_USERNAME", 298 | "Value": { 299 | "Ref": "DBUser" 300 | } 301 | }, 302 | { 303 | "Namespace": "aws:elasticbeanstalk:application:environment", 304 | "OptionName": "SECRET_KEY_BASE", 305 | "Value": { 306 | "Ref": "RailsSecretKey" 307 | } 308 | }, 309 | { 310 | "Namespace": "aws:elasticbeanstalk:application:environment", 311 | "OptionName": "DEFAULT_PASSWORD", 312 | "Value": { 313 | "Ref": "DefaultPassword" 314 | } 315 | }, 316 | { 317 | "Namespace": "aws:autoscaling:launchconfiguration", 318 | "OptionName": "IamInstanceProfile", 319 | "Value": { 320 | "Ref": "IamInstanceProfile" 321 | } 322 | } 323 | ] 324 | } 325 | }, 326 | "MyQueue": { 327 | "Type": "AWS::SQS::Queue", 328 | "Properties": { 329 | "QueueName": "ritoolqueue" 330 | } 331 | }, 332 | "RIEnvironmentWorker": { 333 | "Type": "AWS::ElasticBeanstalk::Environment", 334 | "Properties": { 335 | "ApplicationName": { 336 | "Ref": "RIApplication" 337 | }, 338 | "Description": "Reserved Instances Tool Worker to apply the recommendations automatically", 339 | "SolutionStackName": { "Ref": "BeanstalkStackName" }, 340 | "VersionLabel": { 341 | "Ref": "RIApplicationVersion" 342 | }, 343 | "Tier": { 344 | "Type": "SQS/HTTP", 345 | "Name": "Worker", 346 | "Version": " " 347 | }, 348 | "OptionSettings": [ 349 | { 350 | "Namespace": "aws:autoscaling:launchconfiguration", 351 | "OptionName": "SecurityGroups", 352 | "Value": { 353 | "Ref": "WebServerSecurityGroup" 354 | } 355 | }, 356 | { 357 | "Namespace": "aws:autoscaling:launchconfiguration", 358 | "OptionName": "EC2KeyName", 359 | "Value": { 360 | "Ref": "KeyName" 361 | } 362 | }, 363 | { 364 | "Namespace": "aws:autoscaling:launchconfiguration", 365 | "OptionName": "RootVolumeSize", 366 | "Value": 100 367 | }, 368 | { 369 | "Namespace": "aws:autoscaling:launchconfiguration", 370 | "OptionName": "InstanceType", 371 | "Value": { 372 | "Ref": "WorkerInstanceType" 373 | } 374 | }, 375 | { 376 | "Namespace": "aws:ec2:vpc", 377 | "OptionName": "VPCId", 378 | "Value": { 379 | "Ref": "VPC" 380 | } 381 | }, 382 | { 383 | "Namespace": "aws:ec2:vpc", 384 | "OptionName": "Subnets", 385 | "Value": { 386 | "Ref": "EC2Subnet" 387 | } 388 | }, 389 | { 390 | "Namespace": "aws:ec2:vpc", 391 | "OptionName": "AssociatePublicIpAddress", 392 | "Value": "true" 393 | }, 394 | { 395 | "Namespace": "aws:autoscaling:asg", 396 | "OptionName": "MinSize", 397 | "Value": "1" 398 | }, 399 | { 400 | "Namespace": "aws:autoscaling:asg", 401 | "OptionName": "MaxSize", 402 | "Value": "1" 403 | }, 404 | { 405 | "Namespace": "aws:elasticbeanstalk:sqsd", 406 | "OptionName": "InactivityTimeout", 407 | "Value": "1800" 408 | }, 409 | { 410 | "Namespace": "aws:elasticbeanstalk:sqsd", 411 | "OptionName": "VisibilityTimeout", 412 | "Value": "1850" 413 | }, 414 | { 415 | "Namespace": "aws:elasticbeanstalk:sqsd", 416 | "OptionName": "WorkerQueueURL", 417 | "Value": { 418 | "Ref": "MyQueue" 419 | } 420 | }, 421 | { 422 | "Namespace": "aws:elasticbeanstalk:environment", 423 | "OptionName": "EnvironmentType", 424 | "Value": "SingleInstance" 425 | }, 426 | { 427 | "Namespace": "aws:elasticbeanstalk:application:environment", 428 | "OptionName": "RDS_DB_NAME", 429 | "Value": "ritooldb" 430 | }, 431 | { 432 | "Namespace": "aws:elasticbeanstalk:application:environment", 433 | "OptionName": "RDS_HOSTNAME", 434 | "Value": { 435 | "Fn::GetAtt": [ 436 | "myDB", 437 | "Endpoint.Address" 438 | ] 439 | } 440 | }, 441 | { 442 | "Namespace": "aws:elasticbeanstalk:application:environment", 443 | "OptionName": "RDS_PASSWORD", 444 | "Value": { 445 | "Ref": "DBPassword" 446 | } 447 | }, 448 | { 449 | "Namespace": "aws:elasticbeanstalk:application:environment", 450 | "OptionName": "RDS_USERNAME", 451 | "Value": { 452 | "Ref": "DBUser" 453 | } 454 | }, 455 | { 456 | "Namespace": "aws:elasticbeanstalk:application:environment", 457 | "OptionName": "SECRET_KEY_BASE", 458 | "Value": { 459 | "Ref": "RailsSecretKey" 460 | } 461 | }, 462 | { 463 | "Namespace": "aws:elasticbeanstalk:application:environment", 464 | "OptionName": "DEFAULT_PASSWORD", 465 | "Value": { 466 | "Ref": "DefaultPassword" 467 | } 468 | }, 469 | { 470 | "Namespace": "aws:autoscaling:launchconfiguration", 471 | "OptionName": "IamInstanceProfile", 472 | "Value": { 473 | "Ref": "IamInstanceProfile" 474 | } 475 | } 476 | ] 477 | } 478 | } 479 | }, 480 | "Outputs": { 481 | "URL": { 482 | "Description": "The URL of the Application", 483 | "Value": { 484 | "Fn::Join": [ 485 | "", 486 | [ 487 | "https://", 488 | { 489 | "Fn::GetAtt": [ 490 | "RIEnvironment", 491 | "EndpointURL" 492 | ] 493 | } 494 | ] 495 | ] 496 | } 497 | } 498 | } 499 | } 500 | 501 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/test/controllers/.keep -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/test/fixtures/.keep -------------------------------------------------------------------------------- /test/fixtures/instances.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | accountid: 1 5 | instanceid: MyString 6 | instancetype: MyString 7 | az: MyString 8 | tenancy: MyString 9 | platform: MyString 10 | network: MyString 11 | 12 | two: 13 | accountid: 1 14 | instanceid: MyString 15 | instancetype: MyString 16 | az: MyString 17 | tenancy: MyString 18 | platform: MyString 19 | network: MyString 20 | -------------------------------------------------------------------------------- /test/fixtures/modifications.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | modificationid: MyString 5 | status: MyString 6 | 7 | two: 8 | modificationid: MyString 9 | status: MyString 10 | -------------------------------------------------------------------------------- /test/fixtures/recommendation_caches.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | object: MyText 5 | 6 | two: 7 | object: MyText 8 | -------------------------------------------------------------------------------- /test/fixtures/recommendations.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | ris: MyString 5 | az: MyString 6 | type: 7 | vpc: MyString 8 | count: 1 9 | 10 | two: 11 | ris: MyString 12 | az: MyString 13 | type: 14 | vpc: MyString 15 | count: 1 16 | -------------------------------------------------------------------------------- /test/fixtures/reserved_instances.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | accountid: MyString 5 | reservationid: MyString 6 | instancetype: MyString 7 | az: MyString 8 | tenancy: MyString 9 | platform: MyString 10 | network: MyString 11 | count: 1 12 | enddate: 2016-05-31 15:47:02 13 | status: MyString 14 | 15 | two: 16 | accountid: MyString 17 | reservationid: MyString 18 | instancetype: MyString 19 | az: MyString 20 | tenancy: MyString 21 | platform: MyString 22 | network: MyString 23 | count: 1 24 | enddate: 2016-05-31 15:47:02 25 | status: MyString 26 | -------------------------------------------------------------------------------- /test/fixtures/setups.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | regions: MyText 5 | 6 | two: 7 | regions: MyText 8 | -------------------------------------------------------------------------------- /test/fixtures/summaries.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | instancetype: MyString 5 | az: MyString 6 | tenancy: MyString 7 | platform: MyString 8 | total: 9 | reservations: 1 10 | 11 | two: 12 | instancetype: MyString 13 | az: MyString 14 | tenancy: MyString 15 | platform: MyString 16 | total: 17 | reservations: 1 18 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/test/integration/.keep -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/test/models/.keep -------------------------------------------------------------------------------- /test/models/instance_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class InstanceTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/modification_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ModificationTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/recommendation_cache_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RecommendationCacheTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/recommendation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RecommendationTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/reserved_instance_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ReservedInstanceTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/setup_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SetupTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/summary_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SummaryTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jros2300/reservedinstances/395369cf5ac87dd978e893109035ab39d9400095/vendor/assets/stylesheets/.keep --------------------------------------------------------------------------------