atlantic_net 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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