ec2spot 0.0.2

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.
data/lib/ec2spot.rb ADDED
@@ -0,0 +1,180 @@
1
+ require "aws-sdk-ec2"
2
+ require "aws-sdk-pricing"
3
+ require "time"
4
+
5
+ class EC2Spot
6
+ private_class_method :new
7
+
8
+ def self.instance
9
+ @instance ||= new
10
+ end
11
+
12
+ def self.instance!
13
+ @instance = new
14
+ end
15
+
16
+ def prices(*instance_types)
17
+ json = spot_price_history(*instance_types).group_by(&:instance_type).map do |instance_type, spot_price_history|
18
+ prices = spot_price_history.map(&:spot_price).map(&:to_f)
19
+ {
20
+ instance: instance_type,
21
+ min: prices.min,
22
+ max: prices.max,
23
+ avg: prices.sum / prices.length,
24
+ full: instance_price(instance_type),
25
+ savings: (1 - (prices.sum / prices.length) / instance_price(instance_type)),
26
+ interrupts: spot_advisor_range(instance_type)
27
+ }
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def services(service_code, next_token: nil)
34
+ resp = @pricing_client.describe_services({
35
+ service_code: service_code,
36
+ format_version: "aws_v1",
37
+ next_token: next_token,
38
+ max_results: 100,
39
+ })
40
+ json = resp.services
41
+ if !resp.next_token.nil? && !resp.next_token.empty?
42
+ json += services(service_code, next_token: resp.next_token)
43
+ end
44
+ json
45
+ end
46
+
47
+ def attribute_values(service_code, attribute_name, next_token: nil)
48
+ resp = @pricing_client.get_attribute_values({
49
+ service_code: service_code,
50
+ attribute_name: attribute_name,
51
+ next_token: next_token,
52
+ max_results: 100,
53
+ })
54
+ json = resp.attribute_values
55
+ if !resp.next_token.nil? && !resp.next_token.empty?
56
+ json += attribute_values(service_code, attribute_name, next_token: resp.next_token)
57
+ end
58
+ json
59
+ end
60
+
61
+ def instance_price(instance_type)
62
+ data = products("AmazonEC2", instance_type)
63
+ data["terms"]["OnDemand"].first[1]["priceDimensions"].first[1]["pricePerUnit"]["USD"].to_f
64
+ end
65
+
66
+ def products(service_code, instance_type, next_token: nil)
67
+ resp = @pricing_client.get_products({
68
+ service_code: service_code,
69
+ filters: [
70
+ {
71
+ type: "TERM_MATCH",
72
+ field: "instanceType",
73
+ value: instance_type,
74
+ },
75
+ {
76
+ type: "TERM_MATCH",
77
+ field: "regionCode",
78
+ value: @region,
79
+ },
80
+ {
81
+ type: "TERM_MATCH",
82
+ field: "operatingSystem",
83
+ value: "Linux",
84
+ },
85
+ {
86
+ type: "TERM_MATCH",
87
+ field: "usagetype",
88
+ value: "USE2-BoxUsage:#{instance_type}",
89
+ },
90
+ {
91
+ type: "TERM_MATCH",
92
+ field: "operation",
93
+ value: "RunInstances",
94
+ }
95
+ ],
96
+ format_version: "aws_v1",
97
+ next_token: next_token,
98
+ max_results: 100,
99
+ })
100
+ json = resp.price_list
101
+ if resp.price_list.length > 1 || (!resp.next_token.nil? && !resp.next_token.empty?)
102
+ throw "Only expected one result, but got #{json.length}"
103
+ end
104
+ JSON.parse(json.first)
105
+ end
106
+
107
+ def instance_types()
108
+ resp = @ec2_client.describe_instance_type_offerings({
109
+ max_results: 1000
110
+ })
111
+ resp.instance_type_offerings
112
+ end
113
+
114
+ def spot_price_history(*instance_types, next_token: nil)
115
+ now = Time.now.utc
116
+ its_been_one_week = now - (60 * 60 * 24 * 7)
117
+ json = []
118
+ resp = @ec2_client.describe_spot_price_history({
119
+ end_time: now,
120
+ instance_types: instance_types,
121
+ product_descriptions: [
122
+ "Linux/UNIX (Amazon VPC)",
123
+ ],
124
+ start_time: its_been_one_week,
125
+ max_results: 1000,
126
+ next_token: next_token
127
+ })
128
+ json = resp.spot_price_history
129
+ if !resp.next_token.nil? && !resp.next_token.empty?
130
+ json += spot_price_history(*instance_types, next_token: resp.next_token)
131
+ end
132
+ json
133
+ end
134
+
135
+ def instance_type_info(*instance_types, next_token: nil)
136
+ json = []
137
+ resp = @ec2_client.describe_instance_types({
138
+ instance_types: instance_types,
139
+ next_token: next_token
140
+ })
141
+ json = resp.instance_types
142
+ if !resp.next_token.nil? && !resp.next_token.empty?
143
+ json += instance_type_info(*instance_types, next_token: resp.next_token)
144
+ end
145
+ json
146
+ end
147
+
148
+ def spot_advisor_range(instance_type)
149
+ @spot_advisor_ranges[@spot_advisor[instance_type]["r"]]
150
+ end
151
+
152
+ def initialize
153
+ @ec2_client = Aws::EC2::Client.new()
154
+ @pricing_client = Aws::Pricing::Client.new(region: "us-east-1")
155
+ @region = ENV.fetch("AWS_REGION", "us-east-2")
156
+ data = nil
157
+ begin
158
+ url = URI.parse("https://spot-bid-advisor.s3.amazonaws.com/spot-advisor-data.json")
159
+ http = Net::HTTP.new(url.host, url.port)
160
+ http.use_ssl = (url.scheme == 'https')
161
+ request = Net::HTTP::Get.new(url.path)
162
+ response = http.request(request)
163
+ data = JSON.parse(response.body)
164
+ rescue => e
165
+ puts "Failed to fetch spot-advisor-data.json from S3: #{e.message} using local file instead."
166
+ current_directory = File.dirname(__FILE__)
167
+ filepath = File.join(current_directory, "2024.02.02_SpotAdvisorData.json")
168
+ data = JSON.parse(File.read(filepath))
169
+ end
170
+ @spot_advisor = data["spot_advisor"][@region]["Linux"]
171
+ @spot_advisor_ranges = [
172
+ "<5%",
173
+ "5-10%",
174
+ "10-15%",
175
+ "15-20%",
176
+ ">20%",
177
+ ]
178
+ self
179
+ end
180
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ec2spot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Patrick Wiseman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-02-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-ec2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.437'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1.437'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-pricing
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '1.54'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '1.54'
41
+ description: ec2spot create uses the AWS API to fetch spot prices for a list of instance
42
+ types. It includes the min, max, avg, and ondemand prices over a seven day period.
43
+ It also loads an estimate of the interrupts from the spot advisor tool. It expect
44
+ the AWS client to be configured using one of the standard methods.
45
+ email: patrick@deft.services
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - lib/2024.02.02_SpotAdvisorData.json
51
+ - lib/ec2spot.rb
52
+ homepage: https://github.com/deftinc/ec2spot
53
+ licenses:
54
+ - MIT
55
+ metadata:
56
+ homepage_uri: https://github.com/deftinc/ec2spot
57
+ source_code_uri: https://github.com/deftinc/ec2spot/tree/0.0.2
58
+ rubygems_mfa_required: 'true'
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.2.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements:
74
+ - A configured AWS client with access to the EC2 and Pricing APIs.
75
+ rubygems_version: 3.4.10
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: Wrapper around the AWS SDK for pricing EC2 Spot Instances.
79
+ test_files: []