atlantic_net 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 299bbd01dc49308dc29dcd96726cb87d285ea780
4
+ data.tar.gz: 4a88ca4ab658e36d2304751256add890efb9aff5
5
+ SHA512:
6
+ metadata.gz: a71669c2dcb9c443e6a82dd7a2272b040b80bea41e449ce399caee070ccd86debe8a2ef2cadbfdc6924a9cfb7d2499d8de0c79c968ddb89964dbbe02c33ab5e4
7
+ data.tar.gz: 13edd8a8cb2ce77f949d6fbea6ed3b0e7fb5c3e6f1b711cc586d9d12859879cf4dd4879c5f69f6c7ca48eb72109529bc406cae5bd51915212f6f6be35c07a4ae
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ coverage
3
+ Gemfile.lock
4
+ .idea
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016 Jamie Starke
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # atlantic_net
2
+
3
+ [![Build Status](https://travis-ci.org/jrstarke/atlantic_net.png?branch=master)](https://travis-ci.org/jrstarke/atlantic_net)
4
+ [![Coverage Status](https://coveralls.io/repos/jrstarke/atlantic_net/badge.png)](https://coveralls.io/r/jrstarke/atlantic_net)
5
+
6
+ A lightweight ruby interface for interacting with the Atlantic.net API.
7
+
8
+ ## Installation
9
+
10
+ Add atlantic_net to your Gemfile:
11
+
12
+ ``` ruby
13
+ gem "atlantic_net"
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ``` ruby
19
+ # Require the atlantic_net library
20
+ require 'atlantic_net'
21
+
22
+ # Instantiate the client with your access key and private key
23
+ client = AtlanticNet.new(access_key, private_key)
24
+
25
+ # List instances
26
+ instances = client.list_instances
27
+
28
+ # Reboot an instance
29
+ subject.reboot_instance(instances.first["InstanceId"])
30
+ ```
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "atlantic_net"
3
+ spec.version = "0.1.1"
4
+ spec.authors = ["Jamie Starke"]
5
+ spec.email = ["git@jamiestarke.com"]
6
+ spec.description = "A Ruby wrapper of the Atlantic.net API"
7
+ spec.summary = "A Ruby wrapper of the Atlantic.net API"
8
+ spec.homepage = "http://github.com/jrstarke/atlantic_net"
9
+ spec.license = "MIT"
10
+
11
+ spec.files = `git ls-files`.split($/)
12
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
13
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
14
+ spec.require_paths = ["lib"]
15
+
16
+ spec.add_development_dependency "bundler", "~> 1.3"
17
+ spec.add_development_dependency "rake"
18
+ spec.add_development_dependency "rack-test"
19
+ spec.add_development_dependency "rspec", ">= 3.0.0"
20
+ spec.add_development_dependency "rspec-given"
21
+ spec.add_development_dependency "simplecov"
22
+ spec.add_development_dependency "coveralls"
23
+ spec.add_development_dependency "uuidtools"
24
+ spec.add_development_dependency "webmock"
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency 'pry-nav'
27
+ end
@@ -0,0 +1,260 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+ require 'securerandom'
4
+ require 'pry'
5
+ require 'json'
6
+
7
+ class AtlanticNetException < StandardError
8
+ attr_reader :atlantic_net_instance, :api_response
9
+
10
+ def initialize(instance, response, message)
11
+ super(message)
12
+ @atlantic_net_instance = instance
13
+ @api_response = response
14
+ end
15
+ end
16
+
17
+ class AtlanticNet
18
+
19
+ VERSION = "2010-12-30"
20
+ FORMAT = "json"
21
+
22
+ class HttpTransport
23
+
24
+ API_URI = "https://cloudapi.atlantic.net"
25
+
26
+ # Sends the request to the API endpoint
27
+ #
28
+ # @param [Hash] data The Data to pass with the request
29
+ #
30
+ def send_request (data)
31
+ uri = URI.parse(API_URI)
32
+ uri.query = URI.encode_www_form(data)
33
+
34
+ http = Net::HTTP.new(uri.host, uri.port)
35
+ http.use_ssl = true
36
+
37
+ response = http.get(uri.request_uri)
38
+
39
+ unless response.code.to_i == 200
40
+ fail AtlanticNetException.new(nil, {}, "The Atlantic.net api endpoint was unexpectedly unavailable. The HTTP Status code was #{response.code}")
41
+ end
42
+
43
+ JSON.parse(response.body)
44
+ end
45
+ end
46
+
47
+ # @param [String] access_key The Access Key for your Account.
48
+ # This is the "API Public Key" from the Account Settings page.
49
+ #
50
+ # @param [String] private_key The Private Key for your Account.
51
+ # This is the "API Private Key" from the Account Settings page.
52
+ #
53
+ def initialize(access_key, private_key, options = {})
54
+ @access_key = access_key
55
+ @private_key = private_key
56
+ @transport = transport_from_options(options)
57
+ end
58
+
59
+ # Generates a Base64 encoded Sha256 HMAC given a timestamp and a request_id
60
+ #
61
+ # @param [String,Int] timestamp The timestamp of the requesting call.
62
+ # This should be the time the the request was originally made.
63
+ #
64
+ # @param [String] request_id The UUID representing the request.
65
+ # This should be unique for each request.
66
+ #
67
+ # @return [String] A Base64 encoded HMAC of the timestamp and request_id
68
+ #
69
+ def signature(timestamp, request_id)
70
+ string_to_sign = "#{timestamp}#{request_id}"
71
+
72
+ digest = OpenSSL::Digest.new('sha256')
73
+ Base64.encode64(OpenSSL::HMAC.digest(digest, @private_key, string_to_sign)).strip()
74
+ end
75
+
76
+ # Retrieve the list of currently active cloud servers.
77
+ #
78
+ # @return [Array<Hash>] The list of servers
79
+ #
80
+ def list_instances
81
+ response = api_call('list-instances')
82
+ response['list-instancesresponse']['instancesSet'].values
83
+ end
84
+
85
+ # Restart a specific cloud server.
86
+ #
87
+ # @param [String] instance_id The instance ID of the server you want to reboot.
88
+ #
89
+ # @param [Hash] options The options to include with restart.
90
+ # @option options [String] :reboot_type (soft) Whether you want to perform a soft or hard reboot.
91
+ # soft is the equivalent of performing a graceful shutdown.
92
+ # hard is the equivalent of disconnecting the power and reconnecting it.
93
+ #
94
+ # @return [Hash] The return value of the request
95
+ #
96
+ def reboot_instance(instance_id, options={})
97
+ reboot_type = options[:reboot_type] || "soft"
98
+ response = api_call('reboot-instance', {instanceid: instance_id, reboottype: reboot_type})
99
+ response["reboot-instanceresponse"]["return"]
100
+ end
101
+
102
+ # Describe a specific cloud server.
103
+ #
104
+ # @param [String] instance_id The instance ID of the server you want to describe.
105
+ #
106
+ # @return [Hash] The hash of the instance
107
+ #
108
+ def describe_instance(instance_id)
109
+ response = api_call('describe-instance', {instanceid: instance_id})
110
+ response["describe-instanceresponse"]["instanceSet"]["item"]
111
+ end
112
+
113
+ # Terminate a specific cloud server.
114
+ #
115
+ # @param [String] instance_id The instance ID of the server you want to terminate.
116
+ #
117
+ # @return [Hash] The termination response result.
118
+ #
119
+ def terminate_instance(instance_id)
120
+ response = api_call('terminate-instance', {instanceid: instance_id})
121
+ response["terminate-instanceresponse"]["instancesSet"]["item"]
122
+ end
123
+
124
+ # Run a cloud server with the provided configuration options.
125
+ #
126
+ # @param [String] server_name The name/description that will be used for the instance
127
+ #
128
+ # @param [String] plan_name The plan that should be used for this instance
129
+ #
130
+ # @param [String] vm_location The data center location you want this instance launched
131
+ #
132
+ # @param [Hash] args A hash of the options
133
+ # @option options [String] :image_id The VM image to use for this instance. This or :clone_image are required.
134
+ # @option options [String] :clone_image The server to clone for this instance. This or :image_id are required.
135
+ # @option options [Boolean] :enable_backup Whether backups should be enabled for this instance.
136
+ # @option options [Int] :server_quantity How many instances of this server should be launched.
137
+ # @option options [String] :key_id The ID of the key to deploy for accessing this server.
138
+ #
139
+ # @return [Hash] The run instance result
140
+ #
141
+ def run_instance(server_name, plan_name, vm_location, options={})
142
+ option_mapping = {
143
+ image_id: :imageid,
144
+ clone_image: :cloneimage,
145
+ enable_backup: :enablebackup,
146
+ server_quantity: :serverqty,
147
+ key_id: :key_id
148
+ }
149
+
150
+ request_options = {servername: server_name, planname: plan_name, vm_location: vm_location}
151
+
152
+ unless options.has_key? :image_id or options.has_key? :clone_image
153
+ fail ArgumentError.new("Missing argument: image_id or clone_image are required")
154
+ end
155
+
156
+ option_mapping.each do | key, value |
157
+ if options.has_key? key
158
+ request_options[value] = options[key]
159
+ end
160
+ end
161
+
162
+ response = api_call('run-instance', request_options)
163
+ response["run-instanceresponse"]["instancesSet"]["item"]
164
+ end
165
+
166
+ # Retrieve the description of all available cloud images or the description of a specific cloud image by providing the image id
167
+ #
168
+ # @param [Hash] options A hash of the options
169
+ # @option options [String] :image_id ID indicating the flavor and version number of the operating system image
170
+ #
171
+ # @return [Array<Hash>] A array of image descriptions
172
+ #
173
+ def describe_images(options={})
174
+ args = {}
175
+
176
+ if options.has_key? :image_id
177
+ response = api_call('describe-image', {imageid: options[:image_id]})
178
+ else
179
+ response = api_call('describe-image')
180
+ end
181
+
182
+ response["describe-imageresponse"]["imagesset"].values
183
+ end
184
+
185
+ # Retrieve a list of available cloud server plans, narrow the listing down optionally
186
+ # by server platform, or get information about just one specific plan
187
+ #
188
+ # @param [Hash] options A hash of the options
189
+ # @option options [String] plan_name The name of the plan to describe
190
+ # @option options [String] platform The platform to filter plans by (windows, linux)
191
+ #
192
+ # @return [Array<Hash>] An array of plan descriptions
193
+ #
194
+ def describe_plans(options={})
195
+ if options.empty?
196
+ response = api_call('describe-plan')
197
+ else
198
+ response = api_call('describe-plan',options)
199
+ end
200
+
201
+ response["describe-planresponse"]["plans"].values
202
+ end
203
+
204
+ # Retrieve the details of all SSH Keys associated with the account
205
+ #
206
+ # @return [Array<Hash>] An array of SSH keys
207
+ #
208
+ def list_ssh_keys
209
+ response = api_call('list-sshkeys')
210
+ response["list-sshkeysresponse"]["KeysSet"].values
211
+ end
212
+
213
+
214
+ protected
215
+
216
+ # Generate the next timestamp and request_id combination
217
+ #
218
+ # @return [Int, String] timestamp, request_id
219
+ #
220
+ def generate_request_id_tuple
221
+ timestamp = Time.now().to_i
222
+ request_uuid = SecureRandom.uuid
223
+ [timestamp, request_uuid]
224
+ end
225
+
226
+ # Performs the request and sends it off to the transport
227
+ #
228
+ # @param [String] action The API Action to perform.
229
+ #
230
+ # @param [Hash] args The arguments to pass with the request to the API.
231
+ #
232
+ def api_call(action, args = {})
233
+ timestamp, request_id = generate_request_id_tuple()
234
+ request_signature = signature(timestamp, request_id)
235
+
236
+ args = args.merge(
237
+ Version: VERSION,
238
+ ACSAccessKeyId: @access_key,
239
+ Format: FORMAT,
240
+ Timestamp: timestamp,
241
+ Rndguid: request_id,
242
+ Signature: request_signature,
243
+ Action: action
244
+ )
245
+
246
+ response = @transport.send_request(args)
247
+ if response.has_key? "error"
248
+ fail AtlanticNetException.new(self, response, response["error"]["message"])
249
+ end
250
+
251
+ response
252
+ end
253
+
254
+ private
255
+
256
+ def transport_from_options(options)
257
+ options[:transport] || AtlanticNet::HttpTransport.new
258
+ end
259
+
260
+ end
@@ -0,0 +1,608 @@
1
+ require "spec_helper"
2
+
3
+ describe AtlanticNet do
4
+ let(:subject) { AtlanticNet.new(access_key, private_key, options) }
5
+
6
+ let(:access_key) { "public_key" }
7
+ let(:private_key) { "secret" }
8
+
9
+ let(:options) { { transport: transport } }
10
+ let(:transport) { double(:transport, send_request: {}) }
11
+
12
+ let(:sample_timestamp) { "Mess" }
13
+ let(:sample_request_id) { "age" }
14
+ let(:sample_signature) { "qnR8UCqJggD55PohusaBNviGoOJ67HC6Btry4qXLVZc=" }
15
+
16
+ describe "#signature" do
17
+ it 'returns sample signature' do
18
+ signature = subject.signature(sample_timestamp,sample_request_id)
19
+ expect(signature).to eq sample_signature
20
+ end
21
+ end
22
+
23
+ describe "#list_instances" do
24
+ let(:sample_api_response) {
25
+ {
26
+ "Timestamp" => 1440018626,
27
+ "list-instancesresponse" => {
28
+ "instancesSet" => {
29
+ "1item" => {
30
+ "InstanceId" => "145607",
31
+ "cu_id" => "17",
32
+ "rate_per_hr" => "0.0341",
33
+ "vm_bandwidth" => "600",
34
+ "vm_cpu_req" => "1",
35
+ "vm_created_date" => "1438048503",
36
+ "vm_description" => "New",
37
+ "vm_disk_req" => "40",
38
+ "vm_image" => "CentOS-7.1-cPanel_64bit",
39
+ "vm_image_display_name" => "CentOS 6.5 64bit Server - cPanel/WHM",
40
+ "vm_ip_address" => "209.208.65.177",
41
+ "vm_name" => "17-145607",
42
+ "vm_network_req" => "1",
43
+ "vm_os_architecture" => "64",
44
+ "vm_plan_name" => "S",
45
+ "vm_ram_req" => "1024",
46
+ "vm_status" => "RUNNING"
47
+ },
48
+ "item" => {
49
+ "InstanceId" => "153979",
50
+ "cu_id" => "17",
51
+ "rate_per_hr" => "0.0547",
52
+ "vm_bandwidth" => "600",
53
+ "vm_cpu_req" => "2",
54
+ "vm_created_date" => "1440018294",
55
+ "vm_description" => "apitestserver",
56
+ "vm_disk_req" => "100",
57
+ "vm_image" => "ubuntu-14.04_64bit",
58
+ "vm_image_display_name" => "ubuntu-14.04_64bit",
59
+ "vm_ip_address" => "45.58.35.251",
60
+ "vm_name" => "17-153979",
61
+ "vm_network_req" => "1",
62
+ "vm_os_architecture" => "64",
63
+ "vm_plan_name" => "L",
64
+ "vm_ram_req" => "4096",
65
+ "vm_status" => "RUNNING"
66
+ }
67
+ },
68
+ "requestid" => "c2a1bc2a-4440-438a-bd28-74dbc10a4047"
69
+ }
70
+ }
71
+ }
72
+ let(:expected_instances) {
73
+ [
74
+ {
75
+ "InstanceId" => "145607",
76
+ "cu_id" => "17",
77
+ "rate_per_hr" => "0.0341",
78
+ "vm_bandwidth" => "600",
79
+ "vm_cpu_req" => "1",
80
+ "vm_created_date" => "1438048503",
81
+ "vm_description" => "New",
82
+ "vm_disk_req" => "40",
83
+ "vm_image" => "CentOS-7.1-cPanel_64bit",
84
+ "vm_image_display_name" => "CentOS 6.5 64bit Server - cPanel/WHM",
85
+ "vm_ip_address" => "209.208.65.177",
86
+ "vm_name" => "17-145607",
87
+ "vm_network_req" => "1",
88
+ "vm_os_architecture" => "64",
89
+ "vm_plan_name" => "S",
90
+ "vm_ram_req" => "1024",
91
+ "vm_status" => "RUNNING"
92
+ },
93
+ {
94
+ "InstanceId" => "153979",
95
+ "cu_id" => "17",
96
+ "rate_per_hr" => "0.0547",
97
+ "vm_bandwidth" => "600",
98
+ "vm_cpu_req" => "2",
99
+ "vm_created_date" => "1440018294",
100
+ "vm_description" => "apitestserver",
101
+ "vm_disk_req" => "100",
102
+ "vm_image" => "ubuntu-14.04_64bit",
103
+ "vm_image_display_name" => "ubuntu-14.04_64bit",
104
+ "vm_ip_address" => "45.58.35.251",
105
+ "vm_name" => "17-153979",
106
+ "vm_network_req" => "1",
107
+ "vm_os_architecture" => "64",
108
+ "vm_plan_name" => "L",
109
+ "vm_ram_req" => "4096",
110
+ "vm_status" => "RUNNING"
111
+ }
112
+ ]
113
+ }
114
+
115
+ # it 'calls api_call with list-instances api' do
116
+ # expect(subject).to receive(:api_call).with("list-instances").and_return(sample_api_response)
117
+ # subject.list_instances
118
+ # end
119
+
120
+ it 'calls api_call with list-instances and returns a list of instance hashes' do
121
+ allow(subject).to receive(:api_call).with("list-instances").and_return(sample_api_response)
122
+ instances = subject.list_instances
123
+ expect(instances).to eq expected_instances
124
+ end
125
+ end
126
+
127
+ describe '#reboot_instance' do
128
+ let(:instance_id) { "234" }
129
+ let(:sample_api_response) {
130
+ {
131
+ "Timestamp" => 1440171938,
132
+ "reboot-instanceresponse" => {
133
+ "requestid" => "6723430f-c416-46de-a03d-2d2ea38d3af7",
134
+ "return" => {
135
+ "Message" => "Successfully queued for reboot",
136
+ "value" => "true"
137
+ }
138
+ }
139
+ }
140
+ }
141
+ let(:expected_return) {
142
+ {
143
+ "Message" => "Successfully queued for reboot",
144
+ "value" => "true"
145
+ }
146
+ }
147
+
148
+ context "default options" do
149
+ let(:reboot_type) { "soft" }
150
+
151
+ it 'calls api_call with reboot-instance and instance_id' do
152
+ expect(subject).to receive(:api_call)
153
+ .with("reboot-instance", {instanceid: instance_id, reboottype: reboot_type})
154
+ .and_return(sample_api_response)
155
+ result = subject.reboot_instance(instance_id)
156
+ expect(result).to eq expected_return
157
+ end
158
+ end
159
+
160
+ context "reboot_type override" do
161
+ let(:reboot_type) { "hard" }
162
+
163
+ it 'calls api_call with reboot-instance, instance_id and reboot_type' do
164
+ expect(subject).to receive(:api_call)
165
+ .with("reboot-instance", {instanceid: instance_id, reboottype: reboot_type})
166
+ .and_return(sample_api_response)
167
+ result = subject.reboot_instance(instance_id, reboot_type: reboot_type)
168
+ expect(result).to eq expected_return
169
+ end
170
+ end
171
+ end
172
+
173
+ describe "#describe_instance" do
174
+ let(:instance_id) { "234" }
175
+ let(:sample_api_response) {
176
+ {
177
+ "Timestamp" => 1440020019,
178
+ "describe-instanceresponse" => {
179
+ "instanceSet" => {
180
+ "item" => {
181
+ "InstanceId" => "153979",
182
+ "cloned_from" => "",
183
+ "cu_id" => "17",
184
+ "disallow_deletion" => "N",
185
+ "rate_per_hr" => "0.0547",
186
+ "removed" => "N",
187
+ "reprovisioning_processed_date" => nil,
188
+ "resetpwd_processed_date" => nil,
189
+ "vm_bandwidth" => "600",
190
+ "vm_cpu_req" => "2",
191
+ "vm_created_date" => "1440018294",
192
+ "vm_description" => "apitestserver",
193
+ "vm_disk_req" => "100",
194
+ "vm_id" => "153979",
195
+ "vm_image" => "ubuntu-14.04_64bit",
196
+ "vm_image_display_name" => "ubuntu-14.04_64bit",
197
+ "vm_ip_address" => "45.58.35.251",
198
+ "vm_ip_gateway" => "45.58.34.1",
199
+ "vm_ip_subnet" => "255.255.254.0",
200
+ "vm_network_req" => "1",
201
+ "vm_os_architecture" => "64",
202
+ "vm_plan_name" => "L",
203
+ "vm_ram_req" => "4096",
204
+ "vm_removed_date" => nil,
205
+ "vm_status" => "RUNNING",
206
+ "vm_username" => "root",
207
+ "vm_vnc_password" => "8$EOs$Rs",
208
+ "vnc_port" => "18248"
209
+ }
210
+ },
211
+ "requestid" => "3affbe87-ad45-41de-a1d2-29b522aa88b2"
212
+ }
213
+ }
214
+ }
215
+ let(:expected_instance) {
216
+ {
217
+ "InstanceId" => "153979",
218
+ "cloned_from" => "",
219
+ "cu_id" => "17",
220
+ "disallow_deletion" => "N",
221
+ "rate_per_hr" => "0.0547",
222
+ "removed" => "N",
223
+ "reprovisioning_processed_date" => nil,
224
+ "resetpwd_processed_date" => nil,
225
+ "vm_bandwidth" => "600",
226
+ "vm_cpu_req" => "2",
227
+ "vm_created_date" => "1440018294",
228
+ "vm_description" => "apitestserver",
229
+ "vm_disk_req" => "100",
230
+ "vm_id" => "153979",
231
+ "vm_image" => "ubuntu-14.04_64bit",
232
+ "vm_image_display_name" => "ubuntu-14.04_64bit",
233
+ "vm_ip_address" => "45.58.35.251",
234
+ "vm_ip_gateway" => "45.58.34.1",
235
+ "vm_ip_subnet" => "255.255.254.0",
236
+ "vm_network_req" => "1",
237
+ "vm_os_architecture" => "64",
238
+ "vm_plan_name" => "L",
239
+ "vm_ram_req" => "4096",
240
+ "vm_removed_date" => nil,
241
+ "vm_status" => "RUNNING",
242
+ "vm_username" => "root",
243
+ "vm_vnc_password" => "8$EOs$Rs",
244
+ "vnc_port" => "18248"
245
+ }
246
+ }
247
+
248
+ it 'calls api_call with describe-instance and returns an instance hash' do
249
+ expect(subject).to receive(:api_call)
250
+ .with("describe-instance", {instanceid: instance_id})
251
+ .and_return(sample_api_response)
252
+ result = subject.describe_instance(instance_id)
253
+ expect(result).to eq expected_instance
254
+ end
255
+ end
256
+
257
+ describe "#terminate_instance" do
258
+ let(:instance_id) { "234" }
259
+ let(:sample_api_response) {
260
+ {
261
+ "Timestamp" => 1440175812,
262
+ "terminate-instanceresponse" => {
263
+ "instancesSet" => {
264
+ "item" => {
265
+ "InstanceId" => "154809",
266
+ "message" => "queued for termination",
267
+ "result" => "true"
268
+ }
269
+ },
270
+ "requestid" => "4dec8ab5-29e7-48f3-934e-e4bc3101de80"
271
+ }
272
+ }
273
+ }
274
+ let(:termination_response) {
275
+ {
276
+ "InstanceId" => "154809",
277
+ "message" => "queued for termination",
278
+ "result" => "true"
279
+ }
280
+ }
281
+
282
+ it 'calls api_call with terminate-instance and returns a termination response hash' do
283
+ expect(subject).to receive(:api_call)
284
+ .with("terminate-instance", {instanceid: instance_id})
285
+ .and_return(sample_api_response)
286
+ result = subject.terminate_instance(instance_id)
287
+ expect(result).to eq termination_response
288
+ end
289
+ end
290
+
291
+ describe "#run_instance" do
292
+ let(:sample_api_response) {
293
+ {
294
+ "Timestamp" => 1440018190,
295
+ "run-instanceresponse" => {
296
+ "instancesSet" => {
297
+ "item" => {
298
+ "instanceid" => "153979",
299
+ "ip_address" => "45.58.35.251",
300
+ "password" => "8q%Q6KaQ",
301
+ "username" => "root"
302
+ }
303
+ },
304
+ "requestid" => "6396399d-cb7d-446a-91c6-1334d0f939d8"
305
+ }
306
+ }
307
+ }
308
+ let(:run_response) {
309
+ {
310
+ "instanceid" => "153979",
311
+ "ip_address" => "45.58.35.251",
312
+ "password" => "8q%Q6KaQ",
313
+ "username" => "root"
314
+ }
315
+ }
316
+
317
+
318
+ let(:server_name) {"apitestserver"}
319
+ let(:image_id) {"ubuntu-14.04_64bit"}
320
+ let(:plan_name) {"L"}
321
+ let(:vm_location) {"USEAST2"}
322
+
323
+ context "default parameters" do
324
+ it 'calls the api with the servername, imageid, planname and vm_location' do
325
+ expect(subject).to receive(:api_call)
326
+ .with("run-instance", {servername: server_name, imageid: image_id, planname: plan_name, vm_location: vm_location})
327
+ .and_return(sample_api_response)
328
+ result = subject.run_instance(server_name, plan_name, vm_location, {image_id: image_id})
329
+ expect(result).to eq run_response
330
+ end
331
+ end
332
+
333
+ context "use clone_image instead of image_id" do
334
+ let(:clone_image) {"17-145607"}
335
+
336
+ it 'calls the api with the servername, cloneimage, planname and vm_location' do
337
+ expect(subject).to receive(:api_call)
338
+ .with("run-instance", {servername: server_name, cloneimage: clone_image, planname: plan_name, vm_location: vm_location})
339
+ .and_return(sample_api_response)
340
+ result = subject.run_instance(server_name, plan_name, vm_location, {clone_image: clone_image})
341
+ expect(result).to eq run_response
342
+ end
343
+ end
344
+
345
+ context "no image id or clone image are provided" do
346
+ it 'raises an error when all the required arguments aren\'t specified' do
347
+ expect{subject.run_instance(server_name, plan_name, vm_location)}
348
+ .to raise_error(ArgumentError)
349
+ end
350
+ end
351
+
352
+ context "additional options" do
353
+ let(:enable_backup) { true }
354
+ let(:server_quantity) { 2 }
355
+ let(:key_id) { "yt9p4y64f7dem3e" }
356
+
357
+ it 'calls the api with the default arguments, plus enablebackup, serverqty and key_id' do
358
+ expect(subject).to receive(:api_call)
359
+ .with("run-instance", {
360
+ servername: server_name, imageid: image_id, planname: plan_name, vm_location: vm_location,
361
+ enablebackup: enable_backup, serverqty: server_quantity, key_id: key_id})
362
+ .and_return(sample_api_response)
363
+ result = subject.run_instance(server_name, plan_name, vm_location, {
364
+ image_id: image_id, enable_backup: enable_backup, server_quantity: server_quantity, key_id: key_id
365
+ })
366
+ expect(result).to eq run_response
367
+ end
368
+ end
369
+ end
370
+
371
+ describe "#describe_images" do
372
+ let(:sample_api_response) {
373
+ {
374
+ "Timestamp" => 1439933643,
375
+ "describe-imageresponse" => {
376
+ "imagesset" => {
377
+ "1item" => {
378
+ "architecture" => "x86_64",
379
+ "displayname" => "Ubuntu 14.04 LTS Server 64-Bit",
380
+ "image_type" => "os",
381
+ "imageid" => "ubuntu-14.04_64bit",
382
+ "ostype" => "linux",
383
+ "owner" => "atlantic",
384
+ "platform" => "linux",
385
+ "version" => "14.04 LTS"
386
+ }
387
+ },
388
+ "requestid" => "eb221f31-d023-452d-a7e9-9614fc575a9d"
389
+ }
390
+ }
391
+ }
392
+ let(:image_descriptions) {
393
+ [
394
+ {
395
+ "architecture" => "x86_64",
396
+ "displayname" => "Ubuntu 14.04 LTS Server 64-Bit",
397
+ "image_type" => "os",
398
+ "imageid" => "ubuntu-14.04_64bit",
399
+ "ostype" => "linux",
400
+ "owner" => "atlantic",
401
+ "platform" => "linux",
402
+ "version" => "14.04 LTS"
403
+ }
404
+ ]
405
+ }
406
+
407
+ context 'default options' do
408
+ it 'calls api_call with describe-image and returns a list of image hashes' do
409
+ expect(subject).to receive(:api_call)
410
+ .with("describe-image")
411
+ .and_return(sample_api_response)
412
+ result = subject.describe_images
413
+ expect(result).to eq image_descriptions
414
+ end
415
+ end
416
+
417
+ context 'optional imageid' do
418
+ let(:image_id) { "ubuntu-14.04_64bit" }
419
+ it 'calls api_call with describe-image and a image_id string and returns a list of image hashes' do
420
+ expect(subject).to receive(:api_call)
421
+ .with("describe-image", {imageid: image_id})
422
+ .and_return(sample_api_response)
423
+ result = subject.describe_images({image_id: image_id})
424
+ expect(result).to eq image_descriptions
425
+ end
426
+ end
427
+ end
428
+
429
+ describe "#describe_plans" do
430
+ let(:sample_api_response) {
431
+ {
432
+ "Timestamp" => 1439932362,
433
+ "describe-planresponse" => {
434
+ "plans" => {
435
+ "1item" => {
436
+ "bandwidth" => 600,
437
+ "centos_capable" => "Y",
438
+ "cpanel_capable" => "Y",
439
+ "disk" => "100",
440
+ "display_bandwidth" => "600Mbits",
441
+ "display_disk" => "100GB",
442
+ "display_ram" => "4GB",
443
+ "free_transfer" => "5",
444
+ "num_cpu" => "2",
445
+ "ostype" => "linux",
446
+ "plan_name" => "L",
447
+ "platform" => "linux",
448
+ "ram" => "4096",
449
+ "rate_per_hr" => 0.0547
450
+ }
451
+ },
452
+ "requestid" => "4aae48ae-af7b-4bbd-9309-58aadbfd02d3"
453
+ }
454
+ }
455
+ }
456
+ let(:plan_descriptions) {
457
+ [
458
+ {
459
+ "bandwidth" => 600,
460
+ "centos_capable" => "Y",
461
+ "cpanel_capable" => "Y",
462
+ "disk" => "100",
463
+ "display_bandwidth" => "600Mbits",
464
+ "display_disk" => "100GB",
465
+ "display_ram" => "4GB",
466
+ "free_transfer" => "5",
467
+ "num_cpu" => "2",
468
+ "ostype" => "linux",
469
+ "plan_name" => "L",
470
+ "platform" => "linux",
471
+ "ram" => "4096",
472
+ "rate_per_hr" => 0.0547
473
+ }
474
+ ]
475
+ }
476
+
477
+ context 'default options' do
478
+ it 'calls api_call with describe-plan and returns a list of plan hashes' do
479
+ expect(subject).to receive(:api_call)
480
+ .with("describe-plan")
481
+ .and_return(sample_api_response)
482
+ result = subject.describe_plans
483
+ expect(result).to eq plan_descriptions
484
+ end
485
+ end
486
+
487
+ context 'optional arguments' do
488
+ let(:plan_name) { "L" }
489
+ let(:platform) { "linux" }
490
+ it 'calls api_call with describe-plan plan_name and platform and returns a list of image hashes' do
491
+ expect(subject).to receive(:api_call)
492
+ .with("describe-plan", {plan_name: plan_name, platform: platform})
493
+ .and_return(sample_api_response)
494
+ result = subject.describe_plans({plan_name: plan_name, platform: platform})
495
+ expect(result).to eq plan_descriptions
496
+ end
497
+ end
498
+ end
499
+
500
+ describe "#list_ssh_keys" do
501
+ let(:sample_api_response) {
502
+ {
503
+ "Timestamp" => 1457105502,
504
+ "list-sshkeysresponse" => {
505
+ "KeysSet" => {
506
+ "item" => {
507
+ "key_id" => "yt9p4y64f7dem3e",
508
+ "key_name" => "My Public SSH Key",
509
+ "public_key" => "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAsdfAn4sozeeBHWCv6B/tkTcUFz47fg48FgasdfasdfJNp4T5Fyq+3/6BA6fdZoI9skdudkfy3n6IUxHr5fwIUD0tn7QNsn5jp9rpjVRuXqoHUP3OYh6ZSlPBnsVmRNOI2ZWiBqMCIsWaeUkenVfnmvLZ/eMVwoKiDhakHs1dvaB8X4kEc7DnXKDZEyy0hAb+Eei8ppUqs9uMq+utXLEMCk0cPMTtqMialvk1pnz2lMuVPw1HGNRh2mjyGI7+6DoPCHaYurDQMXcyfF+05pSpBZCQAVJWvZFzivGfzUOAc4bgFBLznECQ== user@workstation-206"
510
+ }
511
+ },
512
+ "requestid" => "acea7888-a756-4ea1-b9db-6d2267673247"
513
+ }
514
+ }
515
+ }
516
+ let(:ssh_keys) {
517
+ [
518
+ {
519
+ "key_id" => "yt9p4y64f7dem3e",
520
+ "key_name" => "My Public SSH Key",
521
+ "public_key" => "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAsdfAn4sozeeBHWCv6B/tkTcUFz47fg48FgasdfasdfJNp4T5Fyq+3/6BA6fdZoI9skdudkfy3n6IUxHr5fwIUD0tn7QNsn5jp9rpjVRuXqoHUP3OYh6ZSlPBnsVmRNOI2ZWiBqMCIsWaeUkenVfnmvLZ/eMVwoKiDhakHs1dvaB8X4kEc7DnXKDZEyy0hAb+Eei8ppUqs9uMq+utXLEMCk0cPMTtqMialvk1pnz2lMuVPw1HGNRh2mjyGI7+6DoPCHaYurDQMXcyfF+05pSpBZCQAVJWvZFzivGfzUOAc4bgFBLznECQ== user@workstation-206"
522
+ }
523
+ ]
524
+ }
525
+
526
+ it 'calls api_call with list-sshkeys and returns a list of ssh key hashes' do
527
+ expect(subject).to receive(:api_call)
528
+ .with("list-sshkeys")
529
+ .and_return(sample_api_response)
530
+ result = subject.list_ssh_keys
531
+ expect(result).to eq ssh_keys
532
+ end
533
+ end
534
+
535
+ describe '#api_call' do
536
+ it 'passes on the action' do
537
+ expect(transport).to receive(:send_request).with(a_hash_including(Action: 'some-action'))
538
+ subject.send(:api_call, 'some-action')
539
+ end
540
+
541
+ it 'includes a valid signature, public key, and timestamp' do
542
+ allow(subject).to receive(:generate_request_id_tuple).and_return([ sample_timestamp, sample_request_id] )
543
+
544
+ expect(transport).to receive(:send_request).with(a_hash_including(Signature: sample_signature, ACSAccessKeyId: access_key, Timestamp: sample_timestamp))
545
+ subject.send(:api_call, 'some-action')
546
+ end
547
+
548
+ it 'passes on any arguments' do
549
+ expect(transport).to receive(:send_request).with(a_hash_including(Value1: 'some-value-1', Value2: 'some-value-2'))
550
+ subject.send(:api_call, 'someother-action', {:Value1 => "some-value-1", :Value2 => "some-value-2"})
551
+ end
552
+
553
+ it 'raises an exception if there is an error' do
554
+ expect(transport).to receive(:send_request).and_return( JSON.parse('{"error":{"message": "Some error occurred"}}') )
555
+ expect{subject.send(:api_call, 'some-erroneous-action')}.to raise_error(AtlanticNetException)
556
+ end
557
+ end
558
+
559
+ describe '#generate_request_id' do
560
+ it 'Uses Time to return epoc timestamp' do
561
+ expect(Time).to receive(:now).and_return(double(:ts, to_i: 100))
562
+ expect(subject.send(:generate_request_id_tuple).first).to eq(100)
563
+ end
564
+
565
+ it 'Returns a valid uuid for the request_id' do
566
+ request_id = subject.send(:generate_request_id_tuple).last
567
+ expect{ UUIDTools::UUID.parse(request_id)}.not_to raise_error
568
+ end
569
+
570
+ it 'Returns different request_ids for subsequent calls' do
571
+ request_id1 = subject.send(:generate_request_id_tuple).last
572
+ request_id2 = subject.send(:generate_request_id_tuple).last
573
+ expect(request_id1).not_to eq (request_id2)
574
+ end
575
+ end
576
+ end
577
+
578
+ describe AtlanticNet::HttpTransport do
579
+ let(:transport) { AtlanticNet::HttpTransport.new }
580
+ let(:sample_data) { {"arg1" => "value1", "arg2" => "value2"} }
581
+ let(:sample_response) { {"result1" => "response_value_1", "result2" => "response_value_2" }}
582
+
583
+ describe "#send_request" do
584
+ it "Passes the arguments and parses the response" do
585
+ stub_request(:get, "https://cloudapi.atlantic.net").with(query: sample_data).to_return(body: sample_response.to_json)
586
+ result = transport.send_request(sample_data)
587
+ expect(result).to eq sample_response
588
+ end
589
+
590
+ it "raises an exception when request is unsuccessful" do
591
+ stub_request(:get, "https://cloudapi.atlantic.net").to_return(status: 404)
592
+ expect{transport.send_request({})}.to raise_error(AtlanticNetException)
593
+ end
594
+ end
595
+ end
596
+
597
+ describe AtlanticNetException do
598
+ let(:atlantic_net_instance) { double }
599
+ let(:api_response) { double }
600
+ let(:message) { "an error message" }
601
+
602
+ it 'Has the message, instance and api_response accessible' do
603
+ atlantic_net_exception = AtlanticNetException.new(atlantic_net_instance, api_response, message)
604
+ expect(atlantic_net_exception.atlantic_net_instance).to eq atlantic_net_instance
605
+ expect(atlantic_net_exception.api_response).to eq api_response
606
+ expect(atlantic_net_exception.message).to eq message
607
+ end
608
+ end
@@ -0,0 +1,11 @@
1
+ require "simplecov"
2
+ require "coveralls"
3
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
4
+ SimpleCov::Formatter::HTMLFormatter,
5
+ Coveralls::SimpleCov::Formatter
6
+ ]
7
+ SimpleCov.start { add_filter "/spec/" }
8
+
9
+ require "atlantic_net"
10
+ require "uuidtools"
11
+ require "webmock/rspec"
metadata ADDED
@@ -0,0 +1,210 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: atlantic_net
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Jamie Starke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack-test
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.0.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-given
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: coveralls
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: uuidtools
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: webmock
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: pry-nav
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: A Ruby wrapper of the Atlantic.net API
168
+ email:
169
+ - git@jamiestarke.com
170
+ executables: []
171
+ extensions: []
172
+ extra_rdoc_files: []
173
+ files:
174
+ - ".gitignore"
175
+ - ".travis.yml"
176
+ - Gemfile
177
+ - LICENSE
178
+ - README.md
179
+ - Rakefile
180
+ - atlantic_net.gemspec
181
+ - lib/atlantic_net.rb
182
+ - spec/atlantic_net_spec.rb
183
+ - spec/spec_helper.rb
184
+ homepage: http://github.com/jrstarke/atlantic_net
185
+ licenses:
186
+ - MIT
187
+ metadata: {}
188
+ post_install_message:
189
+ rdoc_options: []
190
+ require_paths:
191
+ - lib
192
+ required_ruby_version: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: '0'
197
+ required_rubygems_version: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ requirements: []
203
+ rubyforge_project:
204
+ rubygems_version: 2.4.6
205
+ signing_key:
206
+ specification_version: 4
207
+ summary: A Ruby wrapper of the Atlantic.net API
208
+ test_files:
209
+ - spec/atlantic_net_spec.rb
210
+ - spec/spec_helper.rb