infrawrench-bulk_instances 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bccbd61e8ed4801a2ca745978807a8ec752b6b04cc8b5042cf92b01a7406ffeb
4
- data.tar.gz: 529c9965e71e9a194204cfdbba3b78be46ba6779a225d4e580ab83621df2f3db
3
+ metadata.gz: 1d923efcb8b07fa8334b16dc8ecc899b7cd7f47f9ec31a393f589d1565bf5c6b
4
+ data.tar.gz: 83dfaa42f5798bd85d7b4e9b829a43a47566d2684ce8caa6edcd0cd645afc462
5
5
  SHA512:
6
- metadata.gz: a26d838f6e2da662c1dd77bea3f021e509a679fb45b14dd86496d238f2dc71417f85a8c96651d90d2402e0be6244b8919b71e9da1171af308f883ea126f6c896
7
- data.tar.gz: 4045a6ef4cb8451a56de51a4b6215eb0ed2c7f9418b9cfcc7f665fa1e4c221dc1a91b2b80020e79392166e3a8b75ddfc6a86f2723919bbb29e27485e7c271a8a
6
+ metadata.gz: 92fd23d69988cad7b8d3c83d2e1118eb3820c843fa533b56ae3d9ecf72de448aada9ff6b86b7916d242fb4b9353bf172353addf3ef6d87bc43fec4d1f33a567d
7
+ data.tar.gz: 407c205234c2ab52905ebb3a7fb8a304b92d8a3e73cfbbd8fb5907dd4457348c92d95ef032609244fc713a8afdd418a12079d6a6dfe610811aa04fc54cae5c8c
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # infrawrench-bulk_instances
2
2
 
3
- AWS EC2 instance attributes and prices, with their full history, from the bundle
4
- Infrawrench publishes at `instances-bundles.infrawrench.com`.
3
+ AWS instance attributes and prices (EC2, RDS, ElastiCache and more), with their full
4
+ history, from the bundles Infrawrench publishes at `instances-bundles.infrawrench.com`.
5
5
 
6
6
  ```sh
7
7
  gem install infrawrench-bulk_instances
@@ -29,6 +29,43 @@ m5.pricing_history.reserved # every reserved price change, oldest first
29
29
  Any Infrawrench bearer credential works as `token:` (an `iwk_` API key, an agent
30
30
  credential or a WorkOS access token). It defaults to `INFRAWRENCH_API_KEY`.
31
31
 
32
+ ## Other AWS platforms
33
+
34
+ `download_aws_pricing_data(platform, **options)` loads any platform's bundle;
35
+ `download_aws_ec2_pricing_data(**options)` is the same as `download_aws_pricing_data("ec2", **options)`.
36
+ `Infrawrench::BulkInstances::AWS_PLATFORMS` lists the platforms:
37
+
38
+ | Platform | AWS service |
39
+ | --- | --- |
40
+ | `ec2` | EC2 |
41
+ | `rds` | RDS |
42
+ | `elasticache` | ElastiCache |
43
+ | `opensearch` | OpenSearch Service |
44
+ | `redshift` | Redshift |
45
+ | `docdb` | DocumentDB |
46
+ | `neptune` | Neptune |
47
+ | `memorydb` | MemoryDB |
48
+ | `dax` | DynamoDB Accelerator |
49
+ | `sagemaker` | SageMaker |
50
+ | `emr` | EMR |
51
+ | `gamelift` | GameLift |
52
+
53
+ The fields that pick a price differ by platform. `data.variant_keys` names them, and every
54
+ price has a `variant` hash keyed by them:
55
+
56
+ ```ruby
57
+ rds = Infrawrench::BulkInstances.download_aws_pricing_data("rds")
58
+ rds.variant_keys # ["databaseEngine", "databaseEdition", "licenseModel", "deploymentOption", ...]
59
+ rds["db.m5.large"].pricing.on_demand.find do |p|
60
+ p.region_code == "us-east-1" && p.variant["databaseEngine"] == "MySQL" &&
61
+ p.variant["deploymentOption"] == "Multi-AZ"
62
+ end&.hourly
63
+ ```
64
+
65
+ `operating_system`, `tenancy`, `license_model`, `pre_installed_sw` and `capacitystatus` are EC2's
66
+ variant fields. On other platforms they are `""`, unless the platform has a field with that name
67
+ (RDS has `licenseModel`).
68
+
32
69
  Requires Ruby 3.2 or later, and liblzma (part of macOS and of practically every
33
70
  Linux distribution; `xz-utils`/`xz` otherwise) to decompress downloads.
34
71
 
@@ -38,7 +75,8 @@ The bundle is downloaded xz-compressed (about 20 MB) and kept decompressed (abou
38
75
  210 MB) in `~/.infrawrench/aws_pricing_data` (`cache_path:`), so a cached load skips
39
76
  decompression. Each load asks the server which bundle is latest and downloads it
40
77
  only if it is not cached, deleting the older one. If the server cannot be
41
- reached, the newest cached bundle is used instead. Pass `cache_pricing_data: false`
78
+ reached, the newest cached bundle is used instead. Each platform's bundle is cached
79
+ under its own name (`rds-bundle-…json`), so loading one never evicts another. Pass `cache_pricing_data: false`
42
80
  to always download and never touch the disk. The cache is shared with the other
43
81
  Infrawrench SDKs.
44
82
 
@@ -138,7 +138,7 @@ module Infrawrench
138
138
  private
139
139
 
140
140
  def od_key(r)
141
- [r.region, r.operating_system, r.tenancy, r.license_model, r.pre_installed_sw, r.capacitystatus]
141
+ [r.region, *r.variant.values]
142
142
  end
143
143
  end
144
144
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Infrawrench
4
4
  module BulkInstances
5
- # The AWS EC2 pricing data, keyed by instance type.
5
+ # One AWS platform's pricing data, keyed by instance type.
6
6
  #
7
7
  # Nothing is decoded until it is asked for: #get wraps the raw rows, and each
8
8
  # reader on the returned AWSInstance decodes its part on first access and keeps
@@ -15,10 +15,12 @@ module Infrawrench
15
15
  # index into +compression_table+:
16
16
  #
17
17
  # {
18
+ # "offer" => "AmazonRDS", # EC2 bundles leave it out
18
19
  # "compression_table" => [String, ...],
19
20
  # "snapshots" => [Integer, ...],
20
21
  # "regions" => [[location, [[key, value], ...]], ...],
21
- # "variants" => [[os, tenancy, license, sw, capacity], ...],
22
+ # "variant_keys" => [String, ...], # EC2 bundles leave it out (see EC2_VARIANT_KEYS)
23
+ # "variants" => [[value, ...], ...], # one value per variant key
22
24
  # "reserved_terms" => [[lease, offering, purchase], ...],
23
25
  # "instances" => { "m5.large" => [attrs, attr_changes, base_price, on_demand, reserved, created_at, deleted_at] },
24
26
  # }
@@ -50,6 +52,18 @@ module Infrawrench
50
52
  end
51
53
  alias [] get
52
54
 
55
+ # The AWS offer code, e.g. +AmazonEC2+ or +AmazonRDS+.
56
+ # @return [String]
57
+ def offer
58
+ @raw["offer"] || "AmazonEC2"
59
+ end
60
+
61
+ # The names of the fields that pick a price, which every price's +variant+ is keyed by.
62
+ # @return [Array<String>]
63
+ def variant_keys
64
+ @raw["variant_keys"] || EC2_VARIANT_KEYS
65
+ end
66
+
53
67
  # The publication date of every AWS price list in the bundle, oldest first.
54
68
  # @return [Array<Time>]
55
69
  def snapshots
@@ -82,13 +96,18 @@ module Infrawrench
82
96
  # @api private
83
97
  # @return [Variant]
84
98
  def variant(i)
85
- @variants ||= @raw["variants"].map do |os, tenancy, license, sw, capacity|
99
+ @variants ||= @raw["variants"].map do |fields|
100
+ keys = variant_keys
101
+ raise "variant has #{fields.length} fields, expected #{keys.length}" if fields.length != keys.length
102
+
103
+ variant = keys.zip(fields).to_h { |k, v| [k, str(v)] }.freeze
86
104
  Variant.new(
87
- operating_system: str(os),
88
- tenancy: str(tenancy),
89
- license_model: str(license),
90
- pre_installed_sw: str(sw),
91
- capacitystatus: str(capacity),
105
+ variant:,
106
+ operating_system: variant.fetch("operatingSystem", ""),
107
+ tenancy: variant.fetch("tenancy", ""),
108
+ license_model: variant.fetch("licenseModel", ""),
109
+ pre_installed_sw: variant.fetch("preInstalledSw", ""),
110
+ capacitystatus: variant.fetch("capacitystatus", ""),
92
111
  )
93
112
  end.freeze
94
113
  @variants[i]
@@ -2,13 +2,17 @@
2
2
 
3
3
  module Infrawrench
4
4
  module BulkInstances
5
- # Every price and price change starts with its location:
5
+ # Every price and price change starts with its location and variant:
6
6
  #
7
7
  # - +region+: the AWS location name, e.g. <tt>US East (N. Virginia)</tt>.
8
8
  # - +region_code+: e.g. +us-east-1+. +nil+ for locations the price list never gave a code.
9
+ # - +variant+: every field that picks the price, as a frozen <tt>Hash{String => String}</tt> keyed
10
+ # by name in AWSPricingData#variant_keys order: for RDS, +databaseEngine+, +deploymentOption+ and so on.
11
+ # - +operating_system+, +tenancy+, +license_model+, +pre_installed_sw+, +capacitystatus+: EC2's
12
+ # variant fields. For other platforms these are <tt>""</tt> unless the platform shares the field.
9
13
 
10
14
  # The product fields that pick a price.
11
- Variant = Data.define(:operating_system, :tenancy, :license_model, :pre_installed_sw, :capacitystatus)
15
+ Variant = Data.define(:variant, :operating_system, :tenancy, :license_model, :pre_installed_sw, :capacitystatus)
12
16
 
13
17
  ReservedTerm = Data.define(:lease_contract_length, :offering_class, :purchase_option)
14
18
 
@@ -18,7 +22,7 @@ module Infrawrench
18
22
  # @return [Time] When this price took effect.
19
23
  OnDemandPrice = Data.define(
20
24
  :region, :region_code,
21
- :operating_system, :tenancy, :license_model, :pre_installed_sw, :capacitystatus,
25
+ :variant, :operating_system, :tenancy, :license_model, :pre_installed_sw, :capacitystatus,
22
26
  :hourly, :since,
23
27
  )
24
28
 
@@ -30,7 +34,7 @@ module Infrawrench
30
34
  # @return [Time]
31
35
  ReservedPrice = Data.define(
32
36
  :region, :region_code,
33
- :operating_system, :tenancy, :license_model, :pre_installed_sw, :capacitystatus,
37
+ :variant, :operating_system, :tenancy, :license_model, :pre_installed_sw, :capacitystatus,
34
38
  :lease_contract_length, :offering_class, :purchase_option,
35
39
  :hourly, :upfront, :since,
36
40
  )
@@ -47,7 +51,7 @@ module Infrawrench
47
51
  # @return [Float, nil] +nil+ means no longer offered from +at+ on.
48
52
  OnDemandPriceChange = Data.define(
49
53
  :at, :region, :region_code,
50
- :operating_system, :tenancy, :license_model, :pre_installed_sw, :capacitystatus,
54
+ :variant, :operating_system, :tenancy, :license_model, :pre_installed_sw, :capacitystatus,
51
55
  :hourly,
52
56
  )
53
57
 
@@ -59,7 +63,7 @@ module Infrawrench
59
63
  # @return [Float, nil]
60
64
  ReservedPriceChange = Data.define(
61
65
  :at, :region, :region_code,
62
- :operating_system, :tenancy, :license_model, :pre_installed_sw, :capacitystatus,
66
+ :variant, :operating_system, :tenancy, :license_model, :pre_installed_sw, :capacitystatus,
63
67
  :lease_contract_length, :offering_class, :purchase_option,
64
68
  :hourly, :upfront,
65
69
  )
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Infrawrench
4
4
  module BulkInstances
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  end
@@ -14,7 +14,7 @@ require_relative "bulk_instances/aws_pricing_data"
14
14
  require_relative "bulk_instances/aws_instance"
15
15
 
16
16
  module Infrawrench
17
- # AWS EC2 instance attributes and pricing, with full history, from Infrawrench.
17
+ # AWS instance attributes and pricing (EC2, RDS, ElastiCache and more), with full history, from Infrawrench.
18
18
  module BulkInstances
19
19
  DEFAULT_BASE_URL = "https://instances-bundles.infrawrench.com"
20
20
  USER_AGENT = "infrawrench/bulk-instances, ruby, 1.0"
@@ -42,12 +42,15 @@ module Infrawrench
42
42
  end
43
43
  private_constant :Util
44
44
 
45
+ # The AWS platforms Infrawrench publishes a bundle for. Any other lowercase name is
46
+ # requested as is, so a platform added later works without a new SDK.
47
+ AWS_PLATFORMS = %w[ec2 rds elasticache opensearch redshift docdb neptune memorydb dax sagemaker emr gamelift].freeze
48
+
49
+ # The variant keys of EC2 bundles, which don't list their own.
50
+ EC2_VARIANT_KEYS = %w[operatingSystem tenancy licenseModel preInstalledSw capacitystatus].freeze
51
+
45
52
  class << self
46
- # Downloads the AWS EC2 pricing data compressed blob and decompresses it.
47
- #
48
- # The bucket's +latest+ object names the newest bundle. With caching on, a bundle
49
- # is only downloaded when +latest+ names one that is not in the cache yet, and if
50
- # +latest+ cannot be fetched at all (offline, outage) the newest cached bundle is used.
53
+ # Downloads the AWS EC2 pricing data. The same as <tt>download_aws_pricing_data("ec2", ...)</tt>.
51
54
  #
52
55
  # @param token [String, nil] Your Infrawrench API token. Defaults to the +INFRAWRENCH_API_KEY+ environment variable.
53
56
  # @param cache_pricing_data [Boolean] Whether to cache the pricing data.
@@ -56,6 +59,29 @@ module Infrawrench
56
59
  # @return [AWSPricingData]
57
60
  # @raise [AWSPricingDataError]
58
61
  def download_aws_ec2_pricing_data(token: nil, cache_pricing_data: true, cache_path: nil, base_url: nil)
62
+ download_aws_pricing_data("ec2", token:, cache_pricing_data:, cache_path:, base_url:)
63
+ end
64
+
65
+ # Downloads one AWS platform's pricing data compressed blob and decompresses it.
66
+ #
67
+ # The bucket's <tt><platform>-latest</tt> object (+latest+ for EC2) names the newest bundle. With
68
+ # caching on, a bundle is only downloaded when that names one that is not in the cache yet,
69
+ # and if it cannot be fetched at all (offline, outage) the newest cached bundle is used.
70
+ #
71
+ # @param platform [String, Symbol] One of AWS_PLATFORMS, e.g. +"rds"+, or any other lowercase platform name.
72
+ # @param token [String, nil] Your Infrawrench API token. Defaults to the +INFRAWRENCH_API_KEY+ environment variable.
73
+ # @param cache_pricing_data [Boolean] Whether to cache the pricing data.
74
+ # @param cache_path [String, nil] The cache path. Defaults to <tt>~/.infrawrench/aws_pricing_data</tt>.
75
+ # @param base_url [String, nil] Where the bundles are served from. Defaults to https://instances-bundles.infrawrench.com
76
+ # @return [AWSPricingData]
77
+ # @raise [AWSPricingDataError]
78
+ def download_aws_pricing_data(platform, token: nil, cache_pricing_data: true, cache_path: nil, base_url: nil)
79
+ unless (platform.is_a?(String) || platform.is_a?(Symbol)) && platform.match?(/\A[a-z0-9]+\z/)
80
+ raise AWSPricingDataError, %("#{platform}" is not a platform name.)
81
+ end
82
+
83
+ # EC2's keys have no prefix because they were published before the other platforms.
84
+ prefix = platform.to_s == "ec2" ? "" : "#{platform}-"
59
85
  token = ENV.fetch("INFRAWRENCH_API_KEY", nil) if token.nil?
60
86
  if token.nil? || token.empty?
61
87
  raise AWSPricingDataError, "No Infrawrench API token: pass `token:` or set INFRAWRENCH_API_KEY."
@@ -65,15 +91,15 @@ module Infrawrench
65
91
  cache_path = (cache_path || File.join(Dir.home, ".infrawrench", "aws_pricing_data")).to_s
66
92
 
67
93
  begin
68
- latest = download("#{base_url}/latest", token, &:read_body).to_s.dup.force_encoding(Encoding::UTF_8).scrub.strip
94
+ latest = download("#{base_url}/#{prefix}latest", token, &:read_body).to_s.dup.force_encoding(Encoding::UTF_8).scrub.strip
69
95
  unless latest.match?(/\A[A-Za-z0-9._-]+\z/)
70
- raise AWSPricingDataError, %(latest holds "#{latest}", which is not a timestamp.)
96
+ raise AWSPricingDataError, %(#{prefix}latest holds "#{latest}", which is not a timestamp.)
71
97
  end
72
98
 
73
- name = "bundle-#{latest}.json"
99
+ name = "#{prefix}bundle-#{latest}.json"
74
100
  rescue StandardError => e
75
101
  # A rejected token is a real answer; anything else falls back to what we already have.
76
- cached = newest_cached(cache_path) if cache_pricing_data && !(e.is_a?(AWSPricingDataError) && e.status == 401)
102
+ cached = newest_cached(cache_path, prefix) if cache_pricing_data && !(e.is_a?(AWSPricingDataError) && e.status == 401)
77
103
  raise if cached.nil?
78
104
 
79
105
  name = cached
@@ -92,9 +118,9 @@ module Infrawrench
92
118
  tmp = File.join(cache_path, "#{name}.#{Process.pid}.tmp")
93
119
  File.binwrite(tmp, json)
94
120
  File.rename(tmp, File.join(cache_path, name))
95
- # Older bundles, including compressed ones cached by earlier versions of this SDK.
121
+ # This platform's older bundles, including compressed ones cached by earlier versions of this SDK.
96
122
  Dir.each_child(cache_path) do |f|
97
- FileUtils.rm_f(File.join(cache_path, f)) if f != name && f.match?(/\Abundle-.+\.json(\.xz)?\z/)
123
+ FileUtils.rm_f(File.join(cache_path, f)) if f != name && f.match?(/\A#{prefix}bundle-.+\.json(\.xz)?\z/)
98
124
  end
99
125
  end
100
126
  end
@@ -135,9 +161,9 @@ module Infrawrench
135
161
  raise AWSPricingDataError, "GET #{url} failed: #{e.message}"
136
162
  end
137
163
 
138
- # The newest bundle already in the cache directory, if any.
139
- def newest_cached(cache_path)
140
- Dir.children(cache_path).grep(/\Abundle-.+\.json\z/).max
164
+ # The newest bundle for a platform already in the cache directory, if any.
165
+ def newest_cached(cache_path, prefix)
166
+ Dir.children(cache_path).grep(/\A#{prefix}bundle-.+\.json\z/).max
141
167
  rescue SystemCallError
142
168
  nil
143
169
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: infrawrench-bulk_instances
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Infrawrench LLC
@@ -61,5 +61,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
61
  requirements: []
62
62
  rubygems_version: 3.6.9
63
63
  specification_version: 4
64
- summary: AWS EC2 instance attributes and pricing, with full history, from Infrawrench.
64
+ summary: AWS instance attributes and pricing (EC2, RDS, ElastiCache and more), with
65
+ full history, from Infrawrench.
65
66
  test_files: []