digitalronin-rackspace 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +152 -0
- data/LICENSE +21 -0
- data/README.md +43 -0
- data/lib/rackspace.rb +34 -0
- data/lib/rackspace/api.rb +40 -0
- data/lib/rackspace/base.rb +44 -0
- data/lib/rackspace/logger.rb +7 -0
- data/lib/rackspace/server_api.rb +41 -0
- data/lib/rackspace/storage_api.rb +44 -0
- data/lib/rackspace/vm.rb +102 -0
- data/lib/rackspace/volume.rb +55 -0
- data/spec/rackspace/vm_spec.rb +193 -0
- data/spec/rackspace/volume_spec.rb +116 -0
- data/spec/spec_helper.rb +8 -0
- metadata +104 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ba0533cf37abea8c1ea65902886a5268dc5a82d2
|
4
|
+
data.tar.gz: aca6b07411ab9a37a6d96241ead65b4ee55b11d7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 05025ecc9ae813517a0d615b9afa49c40283c0d51534e3909605c90761f43a58da0f01346146c0ab993398357711fdee89626f3d61c8156b4bf4794c4db6e9e9
|
7
|
+
data.tar.gz: 0a88db9edd98d95b489cfe523b1e8f058fe08882dc06b5c196ad7f7d29039220807b26773944d1ad0415e360d1a4ea28e653dfabc2b3df2ae7379e26627eae99
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
CFPropertyList (2.3.1)
|
5
|
+
builder (3.2.2)
|
6
|
+
byebug (4.0.5)
|
7
|
+
columnize (= 0.9.0)
|
8
|
+
coderay (1.1.0)
|
9
|
+
columnize (0.9.0)
|
10
|
+
diff-lcs (1.2.5)
|
11
|
+
excon (0.45.3)
|
12
|
+
fission (0.5.0)
|
13
|
+
CFPropertyList (~> 2.2)
|
14
|
+
fog (1.30.0)
|
15
|
+
fog-atmos
|
16
|
+
fog-aws (~> 0.0)
|
17
|
+
fog-brightbox (~> 0.4)
|
18
|
+
fog-core (~> 1.27, >= 1.27.4)
|
19
|
+
fog-ecloud
|
20
|
+
fog-google (>= 0.0.2)
|
21
|
+
fog-json
|
22
|
+
fog-local
|
23
|
+
fog-powerdns (>= 0.1.1)
|
24
|
+
fog-profitbricks
|
25
|
+
fog-radosgw (>= 0.0.2)
|
26
|
+
fog-riakcs
|
27
|
+
fog-sakuracloud (>= 0.0.4)
|
28
|
+
fog-serverlove
|
29
|
+
fog-softlayer
|
30
|
+
fog-storm_on_demand
|
31
|
+
fog-terremark
|
32
|
+
fog-vmfusion
|
33
|
+
fog-voxel
|
34
|
+
fog-xml (~> 0.1.1)
|
35
|
+
ipaddress (~> 0.5)
|
36
|
+
nokogiri (~> 1.5, >= 1.5.11)
|
37
|
+
fog-atmos (0.1.0)
|
38
|
+
fog-core
|
39
|
+
fog-xml
|
40
|
+
fog-aws (0.1.2)
|
41
|
+
fog-core (~> 1.27)
|
42
|
+
fog-json (~> 1.0)
|
43
|
+
fog-xml (~> 0.1)
|
44
|
+
ipaddress (~> 0.8)
|
45
|
+
fog-brightbox (0.7.1)
|
46
|
+
fog-core (~> 1.22)
|
47
|
+
fog-json
|
48
|
+
inflecto (~> 0.0.2)
|
49
|
+
fog-core (1.30.0)
|
50
|
+
builder
|
51
|
+
excon (~> 0.45)
|
52
|
+
formatador (~> 0.2)
|
53
|
+
mime-types
|
54
|
+
net-scp (~> 1.1)
|
55
|
+
net-ssh (>= 2.1.3)
|
56
|
+
fog-ecloud (0.1.1)
|
57
|
+
fog-core
|
58
|
+
fog-xml
|
59
|
+
fog-google (0.0.5)
|
60
|
+
fog-core
|
61
|
+
fog-json
|
62
|
+
fog-xml
|
63
|
+
fog-json (1.0.1)
|
64
|
+
fog-core (~> 1.0)
|
65
|
+
multi_json (~> 1.0)
|
66
|
+
fog-local (0.2.1)
|
67
|
+
fog-core (~> 1.27)
|
68
|
+
fog-powerdns (0.1.1)
|
69
|
+
fog-core (~> 1.27)
|
70
|
+
fog-json (~> 1.0)
|
71
|
+
fog-xml (~> 0.1)
|
72
|
+
fog-profitbricks (0.0.2)
|
73
|
+
fog-core
|
74
|
+
fog-xml
|
75
|
+
nokogiri
|
76
|
+
fog-radosgw (0.0.4)
|
77
|
+
fog-core (>= 1.21.0)
|
78
|
+
fog-json
|
79
|
+
fog-xml (>= 0.0.1)
|
80
|
+
fog-riakcs (0.1.0)
|
81
|
+
fog-core
|
82
|
+
fog-json
|
83
|
+
fog-xml
|
84
|
+
fog-sakuracloud (1.0.1)
|
85
|
+
fog-core
|
86
|
+
fog-json
|
87
|
+
fog-serverlove (0.1.2)
|
88
|
+
fog-core
|
89
|
+
fog-json
|
90
|
+
fog-softlayer (0.4.5)
|
91
|
+
fog-core
|
92
|
+
fog-json
|
93
|
+
fog-storm_on_demand (0.1.1)
|
94
|
+
fog-core
|
95
|
+
fog-json
|
96
|
+
fog-terremark (0.1.0)
|
97
|
+
fog-core
|
98
|
+
fog-xml
|
99
|
+
fog-vmfusion (0.1.0)
|
100
|
+
fission
|
101
|
+
fog-core
|
102
|
+
fog-voxel (0.1.0)
|
103
|
+
fog-core
|
104
|
+
fog-xml
|
105
|
+
fog-xml (0.1.2)
|
106
|
+
fog-core
|
107
|
+
nokogiri (~> 1.5, >= 1.5.11)
|
108
|
+
formatador (0.2.5)
|
109
|
+
gem-this (0.3.7)
|
110
|
+
inflecto (0.0.2)
|
111
|
+
ipaddress (0.8.0)
|
112
|
+
method_source (0.8.2)
|
113
|
+
mime-types (2.5)
|
114
|
+
mini_portile (0.6.2)
|
115
|
+
multi_json (1.11.0)
|
116
|
+
net-scp (1.2.1)
|
117
|
+
net-ssh (>= 2.6.5)
|
118
|
+
net-ssh (2.9.2)
|
119
|
+
nokogiri (1.6.6.2)
|
120
|
+
mini_portile (~> 0.6.0)
|
121
|
+
pry (0.10.1)
|
122
|
+
coderay (~> 1.1.0)
|
123
|
+
method_source (~> 0.8.1)
|
124
|
+
slop (~> 3.4)
|
125
|
+
pry-byebug (3.1.0)
|
126
|
+
byebug (~> 4.0)
|
127
|
+
pry (~> 0.10)
|
128
|
+
rake (10.4.2)
|
129
|
+
rspec (3.2.0)
|
130
|
+
rspec-core (~> 3.2.0)
|
131
|
+
rspec-expectations (~> 3.2.0)
|
132
|
+
rspec-mocks (~> 3.2.0)
|
133
|
+
rspec-core (3.2.3)
|
134
|
+
rspec-support (~> 3.2.0)
|
135
|
+
rspec-expectations (3.2.1)
|
136
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
137
|
+
rspec-support (~> 3.2.0)
|
138
|
+
rspec-mocks (3.2.1)
|
139
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
140
|
+
rspec-support (~> 3.2.0)
|
141
|
+
rspec-support (3.2.2)
|
142
|
+
slop (3.6.0)
|
143
|
+
|
144
|
+
PLATFORMS
|
145
|
+
ruby
|
146
|
+
|
147
|
+
DEPENDENCIES
|
148
|
+
fog
|
149
|
+
gem-this
|
150
|
+
pry-byebug
|
151
|
+
rake
|
152
|
+
rspec
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 David Salgado
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
A tool to manage Rackspace Cloud servers and Block Storage volumes
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/digitalronin-rackspace.svg)](http://badge.fury.io/rb/digitalronin-rackspace)
|
4
|
+
[![Build Status](https://travis-ci.org/digitalronin/digitalronin-rackspace.svg?branch=master)](https://travis-ci.org/digitalronin/digitalronin-rackspace)
|
5
|
+
[![Dependency Status](https://gemnasium.com/digitalronin/digitalronin-rackspace.svg)](https://gemnasium.com/digitalronin/digitalronin-rackspace)
|
6
|
+
|
7
|
+
[fog](http://fog.io) is awesome, but the API can be a bit cryptic and long-winded, especially for things like creating a VM with an attached block storage volume
|
8
|
+
|
9
|
+
USAGE
|
10
|
+
|
11
|
+
Create an 8G General Purpose VM with an attached 75G SATA block storage volume;
|
12
|
+
|
13
|
+
vm = Rackspace::Vm.new(
|
14
|
+
size: 8,
|
15
|
+
account: 'myaccount',
|
16
|
+
name: 'testserver',
|
17
|
+
volume: { size: 75 }
|
18
|
+
).create
|
19
|
+
|
20
|
+
Destroy a VM and its attached storage volume
|
21
|
+
|
22
|
+
Rackspace::Vm.new(account: 'myaccount', name: 'testserver').destroy(destroy_volume: true)
|
23
|
+
|
24
|
+
See the scripts in the examples directory
|
25
|
+
|
26
|
+
ASSUMPTIONS/PRE-REQUISITES
|
27
|
+
|
28
|
+
* Ruby 2.2.0 (1.9+ should work, but I haven't tested any versions except 2.2)
|
29
|
+
* Valid Rackspace Cloud account and API key (see rackspace_credentials.yml.example for how to supply these via a YAML file)
|
30
|
+
* By default, VMs are "General Purpose" Ubuntu 14.04 machines - this can be overridden by supplying "image_name" and "flavor_id"
|
31
|
+
|
32
|
+
CAVEATS
|
33
|
+
|
34
|
+
* This gem will create and destroy cloud servers and block storage volumes in your account. This could cost you money and might destroy data you care about. Use it with caution and at your own risk.
|
35
|
+
|
36
|
+
* The gem doesn't support attaching multiple volumes to a server.
|
37
|
+
|
38
|
+
* Deleting server with an attached volume can fail if the call to destroy the volume happens before it has finished detaching.
|
39
|
+
|
40
|
+
TODO
|
41
|
+
|
42
|
+
* The gem should wait to ensure success of any API operations before returning from the relevant method call
|
43
|
+
|
data/lib/rackspace.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'fog'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Rackspace
|
5
|
+
# Delay between checks, when waiting for the Rackspace API
|
6
|
+
SLEEP_DELAY = 20
|
7
|
+
# Maximum iterations before we give up waiting for the Rackspace API
|
8
|
+
MAX_ITERATIONS_ON_CREATE = 20
|
9
|
+
|
10
|
+
# Cloud servers
|
11
|
+
DEFAULT_SIZE = 1
|
12
|
+
DEFAULT_IMAGE_NAME = 'Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM)'
|
13
|
+
DEFAULT_FLAVOUR = 'general1'
|
14
|
+
|
15
|
+
# Block storage volumes
|
16
|
+
SATA = 'SATA'
|
17
|
+
SSD = 'SSD'
|
18
|
+
MIN_SIZE = 75
|
19
|
+
MAX_SIZE = 1024
|
20
|
+
AVAILABLE = 'available'
|
21
|
+
|
22
|
+
DEFAULT_REGION = :ord
|
23
|
+
end
|
24
|
+
|
25
|
+
libdir = File.join(File.dirname(__FILE__), 'rackspace')
|
26
|
+
|
27
|
+
require File.join(libdir, 'logger.rb')
|
28
|
+
require File.join(libdir, 'base.rb')
|
29
|
+
require File.join(libdir, 'api.rb')
|
30
|
+
require File.join(libdir, 'server_api.rb')
|
31
|
+
require File.join(libdir, 'storage_api.rb')
|
32
|
+
require File.join(libdir, 'vm.rb')
|
33
|
+
require File.join(libdir, 'volume.rb')
|
34
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Rackspace
|
2
|
+
class Api
|
3
|
+
attr_reader :logger
|
4
|
+
|
5
|
+
def initialize(params)
|
6
|
+
@logger = params.fetch(:logger, Rackspace::Logger.new)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def wait_for(name)
|
12
|
+
logger.log "Waiting for #{object_type} #{name}"
|
13
|
+
|
14
|
+
obj = nil; ready = false; iterations = 1
|
15
|
+
|
16
|
+
while !ready do
|
17
|
+
obj = find_by_name(name)
|
18
|
+
|
19
|
+
if obj.nil?
|
20
|
+
logger.log "Waiting for #{object_type} #{name} to appear in API list..."
|
21
|
+
else
|
22
|
+
logger.log "Found #{object_type} #{name}. Status: #{obj.state}, check #{iterations} of #{MAX_ITERATIONS_ON_CREATE}"
|
23
|
+
|
24
|
+
if obj.state == ready_state
|
25
|
+
ready = true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
iterations += 1
|
30
|
+
|
31
|
+
raise "#{object_type} not ready after #{iterations} sleeps. Aborting." if iterations > MAX_ITERATIONS_ON_CREATE
|
32
|
+
|
33
|
+
sleep(SLEEP_DELAY) unless ready
|
34
|
+
end
|
35
|
+
|
36
|
+
obj
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Rackspace
|
2
|
+
class Base
|
3
|
+
attr_reader :account, :region, :credentials, :logger
|
4
|
+
|
5
|
+
DEFAULT_CREDENTIALS = 'rackspace_credentials.yml'
|
6
|
+
|
7
|
+
def initialize(params)
|
8
|
+
@logger = params.fetch(:logger, Rackspace::Logger.new)
|
9
|
+
@account = params.fetch(:account)
|
10
|
+
@region = params.fetch(:region, DEFAULT_REGION)
|
11
|
+
@credentials = params.fetch(:credentials) {
|
12
|
+
credentials_file = params.fetch(:credentials_file, DEFAULT_CREDENTIALS)
|
13
|
+
YAML.load(File.read(credentials_file))
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def log(msg)
|
18
|
+
logger.log(msg)
|
19
|
+
end
|
20
|
+
|
21
|
+
def server_api
|
22
|
+
@server_api ||= ServerApi.new(rackspace_credentials)
|
23
|
+
end
|
24
|
+
|
25
|
+
def storage_api
|
26
|
+
@storage_api ||= StorageApi.new(rackspace_credentials)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def accounts
|
32
|
+
credentials.fetch(:accounts) { raise("No :accounts section found in credentials") }
|
33
|
+
end
|
34
|
+
|
35
|
+
def rackspace_credentials
|
36
|
+
{
|
37
|
+
rackspace_username: account,
|
38
|
+
rackspace_api_key: accounts.fetch(account) { raise("No API key found for account: #{account}") },
|
39
|
+
rackspace_region: region,
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Rackspace
|
2
|
+
class ServerApi < Api
|
3
|
+
attr_reader :api
|
4
|
+
|
5
|
+
def initialize(rackspace_credentials)
|
6
|
+
super rackspace_credentials
|
7
|
+
@api ||= Fog::Compute.new(
|
8
|
+
rackspace_credentials.merge(
|
9
|
+
version: :v2,
|
10
|
+
provider: 'Rackspace',
|
11
|
+
)
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def find_by_name(name)
|
16
|
+
api.servers.detect {|i| i.name == name}
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(params)
|
20
|
+
api.servers.create(params)
|
21
|
+
wait_for params.fetch(:name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def image_by_name(img_name)
|
25
|
+
img = api.images.detect {|i| i.name == img_name}
|
26
|
+
raise "No such server image #{img_name}" if img.nil?
|
27
|
+
img
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def object_type
|
33
|
+
'server'
|
34
|
+
end
|
35
|
+
|
36
|
+
def ready_state
|
37
|
+
'ACTIVE'
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Rackspace
|
2
|
+
class StorageApi < Api
|
3
|
+
attr_reader :api
|
4
|
+
|
5
|
+
def initialize(rackspace_credentials)
|
6
|
+
super rackspace_credentials
|
7
|
+
@api ||= Fog::Rackspace::BlockStorage.new(rackspace_credentials)
|
8
|
+
end
|
9
|
+
|
10
|
+
def find_by_name(name)
|
11
|
+
api.volumes.detect { |v| v.display_name == name }
|
12
|
+
end
|
13
|
+
|
14
|
+
def find_by_id(volume_id)
|
15
|
+
api.get_volume volume_id
|
16
|
+
end
|
17
|
+
|
18
|
+
def create(params)
|
19
|
+
name = params.fetch(:name)
|
20
|
+
|
21
|
+
api.volumes.create(
|
22
|
+
size: params.fetch(:size),
|
23
|
+
display_name: name,
|
24
|
+
volume_type: params.fetch(:volume_type),
|
25
|
+
)
|
26
|
+
wait_for(name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def destroy(name)
|
30
|
+
find_by_name(name).destroy
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def object_type
|
36
|
+
'volume'
|
37
|
+
end
|
38
|
+
|
39
|
+
def ready_state
|
40
|
+
AVAILABLE
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
data/lib/rackspace/vm.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
module Rackspace
|
2
|
+
class Vm < Base
|
3
|
+
attr_reader :name, :size, :flavor_id, :image_name, :volume
|
4
|
+
|
5
|
+
def initialize(params)
|
6
|
+
super params
|
7
|
+
@name = params.fetch(:name)
|
8
|
+
@image_name = params.fetch(:image_name, DEFAULT_IMAGE_NAME)
|
9
|
+
@size = params.fetch(:size, DEFAULT_SIZE)
|
10
|
+
@flavor_id = params.fetch(:flavor_id, "#{DEFAULT_FLAVOUR}-#{size}")
|
11
|
+
@volume_definition = params[:volume]
|
12
|
+
end
|
13
|
+
|
14
|
+
def create
|
15
|
+
log "Creating server #{name} in account #{account}"
|
16
|
+
@server = server_api.create(
|
17
|
+
name: name,
|
18
|
+
flavor_id: flavor_id,
|
19
|
+
image_id: image_id,
|
20
|
+
personality: personality
|
21
|
+
)
|
22
|
+
build_and_attach_volume if @volume_definition
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def destroy(options = {})
|
27
|
+
log "Destroying server #{name} in account #{account}"
|
28
|
+
server.attachments.map(&:destroy)
|
29
|
+
volume.destroy if options[:destroy_volume]
|
30
|
+
server.destroy
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def public_ip
|
35
|
+
server.ipv4_address
|
36
|
+
end
|
37
|
+
|
38
|
+
def private_ip
|
39
|
+
server.private_ip_address
|
40
|
+
end
|
41
|
+
|
42
|
+
def state
|
43
|
+
server.state
|
44
|
+
end
|
45
|
+
|
46
|
+
def attach_volume(vol)
|
47
|
+
server.attach_volume(vol.volume)
|
48
|
+
end
|
49
|
+
|
50
|
+
def volume
|
51
|
+
@volume ||= begin
|
52
|
+
if attachments.any?
|
53
|
+
Rackspace::Volume.new(
|
54
|
+
account: account,
|
55
|
+
region: region,
|
56
|
+
credentials: credentials,
|
57
|
+
volume_id: attachments.first.volume_id
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def attachments
|
66
|
+
server.attachments
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_and_attach_volume
|
70
|
+
vol = Rackspace::Volume.new(@volume_definition.merge(
|
71
|
+
account: account,
|
72
|
+
region: region,
|
73
|
+
credentials: credentials,
|
74
|
+
name: "#{name}-vol",
|
75
|
+
)).create
|
76
|
+
|
77
|
+
log "Attaching block storage volume #{vol.name}"
|
78
|
+
server.attach_volume(vol.volume)
|
79
|
+
@volume = nil # force an API query next time volume is called
|
80
|
+
end
|
81
|
+
|
82
|
+
def server
|
83
|
+
@server ||= server_api.find_by_name(name)
|
84
|
+
end
|
85
|
+
|
86
|
+
def personality
|
87
|
+
[{
|
88
|
+
'path' => '/root/.ssh/authorized_keys',
|
89
|
+
'contents' => Base64.encode64(credentials.fetch(:public_ssh_key)),
|
90
|
+
}]
|
91
|
+
end
|
92
|
+
|
93
|
+
def image_id
|
94
|
+
server_api.image_by_name(image_name).id
|
95
|
+
end
|
96
|
+
|
97
|
+
def find_by_name
|
98
|
+
server_api.find_by_name(name)
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rackspace
|
2
|
+
class Volume < Base
|
3
|
+
attr_reader :volume_type, :size, :name, :volume_id, :device
|
4
|
+
|
5
|
+
def initialize(params)
|
6
|
+
super params
|
7
|
+
if @volume_id = params[:volume_id]
|
8
|
+
fetch_volume_by_id
|
9
|
+
else
|
10
|
+
@volume_type = params.fetch(:volume_type, SATA)
|
11
|
+
@size = params.fetch(:size, MIN_SIZE)
|
12
|
+
@name = params.fetch(:name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def create
|
17
|
+
log "Creating #{size}GB #{volume_type} block storage volume: #{name}"
|
18
|
+
storage_api.create(
|
19
|
+
size: size,
|
20
|
+
name: name,
|
21
|
+
volume_type: volume_type,
|
22
|
+
)
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def destroy
|
27
|
+
log "Destroying volume #{name} in account #{account}"
|
28
|
+
volume.destroy
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def volume
|
33
|
+
@volume ||= storage_api.find_by_name(name)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Find an existing volume by its volume_id
|
39
|
+
def fetch_volume_by_id
|
40
|
+
vol = storage_api.find_by_id(volume_id)
|
41
|
+
data = vol.body.fetch("volume")
|
42
|
+
@name = data.fetch('display_name')
|
43
|
+
@volume_type = data.fetch('volume_type')
|
44
|
+
@size = data.fetch('size')
|
45
|
+
|
46
|
+
# Sometimes the Rackspace API takes time to update,
|
47
|
+
# and newly-attached volumes don't report any attachments
|
48
|
+
# yet.
|
49
|
+
attachments = data.fetch('attachments')
|
50
|
+
if attachments.any?
|
51
|
+
@device = attachments.first.fetch('device')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require './spec/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Rackspace::Vm do
|
4
|
+
let(:credentials) { {
|
5
|
+
accounts: { 'myaccount' => 'dummy api key' },
|
6
|
+
public_ssh_key: 'dummy ssh key',
|
7
|
+
} }
|
8
|
+
|
9
|
+
let(:params) { {
|
10
|
+
credentials: credentials,
|
11
|
+
account: 'myaccount',
|
12
|
+
name: 'myserver',
|
13
|
+
} }
|
14
|
+
|
15
|
+
subject(:vm) { described_class.new(params) }
|
16
|
+
|
17
|
+
let(:logger) { double(Rackspace::Logger, log: true) }
|
18
|
+
|
19
|
+
let(:rackspace_credentials) { {
|
20
|
+
rackspace_username: 'myaccount',
|
21
|
+
rackspace_api_key: 'dummy api key',
|
22
|
+
rackspace_region: :ord,
|
23
|
+
} }
|
24
|
+
|
25
|
+
let(:personality) { [
|
26
|
+
'path' => '/root/.ssh/authorized_keys',
|
27
|
+
'contents' => Base64.encode64('dummy ssh key')
|
28
|
+
] }
|
29
|
+
|
30
|
+
let(:image) { double(id: 'some image') }
|
31
|
+
|
32
|
+
let(:api) { double(Rackspace::ServerApi, image_by_name: image, create: true) }
|
33
|
+
|
34
|
+
before do
|
35
|
+
allow(Rackspace::Logger).to receive(:new).and_return(logger)
|
36
|
+
end
|
37
|
+
|
38
|
+
before do
|
39
|
+
allow(Rackspace::ServerApi).to receive(:new).and_return(api)
|
40
|
+
end
|
41
|
+
|
42
|
+
context "with defaults" do
|
43
|
+
it "creates ServerApi instance" do
|
44
|
+
expect(Rackspace::ServerApi).to receive(:new).with(rackspace_credentials).and_return(api)
|
45
|
+
vm.create
|
46
|
+
end
|
47
|
+
|
48
|
+
it "creates a vm" do
|
49
|
+
expect(api).to receive(:create).with({
|
50
|
+
name: 'myserver',
|
51
|
+
flavor_id: 'general1-1',
|
52
|
+
image_id: 'some image',
|
53
|
+
personality: personality,
|
54
|
+
})
|
55
|
+
|
56
|
+
vm.create
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "overriding vm size" do
|
61
|
+
let(:params) { super().merge(size: 2) }
|
62
|
+
|
63
|
+
it "creates a vm" do
|
64
|
+
expect(api).to receive(:create).with(hash_including(flavor_id: 'general1-2'))
|
65
|
+
vm.create
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "overriding image" do
|
70
|
+
let(:image2) { double(id: 'image2-id') }
|
71
|
+
let(:params) { super().merge(image_name: 'another image') }
|
72
|
+
|
73
|
+
it "creates a vm" do
|
74
|
+
allow(api).to receive(:image_by_name).with('another image').and_return(image2)
|
75
|
+
expect(api).to receive(:create).with(hash_including(image_id: 'image2-id'))
|
76
|
+
vm.create
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "overriding flavor_id" do
|
81
|
+
let(:params) { super().merge(flavor_id: 'some-flavor-id') }
|
82
|
+
|
83
|
+
it "creates a vm" do
|
84
|
+
expect(api).to receive(:create).with(hash_including(flavor_id: 'some-flavor-id'))
|
85
|
+
vm.create
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "overriding region" do
|
90
|
+
let(:params) { super().merge(region: :iad) }
|
91
|
+
|
92
|
+
it "creates ServerApi instance" do
|
93
|
+
expect(Rackspace::ServerApi).to receive(:new).with(hash_including(rackspace_region: :iad))
|
94
|
+
vm.create
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "destroying" do
|
99
|
+
let(:attachment) { double(destroy: true) }
|
100
|
+
let(:server) { double(attachments: [attachment], destroy: true) }
|
101
|
+
|
102
|
+
before do
|
103
|
+
allow(api).to receive(:attachments).and_return([])
|
104
|
+
allow(api).to receive(:find_by_name).with('myserver').and_return(server)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "finds the vm" do
|
108
|
+
expect(api).to receive(:find_by_name).with('myserver')
|
109
|
+
vm.destroy
|
110
|
+
end
|
111
|
+
|
112
|
+
it "destroys the vm" do
|
113
|
+
expect(server).to receive(:destroy)
|
114
|
+
vm.destroy
|
115
|
+
end
|
116
|
+
|
117
|
+
it "detaches volumes" do
|
118
|
+
expect(attachment).to receive(:destroy)
|
119
|
+
vm.destroy
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "attached block storage volumes" do
|
124
|
+
context "attaching volumes" do
|
125
|
+
let(:vol) { double }
|
126
|
+
let(:volume) { double(Rackspace::Volume, volume: vol) }
|
127
|
+
let(:server) { double }
|
128
|
+
|
129
|
+
before do
|
130
|
+
allow(api).to receive(:find_by_name).with('myserver').and_return(server)
|
131
|
+
end
|
132
|
+
|
133
|
+
it "attaches" do
|
134
|
+
expect(server).to receive(:attach_volume).with(vol)
|
135
|
+
vm.attach_volume(volume)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context "building and attaching a volume" do
|
140
|
+
let(:params) { super().merge(volume: {size: 100}) }
|
141
|
+
let(:vol) { double }
|
142
|
+
let(:volume) { double(Rackspace::Volume, volume: vol, name: 'myvol') }
|
143
|
+
let(:server) { double(attach_volume: true) }
|
144
|
+
|
145
|
+
let(:v_params) { {
|
146
|
+
account: 'myaccount',
|
147
|
+
region: :ord,
|
148
|
+
credentials: credentials,
|
149
|
+
name: 'myserver-vol',
|
150
|
+
size: 100,
|
151
|
+
} }
|
152
|
+
|
153
|
+
before do
|
154
|
+
allow(api).to receive(:find_by_name).with('myserver').and_return(server)
|
155
|
+
allow(api).to receive(:create).and_return(server)
|
156
|
+
allow(Rackspace::Volume).to receive(:new).with(v_params).and_return(volume)
|
157
|
+
allow(volume).to receive(:create).and_return(volume)
|
158
|
+
end
|
159
|
+
|
160
|
+
it "creates the volume" do
|
161
|
+
expect(Rackspace::Volume).to receive(:new).with(v_params).and_return(volume)
|
162
|
+
expect(volume).to receive(:create).and_return(volume)
|
163
|
+
vm.create
|
164
|
+
end
|
165
|
+
|
166
|
+
it "attaches the volume" do
|
167
|
+
expect(server).to receive(:attach_volume).with(vol)
|
168
|
+
vm.create
|
169
|
+
end
|
170
|
+
|
171
|
+
context "querying volume" do
|
172
|
+
let(:attachment) { double(volume_id: 'foo') }
|
173
|
+
|
174
|
+
let(:v_params) { {
|
175
|
+
account: 'myaccount',
|
176
|
+
region: :ord,
|
177
|
+
credentials: credentials,
|
178
|
+
volume_id: 'foo',
|
179
|
+
} }
|
180
|
+
|
181
|
+
before do
|
182
|
+
allow(server).to receive(:attachments).and_return([attachment])
|
183
|
+
end
|
184
|
+
|
185
|
+
it "returns the first attached volume" do
|
186
|
+
expect(Rackspace::Volume).to receive(:new).with(v_params)
|
187
|
+
vm.volume
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require './spec/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Rackspace::Volume do
|
4
|
+
let(:credentials) { {
|
5
|
+
accounts: { 'myaccount' => 'dummy api key' },
|
6
|
+
public_ssh_key: 'dummy ssh key',
|
7
|
+
} }
|
8
|
+
|
9
|
+
let(:params) { {
|
10
|
+
credentials: credentials,
|
11
|
+
account: 'myaccount',
|
12
|
+
name: 'myvolume',
|
13
|
+
} }
|
14
|
+
|
15
|
+
subject(:volume) { described_class.new(params) }
|
16
|
+
|
17
|
+
let(:logger) { double(Rackspace::Logger, log: true) }
|
18
|
+
|
19
|
+
let(:rackspace_credentials) { {
|
20
|
+
rackspace_username: 'myaccount',
|
21
|
+
rackspace_api_key: 'dummy api key',
|
22
|
+
rackspace_region: :ord,
|
23
|
+
} }
|
24
|
+
|
25
|
+
let(:api) { double(Rackspace::StorageApi, create: true) }
|
26
|
+
|
27
|
+
before do
|
28
|
+
allow(Rackspace::Logger).to receive(:new).and_return(logger)
|
29
|
+
end
|
30
|
+
|
31
|
+
before do
|
32
|
+
allow(Rackspace::StorageApi).to receive(:new).and_return(api)
|
33
|
+
end
|
34
|
+
|
35
|
+
context "finding by id" do
|
36
|
+
let(:params) { super().merge(volume_id: "dummy-volume-id") }
|
37
|
+
let(:body) { {
|
38
|
+
"volume" => {
|
39
|
+
"display_name" => "myvol",
|
40
|
+
"attachments" => [{"device" => "/dev/xvdb"}],
|
41
|
+
"volume_type" => "SATA",
|
42
|
+
"size" => 75
|
43
|
+
}
|
44
|
+
} }
|
45
|
+
let(:obj) { double(body: body) }
|
46
|
+
|
47
|
+
it "finds" do
|
48
|
+
expect(Rackspace::StorageApi).to receive(:new).with(rackspace_credentials).and_return(api)
|
49
|
+
expect(api).to receive(:find_by_id).with("dummy-volume-id").and_return(obj)
|
50
|
+
volume.device
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "with defaults" do
|
55
|
+
it "creates StorageApi instance" do
|
56
|
+
expect(Rackspace::StorageApi).to receive(:new).with(rackspace_credentials).and_return(api)
|
57
|
+
volume.create
|
58
|
+
end
|
59
|
+
|
60
|
+
it "creates a volume" do
|
61
|
+
expect(api).to receive(:create).with({
|
62
|
+
name: 'myvolume',
|
63
|
+
volume_type: 'SATA',
|
64
|
+
size: 75,
|
65
|
+
})
|
66
|
+
|
67
|
+
volume.create
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "overriding volume size" do
|
72
|
+
let(:params) { super().merge(size: 200) }
|
73
|
+
|
74
|
+
it "creates a volume" do
|
75
|
+
expect(api).to receive(:create).with(hash_including(size: 200))
|
76
|
+
volume.create
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "overriding volume type" do
|
81
|
+
let(:params) { super().merge(volume_type: 'SSD') }
|
82
|
+
|
83
|
+
it "creates a volume" do
|
84
|
+
expect(api).to receive(:create).with(hash_including(volume_type: 'SSD'))
|
85
|
+
volume.create
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "overriding region" do
|
90
|
+
let(:params) { super().merge(region: :iad) }
|
91
|
+
|
92
|
+
it "creates StorageApi instance" do
|
93
|
+
expect(Rackspace::StorageApi).to receive(:new).with(hash_including(rackspace_region: :iad))
|
94
|
+
volume.create
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "destroying" do
|
99
|
+
let(:vol) { double(destroy: true) }
|
100
|
+
|
101
|
+
before do
|
102
|
+
allow(api).to receive(:find_by_name).with('myvolume').and_return(vol)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "finds the volume" do
|
106
|
+
expect(api).to receive(:find_by_name).with('myvolume')
|
107
|
+
volume.destroy
|
108
|
+
end
|
109
|
+
|
110
|
+
it "destroys the volume" do
|
111
|
+
expect(vol).to receive(:destroy)
|
112
|
+
volume.destroy
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: digitalronin-rackspace
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Salgado
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: fog
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.30'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.30'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.2'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry-byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.1'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.1'
|
55
|
+
description: Create/Query/Destroy rackspace cloud VMs and attached (or not) block
|
56
|
+
storage volumes
|
57
|
+
email: david@digitalronin.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files:
|
61
|
+
- README.md
|
62
|
+
files:
|
63
|
+
- Gemfile
|
64
|
+
- Gemfile.lock
|
65
|
+
- LICENSE
|
66
|
+
- README.md
|
67
|
+
- lib/rackspace.rb
|
68
|
+
- lib/rackspace/api.rb
|
69
|
+
- lib/rackspace/base.rb
|
70
|
+
- lib/rackspace/logger.rb
|
71
|
+
- lib/rackspace/server_api.rb
|
72
|
+
- lib/rackspace/storage_api.rb
|
73
|
+
- lib/rackspace/vm.rb
|
74
|
+
- lib/rackspace/volume.rb
|
75
|
+
- spec/rackspace/vm_spec.rb
|
76
|
+
- spec/rackspace/volume_spec.rb
|
77
|
+
- spec/spec_helper.rb
|
78
|
+
homepage: https://github.com/digitalronin/digitalronin-rackspace
|
79
|
+
licenses:
|
80
|
+
- MIT
|
81
|
+
metadata: {}
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options:
|
84
|
+
- "--main"
|
85
|
+
- README.md
|
86
|
+
require_paths:
|
87
|
+
- lib
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
requirements: []
|
99
|
+
rubyforge_project:
|
100
|
+
rubygems_version: 2.4.6
|
101
|
+
signing_key:
|
102
|
+
specification_version: 4
|
103
|
+
summary: Rackspace Cloud Servers and Block Storage Volumes
|
104
|
+
test_files: []
|