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 +4 -4
- data/README.md +41 -3
- data/lib/infrawrench/bulk_instances/aws_instance.rb +1 -1
- data/lib/infrawrench/bulk_instances/aws_pricing_data.rb +27 -8
- data/lib/infrawrench/bulk_instances/types.rb +10 -6
- data/lib/infrawrench/bulk_instances/version.rb +1 -1
- data/lib/infrawrench/bulk_instances.rb +41 -15
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d923efcb8b07fa8334b16dc8ecc899b7cd7f47f9ec31a393f589d1565bf5c6b
|
|
4
|
+
data.tar.gz: 83dfaa42f5798bd85d7b4e9b829a43a47566d2684ce8caa6edcd0cd645afc462
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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.
|
|
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
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Infrawrench
|
|
4
4
|
module BulkInstances
|
|
5
|
-
#
|
|
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
|
-
# "
|
|
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 |
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
)
|
|
@@ -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
|
|
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
|
|
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}
|
|
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
|
-
#
|
|
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?(/\
|
|
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(/\
|
|
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.
|
|
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
|
|
64
|
+
summary: AWS instance attributes and pricing (EC2, RDS, ElastiCache and more), with
|
|
65
|
+
full history, from Infrawrench.
|
|
65
66
|
test_files: []
|