cloudservers 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/COPYING +10 -0
- data/README.rdoc +87 -0
- data/Rakefile +17 -0
- data/TODO +13 -0
- data/VERSION +1 -0
- data/cloudservers.gemspec +62 -0
- data/lib/cloudservers/authentication.rb +38 -0
- data/lib/cloudservers/connection.rb +306 -0
- data/lib/cloudservers/entity_manager.rb +4 -0
- data/lib/cloudservers/exception.rb +66 -0
- data/lib/cloudservers/flavor.rb +31 -0
- data/lib/cloudservers/image.rb +60 -0
- data/lib/cloudservers/server.rb +254 -0
- data/lib/cloudservers/shared_ip_group.rb +52 -0
- data/lib/cloudservers.rb +83 -0
- data/test/cloudservers_authentication_test.rb +37 -0
- data/test/test_helper.rb +5 -0
- metadata +93 -0
data/.gitignore
ADDED
data/COPYING
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
Unless otherwise noted, all files are released under the MIT license, exceptions contain licensing information in them.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
8
|
+
|
9
|
+
Except as contained in this notice, the name of Rackspace US, Inc. shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Rackspace US, Inc.
|
10
|
+
|
data/README.rdoc
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
= Rackspace Cloud Servers
|
2
|
+
|
3
|
+
== Description
|
4
|
+
|
5
|
+
This is a Ruby interface into the Rackspace[http://rackspacecloud.com/] {Cloud Servers}[http://www.rackspacecloud.com/cloud_hosting_products/servers] service. Cloud Servers from The Rackspace Cloud put you in complete control of your hosting infrastructure. Each Cloud Server is a fully-customizable, pay by the hour, virtualized Windows or Linux server instance that you launch, maintain, and control with full root access.
|
6
|
+
|
7
|
+
*Note that in version 0.2.0 the connection style changed, from positional arguments to a hash of options*
|
8
|
+
|
9
|
+
== Installation
|
10
|
+
|
11
|
+
This source is available on Github[http://github.com/rackspace/ruby-cloudservers/] and the gem is available on Gemcutter[http://gemcutter.org/]. To install it, do
|
12
|
+
|
13
|
+
gem sources -a http://gemcutter.org/ (Newer Ruby Gems have this already)
|
14
|
+
|
15
|
+
sudo gem install cloudservers
|
16
|
+
|
17
|
+
To use it in a Rails application, add the following information to your config/environment.rb
|
18
|
+
|
19
|
+
config.gem "cloudservers"
|
20
|
+
|
21
|
+
|
22
|
+
== Examples
|
23
|
+
|
24
|
+
See the class definitions for documentation on specific methods and operations.
|
25
|
+
|
26
|
+
require 'rubygems'
|
27
|
+
require 'cloudservers'
|
28
|
+
|
29
|
+
# Log into the Cloud Servers system
|
30
|
+
cs = CloudServers::Connection.new(:username => USERNAME, :api_key => API_KEY)
|
31
|
+
|
32
|
+
# Get a listing of all current servers
|
33
|
+
>> cs.servers
|
34
|
+
=> [{:name=>"RenamedRubyTest", :id=>110917}]
|
35
|
+
|
36
|
+
# Access a specific server
|
37
|
+
>> server = cs.server(110917)
|
38
|
+
>> server.name
|
39
|
+
=> "RenamedRubyTest"
|
40
|
+
|
41
|
+
# or...
|
42
|
+
server_manager.find(110917)
|
43
|
+
|
44
|
+
|
45
|
+
# See what type of server this is
|
46
|
+
>> server.flavor.name
|
47
|
+
=> "256 server"
|
48
|
+
>> server.image.name
|
49
|
+
=> "Ubuntu 8.04.2 LTS (hardy)"
|
50
|
+
|
51
|
+
# Soft-reboot the server
|
52
|
+
>> server.reboot
|
53
|
+
=> true
|
54
|
+
|
55
|
+
# Create a new 512MB CentOS 5.2 server. The root password is returned in the adminPass method.
|
56
|
+
>> image = cs.get_image(8)
|
57
|
+
=> #<CloudServers::Image:0x1014a8060 ...>, status"ACTIVE"
|
58
|
+
>> image.name
|
59
|
+
=> "CentOS 5.2"
|
60
|
+
>> flavor = cs.get_flavor(2)
|
61
|
+
=> #<CloudServers::Flavor:0x101469130 @disk=20, @name="512 server", @id=2, @ram=512>
|
62
|
+
>> flavor.name
|
63
|
+
=> "512 server"
|
64
|
+
>> newserver = cs.create_server(:name => "New Server", :imageId => image.id, :flavorId => flavor.id)
|
65
|
+
=> #<CloudServers::Server:0x101433f08 ....
|
66
|
+
>> newserver.status
|
67
|
+
=> "BUILD"
|
68
|
+
>> newserver.progress
|
69
|
+
=> 0
|
70
|
+
>> newserver.adminPass
|
71
|
+
=> "NewServerMbhzUnO"
|
72
|
+
>> newserver.refresh
|
73
|
+
=> true
|
74
|
+
>> newserver.progress
|
75
|
+
=> 12
|
76
|
+
|
77
|
+
# Delete the new server
|
78
|
+
>> newserver.delete!
|
79
|
+
=> true
|
80
|
+
|
81
|
+
== Authors
|
82
|
+
|
83
|
+
By H. Wade Minter <wade.minter@rackspace.com> and Mike Mayo <mike.mayo@rackspace.com>
|
84
|
+
|
85
|
+
== License
|
86
|
+
|
87
|
+
See COPYING for license information.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require './lib/cloudservers.rb'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gemspec|
|
7
|
+
gemspec.name = "cloudservers"
|
8
|
+
gemspec.summary = "Rackspace Cloud Servers Ruby API"
|
9
|
+
gemspec.description = "A Ruby API to version 1.0 of the Rackspace Cloud Servers product."
|
10
|
+
gemspec.email = "wade.minter@rackspace.com"
|
11
|
+
gemspec.homepage = "http://github.com/rackspace/cloudservers"
|
12
|
+
gemspec.authors = ["H. Wade Minter","Mike Mayo"]
|
13
|
+
gemspec.add_dependency 'json'
|
14
|
+
end
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler not available. Install it with: sudo gem install jeweler"
|
17
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
* There are caching bugs in the API, so that accurate information is not always returned (ie. if you add a server and
|
2
|
+
then list available servers, the new one will not show up.). That needs to be corrected on the API end before
|
3
|
+
data will be accurate.
|
4
|
+
|
5
|
+
* Add pagination in object listing.
|
6
|
+
|
7
|
+
* Allow some sort of flag to get the stack trace when an exception is thrown.
|
8
|
+
|
9
|
+
* Shared IP group modification code.
|
10
|
+
|
11
|
+
* Support the changes-since parameter.
|
12
|
+
|
13
|
+
* Tests. Accusations of heresy and/or apostasy are expected. :-)
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{cloudservers}
|
8
|
+
s.version = "0.2.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["H. Wade Minter", "Mike Mayo"]
|
12
|
+
s.date = %q{2010-05-06}
|
13
|
+
s.description = %q{A Ruby API to version 1.0 of the Rackspace Cloud Servers product.}
|
14
|
+
s.email = %q{wade.minter@rackspace.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.rdoc",
|
17
|
+
"TODO"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".gitignore",
|
21
|
+
"COPYING",
|
22
|
+
"README.rdoc",
|
23
|
+
"Rakefile",
|
24
|
+
"TODO",
|
25
|
+
"VERSION",
|
26
|
+
"cloudservers.gemspec",
|
27
|
+
"lib/cloudservers.rb",
|
28
|
+
"lib/cloudservers/authentication.rb",
|
29
|
+
"lib/cloudservers/connection.rb",
|
30
|
+
"lib/cloudservers/entity_manager.rb",
|
31
|
+
"lib/cloudservers/exception.rb",
|
32
|
+
"lib/cloudservers/flavor.rb",
|
33
|
+
"lib/cloudservers/image.rb",
|
34
|
+
"lib/cloudservers/server.rb",
|
35
|
+
"lib/cloudservers/shared_ip_group.rb",
|
36
|
+
"test/cloudservers_authentication_test.rb",
|
37
|
+
"test/test_helper.rb"
|
38
|
+
]
|
39
|
+
s.homepage = %q{http://github.com/rackspace/cloudservers}
|
40
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
41
|
+
s.require_paths = ["lib"]
|
42
|
+
s.rubygems_version = %q{1.3.6}
|
43
|
+
s.summary = %q{Rackspace Cloud Servers Ruby API}
|
44
|
+
s.test_files = [
|
45
|
+
"test/cloudservers_authentication_test.rb",
|
46
|
+
"test/test_helper.rb"
|
47
|
+
]
|
48
|
+
|
49
|
+
if s.respond_to? :specification_version then
|
50
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
51
|
+
s.specification_version = 3
|
52
|
+
|
53
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
54
|
+
s.add_runtime_dependency(%q<json>, [">= 0"])
|
55
|
+
else
|
56
|
+
s.add_dependency(%q<json>, [">= 0"])
|
57
|
+
end
|
58
|
+
else
|
59
|
+
s.add_dependency(%q<json>, [">= 0"])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module CloudServers
|
2
|
+
class Authentication
|
3
|
+
|
4
|
+
# Performs an authentication to the Rackspace Cloud authorization servers. Opens a new HTTP connection to the API server,
|
5
|
+
# sends the credentials, and looks for a successful authentication. If it succeeds, it sets the svrmgmthost,
|
6
|
+
# svrmgtpath, svrmgmtport, svrmgmtscheme, authtoken, and authok variables on the connection. If it fails, it raises
|
7
|
+
# an exception.
|
8
|
+
#
|
9
|
+
# Should probably never be called directly.
|
10
|
+
def initialize(connection)
|
11
|
+
path = '/v1.0'
|
12
|
+
hdrhash = { "X-Auth-User" => connection.authuser, "X-Auth-Key" => connection.authkey }
|
13
|
+
begin
|
14
|
+
server = Net::HTTP::Proxy(connection.proxy_host, connection.proxy_port).new('auth.api.rackspacecloud.com',443)
|
15
|
+
server.use_ssl = true
|
16
|
+
server.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
17
|
+
server.start
|
18
|
+
rescue
|
19
|
+
raise CloudServers::Exception::Connection, "Unable to connect to #{server}"
|
20
|
+
end
|
21
|
+
response = server.get(path,hdrhash)
|
22
|
+
if (response.code == "204")
|
23
|
+
connection.authtoken = response["x-auth-token"]
|
24
|
+
connection.svrmgmthost = URI.parse(response["x-server-management-url"]).host
|
25
|
+
connection.svrmgmtpath = URI.parse(response["x-server-management-url"]).path
|
26
|
+
# Force the path into the v1.0 URL space
|
27
|
+
connection.svrmgmtpath.sub!(/\/.*?\//, '/v1.0/')
|
28
|
+
connection.svrmgmtport = URI.parse(response["x-server-management-url"]).port
|
29
|
+
connection.svrmgmtscheme = URI.parse(response["x-server-management-url"]).scheme
|
30
|
+
connection.authok = true
|
31
|
+
else
|
32
|
+
connection.authtoken = false
|
33
|
+
raise CloudServers::Exception::Authentication, "Authentication failed with response code #{response.code}"
|
34
|
+
end
|
35
|
+
server.finish
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,306 @@
|
|
1
|
+
module CloudServers
|
2
|
+
class Connection
|
3
|
+
|
4
|
+
attr_reader :authuser
|
5
|
+
attr_reader :authkey
|
6
|
+
attr_accessor :authtoken
|
7
|
+
attr_accessor :authok
|
8
|
+
attr_accessor :svrmgmthost
|
9
|
+
attr_accessor :svrmgmtpath
|
10
|
+
attr_accessor :svrmgmtport
|
11
|
+
attr_accessor :svrmgmtscheme
|
12
|
+
attr_reader :proxy_host
|
13
|
+
attr_reader :proxy_port
|
14
|
+
|
15
|
+
# Creates a new CloudServers::Connection object. Uses CloudServers::Authentication to perform the login for the connection.
|
16
|
+
#
|
17
|
+
# Setting the retry_auth option to false will cause an exception to be thrown if your authorization token expires.
|
18
|
+
# Otherwise, it will attempt to reauthenticate.
|
19
|
+
#
|
20
|
+
# This is useful if you are using the library on a Rackspace-hosted system, as it provides faster speeds, keeps traffic off of
|
21
|
+
# the public network, and the bandwidth is not billed.
|
22
|
+
#
|
23
|
+
# This will likely be the base class for most operations.
|
24
|
+
#
|
25
|
+
# The constructor takes a hash of options, including:
|
26
|
+
#
|
27
|
+
# :username - Your Rackspace Cloud username *required*
|
28
|
+
# :api_key - Your Rackspace Cloud API key *required*
|
29
|
+
# :retry_auth - Whether to retry if your auth token expires (defaults to true)
|
30
|
+
# :proxy_host - If you need to connect through a proxy, supply the hostname here
|
31
|
+
# :proxy_port - If you need to connect through a proxy, supply the port here
|
32
|
+
#
|
33
|
+
# cf = CloudServers::Connection.new(:username => 'YOUR_USERNAME', :api_key => 'YOUR_API_KEY')
|
34
|
+
def initialize(options = {:retry_auth => true})
|
35
|
+
@authuser = options[:username] || (raise Authentication, "Must supply a :username")
|
36
|
+
@authkey = options[:api_key] || (raise Authentication, "Must supply an :api_key")
|
37
|
+
@retry_auth = options[:retry_auth]
|
38
|
+
@proxy_host = options[:proxy_host]
|
39
|
+
@proxy_port = options[:proxy_port]
|
40
|
+
@authok = false
|
41
|
+
@http = {}
|
42
|
+
CloudServers::Authentication.new(self)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns true if the authentication was successful and returns false otherwise.
|
46
|
+
#
|
47
|
+
# cs.authok?
|
48
|
+
# => true
|
49
|
+
def authok?
|
50
|
+
@authok
|
51
|
+
end
|
52
|
+
|
53
|
+
# This method actually makes the HTTP REST calls out to the server
|
54
|
+
def csreq(method,server,path,port,scheme,headers = {},data = nil,attempts = 0) # :nodoc:
|
55
|
+
start = Time.now
|
56
|
+
hdrhash = headerprep(headers)
|
57
|
+
start_http(server,path,port,scheme,hdrhash)
|
58
|
+
request = Net::HTTP.const_get(method.to_s.capitalize).new(path,hdrhash)
|
59
|
+
request.body = data
|
60
|
+
response = @http[server].request(request)
|
61
|
+
raise CloudServers::Exception::ExpiredAuthToken if response.code == "401"
|
62
|
+
response
|
63
|
+
rescue Errno::EPIPE, Timeout::Error, Errno::EINVAL, EOFError
|
64
|
+
# Server closed the connection, retry
|
65
|
+
raise CloudServers::Exception::Connection, "Unable to reconnect to #{server} after #{count} attempts" if attempts >= 5
|
66
|
+
attempts += 1
|
67
|
+
@http[server].finish
|
68
|
+
start_http(server,path,port,scheme,headers)
|
69
|
+
retry
|
70
|
+
rescue CloudServers::Exception::ExpiredAuthToken
|
71
|
+
raise CloudServers::Exception::Connection, "Authentication token expired and you have requested not to retry" if @retry_auth == false
|
72
|
+
CloudFiles::Authentication.new(self)
|
73
|
+
retry
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns the CloudServers::Server object identified by the given id.
|
77
|
+
#
|
78
|
+
# >> server = cs.get_server(110917)
|
79
|
+
# => #<CloudServers::Server:0x101407ae8 ...>
|
80
|
+
# >> server.name
|
81
|
+
# => "MyServer"
|
82
|
+
def get_server(id)
|
83
|
+
CloudServers::Server.new(self,id)
|
84
|
+
end
|
85
|
+
alias :server :get_server
|
86
|
+
|
87
|
+
# Returns an array of hashes, one for each server that exists under this account. The hash keys are :name and :id.
|
88
|
+
#
|
89
|
+
# >> cs.list_servers
|
90
|
+
# => [{:name=>"MyServer", :id=>110917}]
|
91
|
+
def list_servers(options = {})
|
92
|
+
url_params = "?limit=#{URI.escape(options[:limit].to_s)}&offset=#{URI.escape(options[:offset].to_s)}" if options[:limit] && options[:offset]
|
93
|
+
response = csreq("GET",svrmgmthost,"#{svrmgmtpath}/servers",svrmgmtport,svrmgmtscheme)
|
94
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
95
|
+
CloudServers.symbolize_keys(JSON.parse(response.body)["servers"])
|
96
|
+
end
|
97
|
+
alias :servers :list_servers
|
98
|
+
|
99
|
+
# Returns an array of hashes with more details about each server that exists under this account. Additional information
|
100
|
+
# includes public and private IP addresses, status, hostID, and more. All hash keys are symbols except for the metadata
|
101
|
+
# hash, which are verbatim strings.
|
102
|
+
#
|
103
|
+
# >> cs.list_servers_detail
|
104
|
+
# => [{:name=>"MyServer", :addresses=>{:public=>["67.23.42.37"], :private=>["10.176.241.237"]}, :metadata=>{"MyData" => "Valid"}, :imageId=>10, :progress=>100, :hostId=>"36143b12e9e48998c2aef79b50e144d2", :flavorId=>1, :id=>110917, :status=>"ACTIVE"}]
|
105
|
+
def list_servers_detail
|
106
|
+
response = csreq("GET",svrmgmthost,"#{svrmgmtpath}/servers/detail",svrmgmtport,svrmgmtscheme)
|
107
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
108
|
+
CloudServers.symbolize_keys(JSON.parse(response.body)["servers"])
|
109
|
+
end
|
110
|
+
alias :servers_detail :list_servers_detail
|
111
|
+
|
112
|
+
# Creates a new server instance on Cloud Servers
|
113
|
+
#
|
114
|
+
# The argument is a hash of options. The keys :name, :flavorId, and :imageId are required, :metadata and :personality are optional.
|
115
|
+
#
|
116
|
+
# :flavorId and :imageId are numbers identifying a particular server flavor and image to use when building the server. The :imageId can either
|
117
|
+
# be a stock Cloud Servers image, or one of your own created with the server.create_image method.
|
118
|
+
#
|
119
|
+
# The :metadata argument will take a hash of up to five key/value pairs. This metadata will be applied to the server at the Cloud Servers
|
120
|
+
# API level.
|
121
|
+
#
|
122
|
+
# The "Personality" option allows you to include up to five files, of 10Kb or less in size, that will be placed on the created server.
|
123
|
+
# For :personality, pass a hash of the form {'local_path' => 'server_path'}. The file located at local_path will be base64-encoded
|
124
|
+
# and placed at the location identified by server_path on the new server.
|
125
|
+
#
|
126
|
+
# Returns a CloudServers::Server object. The root password is available in the adminPass instance method.
|
127
|
+
#
|
128
|
+
# >> server = cs.create_server(:name => "New Server", :imageId => 2, :flavorId => 2, :metadata => {'Racker' => 'Fanatical'}, :personality => {'/Users/me/Pictures/wedding.jpg' => '/root/me.jpg'})
|
129
|
+
# => #<CloudServers::Server:0x101229eb0 ...>
|
130
|
+
# >> server.name
|
131
|
+
# => "NewServer"
|
132
|
+
# >> server.status
|
133
|
+
# => "BUILD"
|
134
|
+
# >> server.adminPass
|
135
|
+
# => "NewServerSHMGpvI"
|
136
|
+
def create_server(options)
|
137
|
+
raise CloudServers::Exception::MissingArgument, "Server name, flavor ID, and image ID must be supplied" unless (options[:name] && options[:flavorId] && options[:imageId])
|
138
|
+
options[:personality] = get_personality(options[:personality])
|
139
|
+
raise TooManyMetadataItems, "Metadata is limited to a total of #{MAX_PERSONALITY_METADATA_ITEMS} key/value pairs" if options[:metadata].is_a?(Hash) && options[:metadata].keys.size > MAX_PERSONALITY_METADATA_ITEMS
|
140
|
+
data = JSON.generate(:server => options)
|
141
|
+
response = csreq("POST",svrmgmthost,"#{svrmgmtpath}/servers",svrmgmtport,svrmgmtscheme,{'content-type' => 'application/json'},data)
|
142
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
143
|
+
server_info = JSON.parse(response.body)['server']
|
144
|
+
server = CloudServers::Server.new(self,server_info['id'])
|
145
|
+
server.adminPass = server_info['adminPass']
|
146
|
+
return server
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns an array of hashes listing available server images that you have access too, including stock Cloud Servers images and
|
150
|
+
# any that you have created. The "id" key in the hash can be used where imageId is required.
|
151
|
+
#
|
152
|
+
# >> cs.list_images
|
153
|
+
# => [{:name=>"CentOS 5.2", :id=>2, :updated=>"2009-07-20T09:16:57-05:00", :status=>"ACTIVE", :created=>"2009-07-20T09:16:57-05:00"},
|
154
|
+
# {:name=>"Gentoo 2008.0", :id=>3, :updated=>"2009-07-20T09:16:57-05:00", :status=>"ACTIVE", :created=>"2009-07-20T09:16:57-05:00"},...
|
155
|
+
def list_images
|
156
|
+
response = csreq("GET",svrmgmthost,"#{svrmgmtpath}/images/detail",svrmgmtport,svrmgmtscheme)
|
157
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
158
|
+
CloudServers.symbolize_keys(JSON.parse(response.body)['images'])
|
159
|
+
end
|
160
|
+
alias :images :list_images
|
161
|
+
|
162
|
+
# Returns a CloudServers::Image object for the image identified by the provided id.
|
163
|
+
#
|
164
|
+
# >> image = cs.get_image(8)
|
165
|
+
# => #<CloudServers::Image:0x101659698 ...>
|
166
|
+
def get_image(id)
|
167
|
+
CloudServers::Image.new(self,id)
|
168
|
+
end
|
169
|
+
alias :image :get_image
|
170
|
+
|
171
|
+
# Returns an array of hashes listing all available server flavors. The :id key in the hash can be used when flavorId is required.
|
172
|
+
#
|
173
|
+
# >> cs.list_flavors
|
174
|
+
# => [{:name=>"256 server", :id=>1, :ram=>256, :disk=>10},
|
175
|
+
# {:name=>"512 server", :id=>2, :ram=>512, :disk=>20},...
|
176
|
+
def list_flavors
|
177
|
+
response = csreq("GET",svrmgmthost,"#{svrmgmtpath}/flavors/detail",svrmgmtport,svrmgmtscheme)
|
178
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
179
|
+
CloudServers.symbolize_keys(JSON.parse(response.body)['flavors'])
|
180
|
+
end
|
181
|
+
alias :flavors :list_flavors
|
182
|
+
|
183
|
+
# Returns a CloudServers::Flavor object for the flavor identified by the provided ID.
|
184
|
+
#
|
185
|
+
# >> flavor = cs.flavor(1)
|
186
|
+
# => #<CloudServers::Flavor:0x10156dcc0 @name="256 server", @disk=10, @id=1, @ram=256>
|
187
|
+
def get_flavor(id)
|
188
|
+
CloudServers::Flavor.new(self,id)
|
189
|
+
end
|
190
|
+
alias :flavor :get_flavor
|
191
|
+
|
192
|
+
# Returns an array of hashes for all Shared IP Groups that are available. The :id key can be used to find that specific object.
|
193
|
+
#
|
194
|
+
# >> cs.list_shared_ip_groups
|
195
|
+
# => [{:name=>"New Group", :id=>127}]
|
196
|
+
def list_shared_ip_groups
|
197
|
+
response = csreq("GET",svrmgmthost,"#{svrmgmtpath}/shared_ip_groups/detail",svrmgmtport,svrmgmtscheme)
|
198
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
199
|
+
CloudServers.symbolize_keys(JSON.parse(response.body)['sharedIpGroups'])
|
200
|
+
end
|
201
|
+
alias :shared_ip_groups :list_shared_ip_groups
|
202
|
+
|
203
|
+
# Returns a CloudServers::SharedIPGroup object for the IP group identified by the provided ID.
|
204
|
+
#
|
205
|
+
# >> sig = cs.get_shared_ip_group(127)
|
206
|
+
# => #<CloudServers::SharedIPGroup:0x10153ca30 ...>
|
207
|
+
def get_shared_ip_group(id)
|
208
|
+
CloudServers::SharedIPGroup.new(self,id)
|
209
|
+
end
|
210
|
+
alias :shared_ip_group :get_shared_ip_group
|
211
|
+
|
212
|
+
# Creates a new Shared IP group. Takes a hash as an argument.
|
213
|
+
#
|
214
|
+
# Valid hash keys are :name (required) and :server (optional), which indicates the one server to place into this group by default.
|
215
|
+
#
|
216
|
+
# >> sig = cs.create_shared_ip_group(:name => "Production Web", :server => 110917)
|
217
|
+
# => #<CloudServers::SharedIPGroup:0x101501d18 ...>
|
218
|
+
# >> sig.name
|
219
|
+
# => "Production Web"
|
220
|
+
# >> sig.servers
|
221
|
+
# => [110917]
|
222
|
+
def create_shared_ip_group(options)
|
223
|
+
data = JSON.generate(:sharedIpGroup => options)
|
224
|
+
response = csreq("POST",svrmgmthost,"#{svrmgmtpath}/shared_ip_groups",svrmgmtport,svrmgmtscheme,{'content-type' => 'application/json'},data)
|
225
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
226
|
+
ip_group = JSON.parse(response.body)['sharedIpGroup']
|
227
|
+
CloudServers::SharedIPGroup.new(self,ip_group['id'])
|
228
|
+
end
|
229
|
+
|
230
|
+
# Returns the current state of the programatic API limits. Each account has certain limits on the number of resources
|
231
|
+
# allowed in the account, and a rate of API operations.
|
232
|
+
#
|
233
|
+
# The operation returns a hash. The :absolute hash key reveals the account resource limits, including the maxmimum
|
234
|
+
# amount of total RAM that can be allocated (combined among all servers), the maximum members of an IP group, and the
|
235
|
+
# maximum number of IP groups that can be created.
|
236
|
+
#
|
237
|
+
# The :rate hash key returns an array of hashes indicating the limits on the number of operations that can be performed in a
|
238
|
+
# given amount of time. An entry in this array looks like:
|
239
|
+
#
|
240
|
+
# {:regex=>"^/servers", :value=>50, :verb=>"POST", :remaining=>50, :unit=>"DAY", :resetTime=>1272399820, :URI=>"/servers*"}
|
241
|
+
#
|
242
|
+
# This indicates that you can only run 50 POST operations against URLs in the /servers URI space per day, we have not run
|
243
|
+
# any operations today (50 remaining), and gives the Unix time that the limits reset.
|
244
|
+
#
|
245
|
+
# Another example is:
|
246
|
+
#
|
247
|
+
# {:regex=>".*", :value=>10, :verb=>"PUT", :remaining=>10, :unit=>"MINUTE", :resetTime=>1272399820, :URI=>"*"}
|
248
|
+
#
|
249
|
+
# This says that you can run 10 PUT operations on all possible URLs per minute, and also gives the number remaining and the
|
250
|
+
# time that the limit resets.
|
251
|
+
#
|
252
|
+
# Use this information as you're building your applications to put in relevant pauses if you approach your API limitations.
|
253
|
+
def limits
|
254
|
+
response = csreq("GET",svrmgmthost,"#{svrmgmtpath}/limits",svrmgmtport,svrmgmtscheme)
|
255
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
256
|
+
CloudServers.symbolize_keys(JSON.parse(response.body)['limits'])
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
|
261
|
+
# Sets up standard HTTP headers
|
262
|
+
def headerprep(headers = {}) # :nodoc:
|
263
|
+
default_headers = {}
|
264
|
+
default_headers["X-Auth-Token"] = @authtoken if (authok? && @account.nil?)
|
265
|
+
default_headers["X-Storage-Token"] = @authtoken if (authok? && !@account.nil?)
|
266
|
+
default_headers["Connection"] = "Keep-Alive"
|
267
|
+
default_headers["User-Agent"] = "CloudServers Ruby API #{VERSION}"
|
268
|
+
default_headers["Accept"] = "application/json"
|
269
|
+
default_headers.merge(headers)
|
270
|
+
end
|
271
|
+
|
272
|
+
# Starts (or restarts) the HTTP connection
|
273
|
+
def start_http(server,path,port,scheme,headers) # :nodoc:
|
274
|
+
if (@http[server].nil?)
|
275
|
+
begin
|
276
|
+
@http[server] = Net::HTTP::Proxy(self.proxy_host, self.proxy_port).new(server,port)
|
277
|
+
if scheme == "https"
|
278
|
+
@http[server].use_ssl = true
|
279
|
+
@http[server].verify_mode = OpenSSL::SSL::VERIFY_NONE
|
280
|
+
end
|
281
|
+
@http[server].start
|
282
|
+
rescue
|
283
|
+
raise ConnectionException, "Unable to connect to #{server}"
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Handles parsing the Personality hash to load it up with Base64-encoded data.
|
289
|
+
def get_personality(options)
|
290
|
+
return if options.nil?
|
291
|
+
require 'base64'
|
292
|
+
data = []
|
293
|
+
itemcount = 0
|
294
|
+
options.each do |localpath,svrpath|
|
295
|
+
raise CloudServers::Exception::TooManyPersonalityItems, "Personality files are limited to a total of #{MAX_PERSONALITY_ITEMS} items" if itemcount >= MAX_PERSONALITY_ITEMS
|
296
|
+
raise CloudServers::Exception::PersonalityFilePathTooLong, "Server-side path of #{svrpath} exceeds the maximum length of #{MAX_SERVER_PATH_LENGTH} characters" if svrpath.size > MAX_SERVER_PATH_LENGTH
|
297
|
+
raise CloudServers::Exception::PersonalityFileTooLarge, "Local file #{localpath} exceeds the maximum size of #{MAX_PERSONALITY_FILE_SIZE} bytes" if File.size(localpath) > MAX_PERSONALITY_FILE_SIZE
|
298
|
+
b64 = Base64.encode64(IO.read(localpath))
|
299
|
+
data.push({:path => svrpath, :contents => b64})
|
300
|
+
itemcount += 1
|
301
|
+
end
|
302
|
+
return data
|
303
|
+
end
|
304
|
+
|
305
|
+
end
|
306
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module CloudServers
|
2
|
+
class Exception
|
3
|
+
|
4
|
+
class CloudServersFault < StandardError # :nodoc:
|
5
|
+
end
|
6
|
+
class ServiceUnavailable < StandardError # :nodoc:
|
7
|
+
end
|
8
|
+
class Unauthorized < StandardError # :nodoc:
|
9
|
+
end
|
10
|
+
class BadRequest < StandardError # :nodoc:
|
11
|
+
end
|
12
|
+
class OverLimit < StandardError # :nodoc:
|
13
|
+
end
|
14
|
+
class BadMediaType < StandardError # :nodoc:
|
15
|
+
end
|
16
|
+
class BadMethod < StandardError # :nodoc:
|
17
|
+
end
|
18
|
+
class ItemNotFound < StandardError # :nodoc:
|
19
|
+
end
|
20
|
+
class BuildInProgress < StandardError # :nodoc:
|
21
|
+
end
|
22
|
+
class ServerCapacityUnavailable < StandardError # :nodoc:
|
23
|
+
end
|
24
|
+
class BackupOrResizeInProgress < StandardError # :nodoc:
|
25
|
+
end
|
26
|
+
class ResizeNotAllowed < StandardError # :nodoc:
|
27
|
+
end
|
28
|
+
class NotImplemented < StandardError # :nodoc:
|
29
|
+
end
|
30
|
+
|
31
|
+
# Plus some others that we define here
|
32
|
+
|
33
|
+
class Other < StandardError # :nodoc:
|
34
|
+
end
|
35
|
+
class ExpiredAuthToken < StandardError # :nodoc:
|
36
|
+
end
|
37
|
+
class MissingArgument < StandardError # :nodoc:
|
38
|
+
end
|
39
|
+
class TooManyPersonalityItems < StandardError # :nodoc:
|
40
|
+
end
|
41
|
+
class PersonalityFilePathTooLong < StandardError # :nodoc:
|
42
|
+
end
|
43
|
+
class PersonalityFileTooLarge < StandardError # :nodoc:
|
44
|
+
end
|
45
|
+
class Authentication < StandardError # :nodoc:
|
46
|
+
end
|
47
|
+
class Connection < StandardError # :nodoc:
|
48
|
+
end
|
49
|
+
|
50
|
+
# In the event of a non-200 HTTP status code, this method takes the HTTP response, parses
|
51
|
+
# the JSON from the body to get more information about the exception, then raises the
|
52
|
+
# proper error. Note that all exceptions are scoped in the CloudServers::Exception namespace.
|
53
|
+
def self.raise_exception(response)
|
54
|
+
return if response.code =~ /^20.$/
|
55
|
+
fault,info = JSON.parse(response.body).first
|
56
|
+
begin
|
57
|
+
exception_class = self.const_get(fault[0,1].capitalize+fault[1,fault.length])
|
58
|
+
raise exception_class, info["message"]
|
59
|
+
rescue NameError
|
60
|
+
raise CloudServers::Exception::Other, "The server returned status #{response.code}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module CloudServers
|
2
|
+
class Flavor
|
3
|
+
|
4
|
+
attr_reader :id
|
5
|
+
attr_reader :name
|
6
|
+
attr_reader :ram
|
7
|
+
attr_reader :disk
|
8
|
+
|
9
|
+
# This class provides an object for the "Flavor" of a server. The Flavor can generally be taken as the server specification,
|
10
|
+
# providing information on things like memory and disk space.
|
11
|
+
#
|
12
|
+
# The disk attribute is an integer representing the disk space in GB. The memory attribute is an integer representing the RAM in MB.
|
13
|
+
#
|
14
|
+
# This is called from the get_flavor method on a CloudServers::Connection object, returns a CloudServers::Flavor object, and will likely not be called directly.
|
15
|
+
#
|
16
|
+
# >> flavor = cs.get_flavor(1)
|
17
|
+
# => #<CloudServers::Flavor:0x1014f8bc8 @name="256 server", @disk=10, @id=1, @ram=256>
|
18
|
+
# >> flavor.name
|
19
|
+
# => "256 server"
|
20
|
+
def initialize(connection,id)
|
21
|
+
response = connection.csreq("GET",connection.svrmgmthost,"#{connection.svrmgmtpath}/flavors/#{URI.escape(id.to_s)}",connection.svrmgmtport,connection.svrmgmtscheme)
|
22
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
23
|
+
data = JSON.parse(response.body)['flavor']
|
24
|
+
@id = data['id']
|
25
|
+
@name = data['name']
|
26
|
+
@ram = data['ram']
|
27
|
+
@disk = data['disk']
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module CloudServers
|
2
|
+
class Image
|
3
|
+
|
4
|
+
attr_reader :id
|
5
|
+
attr_reader :name
|
6
|
+
attr_reader :serverId
|
7
|
+
attr_reader :updated
|
8
|
+
attr_reader :created
|
9
|
+
attr_reader :status
|
10
|
+
attr_reader :progress
|
11
|
+
|
12
|
+
# This class provides an object for the "Image" of a server. The Image refers to the Operating System type and version.
|
13
|
+
#
|
14
|
+
# Returns the Image object identifed by the supplied ID number. Called from the get_image instance method of CloudServers::Connection,
|
15
|
+
# it will likely not be called directly from user code.
|
16
|
+
#
|
17
|
+
# >> cs = CloudServers::Connection.new(USERNAME,API_KEY)
|
18
|
+
# >> image = cs.get_image(2)
|
19
|
+
# => #<CloudServers::Image:0x1015371c0 ...>
|
20
|
+
# >> image.name
|
21
|
+
# => "CentOS 5.2"
|
22
|
+
def initialize(connection,id)
|
23
|
+
@id = id
|
24
|
+
@connection = connection
|
25
|
+
populate
|
26
|
+
end
|
27
|
+
|
28
|
+
# Makes the HTTP call to load information about the provided image. Can also be called directly on the Image object to refresh data.
|
29
|
+
# Returns true if the refresh call succeeds.
|
30
|
+
#
|
31
|
+
# >> image.populate
|
32
|
+
# => true
|
33
|
+
def populate
|
34
|
+
response = @connection.csreq("GET",@connection.svrmgmthost,"#{@connection.svrmgmtpath}/images/#{URI.escape(self.id.to_s)}",@connection.svrmgmtport,@connection.svrmgmtscheme)
|
35
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
36
|
+
data = JSON.parse(response.body)['image']
|
37
|
+
@id = data['id']
|
38
|
+
@name = data['name']
|
39
|
+
@serverId = data['serverId']
|
40
|
+
@updated = DateTime.parse(data['updated'])
|
41
|
+
@created = DateTime.parse(data['created'])
|
42
|
+
@status = data['status']
|
43
|
+
@progress = data['progress']
|
44
|
+
return true
|
45
|
+
end
|
46
|
+
alias :refresh :populate
|
47
|
+
|
48
|
+
# Delete an image. This should be returning invalid permissions when attempting to delete system images, but it's not.
|
49
|
+
# Returns true if the deletion succeeds.
|
50
|
+
#
|
51
|
+
# >> image.delete!
|
52
|
+
# => true
|
53
|
+
def delete!
|
54
|
+
response = @connection.csreq("DELETE",@connection.svrmgmthost,"#{@connection.svrmgmtpath}/images/#{URI.escape(self.id.to_s)}",@connection.svrmgmtport,@connection.svrmgmtscheme)
|
55
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
module CloudServers
|
2
|
+
class Server
|
3
|
+
|
4
|
+
attr_reader :id
|
5
|
+
attr_reader :name
|
6
|
+
attr_reader :status
|
7
|
+
attr_reader :progress
|
8
|
+
attr_reader :addresses
|
9
|
+
attr_reader :metadata
|
10
|
+
attr_reader :hostId
|
11
|
+
attr_reader :imageId
|
12
|
+
attr_reader :flavorId
|
13
|
+
attr_reader :metadata
|
14
|
+
attr_accessor :adminPass
|
15
|
+
|
16
|
+
# This class is the representation of a single Cloud Server object. The constructor finds the server identified by the specified
|
17
|
+
# ID number, accesses the API via the populate method to get information about that server, and returns the object.
|
18
|
+
#
|
19
|
+
# Will be called via the get_server or create_server methods on the CloudServers::Connection object, and will likely not be called directly.
|
20
|
+
#
|
21
|
+
# >> server = cs.get_server(110917)
|
22
|
+
# => #<CloudServers::Server:0x1014e5438 ....>
|
23
|
+
# >> server.name
|
24
|
+
# => "RenamedRubyTest"
|
25
|
+
def initialize(connection,id)
|
26
|
+
@connection = connection
|
27
|
+
@id = id
|
28
|
+
@svrmgmthost = connection.svrmgmthost
|
29
|
+
@svrmgmtpath = connection.svrmgmtpath
|
30
|
+
@svrmgmtport = connection.svrmgmtport
|
31
|
+
@svrmgmtscheme = connection.svrmgmtscheme
|
32
|
+
populate
|
33
|
+
return self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Makes the actual API call to get information about the given server object. If you are attempting to track the status or project of
|
37
|
+
# a server object (for example, when rebuilding, creating, or resizing a server), you will likely call this method within a loop until
|
38
|
+
# the status becomes "ACTIVE" or other conditions are met.
|
39
|
+
#
|
40
|
+
# Returns true if the API call succeeds.
|
41
|
+
#
|
42
|
+
# >> server.refresh
|
43
|
+
# => true
|
44
|
+
def populate
|
45
|
+
response = @connection.csreq("GET",@svrmgmthost,"#{@svrmgmtpath}/servers/#{URI.encode(@id.to_s)}",@svrmgmtport,@svrmgmtscheme)
|
46
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
47
|
+
data = JSON.parse(response.body)["server"]
|
48
|
+
@id = data["id"]
|
49
|
+
@name = data["name"]
|
50
|
+
@status = data["status"]
|
51
|
+
@progress = data["progress"]
|
52
|
+
@addresses = CloudServers.symbolize_keys(data["addresses"])
|
53
|
+
@metadata = data["metadata"]
|
54
|
+
@hostId = data["hostId"]
|
55
|
+
@imageId = data["imageId"]
|
56
|
+
@flavorId = data["flavorId"]
|
57
|
+
@metadata = data["metadata"]
|
58
|
+
true
|
59
|
+
end
|
60
|
+
alias :refresh :populate
|
61
|
+
|
62
|
+
# Returns a new CloudServers::Flavor object for the flavor assigned to this server.
|
63
|
+
#
|
64
|
+
# >> flavor = server.flavor
|
65
|
+
# => #<CloudServers::Flavor:0x1014aac20 @name="256 server", @disk=10, @id=1, @ram=256>
|
66
|
+
# >> flavor.name
|
67
|
+
# => "256 server"
|
68
|
+
def flavor
|
69
|
+
CloudServers::Flavor.new(@connection,self.flavorId)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns a new CloudServers::Image object for the image assigned to this server.
|
73
|
+
#
|
74
|
+
# >> image = server.image
|
75
|
+
# => #<CloudServers::Image:0x10149a960 ...>
|
76
|
+
# >> image.name
|
77
|
+
# => "Ubuntu 8.04.2 LTS (hardy)"
|
78
|
+
def image
|
79
|
+
CloudServers::Image.new(@connection,self.imageId)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Sends an API request to reboot this server. Takes an optional argument for the type of reboot, which can be "SOFT" (graceful shutdown)
|
83
|
+
# or "HARD" (power cycle). The hard reboot is also triggered by server.reboot!, so that may be a better way to call it.
|
84
|
+
#
|
85
|
+
# Returns true if the API call succeeds.
|
86
|
+
#
|
87
|
+
# >> server.reboot
|
88
|
+
# => true
|
89
|
+
def reboot(type="SOFT")
|
90
|
+
data = JSON.generate(:reboot => {:type => type})
|
91
|
+
response = @connection.csreq("POST",@svrmgmthost,"#{@svrmgmtpath}/servers/#{URI.encode(self.id.to_s)}/action",@svrmgmtport,@svrmgmtscheme,{'content-type' => 'application/json'},data)
|
92
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
# Sends an API request to hard-reboot (power cycle) the server. See the reboot method for more information.
|
97
|
+
#
|
98
|
+
# Returns true if the API call succeeds.
|
99
|
+
#
|
100
|
+
# >> server.reboot!
|
101
|
+
# => true
|
102
|
+
def reboot!
|
103
|
+
self.reboot("HARD")
|
104
|
+
end
|
105
|
+
|
106
|
+
# Updates various parameters about the server. Currently, the only operations supported are changing the server name (not the actual hostname
|
107
|
+
# on the server, but simply the label in the Cloud Servers API) and the administrator password (note: changing the admin password will trigger
|
108
|
+
# a reboot of the server). Other options are ignored. One or both key/value pairs may be provided. Keys are case-sensitive.
|
109
|
+
#
|
110
|
+
# Input hash key values are :name and :adminPass. Returns true if the API call succeeds.
|
111
|
+
#
|
112
|
+
# >> server.update(:name => "MyServer", :adminPass => "12345")
|
113
|
+
# => true
|
114
|
+
# >> server.name
|
115
|
+
# => "MyServer"
|
116
|
+
def update(options)
|
117
|
+
data = JSON.generate(:server => options)
|
118
|
+
response = @connection.csreq("PUT",@svrmgmthost,"#{@svrmgmtpath}/servers/#{URI.encode(self.id.to_s)}",@svrmgmtport,@svrmgmtscheme,{'content-type' => 'application/json'},data)
|
119
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
120
|
+
# If we rename the instance, repopulate the object
|
121
|
+
self.populate if options[:name]
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
# Deletes the server from Cloud Servers. The server will be shut down, data deleted, and billing stopped.
|
126
|
+
#
|
127
|
+
# Returns true if the API call succeeds.
|
128
|
+
#
|
129
|
+
# >> server.delete!
|
130
|
+
# => true
|
131
|
+
def delete!
|
132
|
+
response = @connection.csreq("DELETE",@svrmgmthost,"#{@svrmgmtpath}/servers/#{URI.encode(self.id.to_s)}",@svrmgmtport,@svrmgmtscheme)
|
133
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
# Takes the existing server and rebuilds it with the image identified by the imageId argument. If no imageId is provided, the current image
|
138
|
+
# will be used.
|
139
|
+
#
|
140
|
+
# This will wipe and rebuild the server, but keep the server ID number, name, and IP addresses the same.
|
141
|
+
#
|
142
|
+
# Returns true if the API call succeeds.
|
143
|
+
#
|
144
|
+
# >> server.rebuild!
|
145
|
+
# => true
|
146
|
+
def rebuild!(imageId = self.imageId)
|
147
|
+
data = JSON.generate(:rebuild => {:imageId => imageId})
|
148
|
+
response = @connection.csreq("POST",@svrmgmthost,"#{@svrmgmtpath}/servers/#{URI.encode(self.id.to_s)}/action",@svrmgmtport,@svrmgmtscheme,{'content-type' => 'application/json'},data)
|
149
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
150
|
+
self.populate
|
151
|
+
true
|
152
|
+
end
|
153
|
+
|
154
|
+
# Takes a snapshot of the server and creates a server image from it. That image can then be used to build new servers. The
|
155
|
+
# snapshot is saved asynchronously. Check the image status to make sure that it is ACTIVE before attempting to perform operations
|
156
|
+
# on it.
|
157
|
+
#
|
158
|
+
# A name string for the saved image must be provided. A new CloudServers::Image object for the saved image is returned.
|
159
|
+
#
|
160
|
+
# The image is saved as a backup, of which there are only three available slots. If there are no backup slots available,
|
161
|
+
# A CloudServers::Exception::CloudServersFault will be raised.
|
162
|
+
#
|
163
|
+
# >> image = server.create_image("My Rails Server")
|
164
|
+
# =>
|
165
|
+
def create_image(name)
|
166
|
+
data = JSON.generate(:image => {:serverId => self.id, :name => name})
|
167
|
+
response = @connection.csreq("POST",@svrmgmthost,"#{@svrmgmtpath}/images",@svrmgmtport,@svrmgmtscheme,{'content-type' => 'application/json'},data)
|
168
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
169
|
+
CloudServers::Image.new(@connection,JSON.parse(response.body)['image']['id'])
|
170
|
+
end
|
171
|
+
|
172
|
+
# Resizes the server to the size contained in the server flavor found at ID flavorId. The server name, ID number, and IP addresses
|
173
|
+
# will remain the same. After the resize is done, the server.status will be set to "VERIFY_RESIZE" until the resize is confirmed or reverted.
|
174
|
+
#
|
175
|
+
# Refreshes the CloudServers::Server object, and returns true if the API call succeeds.
|
176
|
+
#
|
177
|
+
# >> server.resize!(1)
|
178
|
+
# => true
|
179
|
+
def resize!(flavorId)
|
180
|
+
data = JSON.generate(:resize => {:flavorId => flavorId})
|
181
|
+
response = @connection.csreq("POST",@svrmgmthost,"#{@svrmgmtpath}/servers/#{URI.encode(self.id.to_s)}/action",@svrmgmtport,@svrmgmtscheme,{'content-type' => 'application/json'},data)
|
182
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
183
|
+
self.populate
|
184
|
+
true
|
185
|
+
end
|
186
|
+
|
187
|
+
# After a server resize is complete, calling this method will confirm the resize with the Cloud Servers API, and discard the fallback/original image.
|
188
|
+
#
|
189
|
+
# Returns true if the API call succeeds.
|
190
|
+
#
|
191
|
+
# >> server.confirm_resize!
|
192
|
+
# => true
|
193
|
+
def confirm_resize!
|
194
|
+
# If the resize bug gets figured out, should put a check here to make sure that it's in the proper state for this.
|
195
|
+
data = JSON.generate(:confirmResize => nil)
|
196
|
+
response = @connection.csreq("POST",@svrmgmthost,"#{@svrmgmtpath}/servers/#{URI.encode(self.id.to_s)}/action",@svrmgmtport,@svrmgmtscheme,{'content-type' => 'application/json'},data)
|
197
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
198
|
+
self.populate
|
199
|
+
true
|
200
|
+
end
|
201
|
+
|
202
|
+
# After a server resize is complete, calling this method will reject the resized server with the Cloud Servers API, destroying
|
203
|
+
# the new image and replacing it with the pre-resize fallback image.
|
204
|
+
#
|
205
|
+
# Returns true if the API call succeeds.
|
206
|
+
#
|
207
|
+
# >> server.confirm_resize!
|
208
|
+
# => true
|
209
|
+
def revert_resize!
|
210
|
+
# If the resize bug gets figured out, should put a check here to make sure that it's in the proper state for this.
|
211
|
+
data = JSON.generate(:revertResize => nil)
|
212
|
+
response = @connection.csreq("POST",@svrmgmthost,"#{@svrmgmtpath}/servers/#{URI.encode(self.id.to_s)}/action",@svrmgmtport,@svrmgmtscheme,{'content-type' => 'application/json'},data)
|
213
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
214
|
+
self.populate
|
215
|
+
true
|
216
|
+
end
|
217
|
+
|
218
|
+
# Provides information about the backup schedule for this server. Returns a hash of the form
|
219
|
+
# {"weekly" => state, "daily" => state, "enabled" => boolean}
|
220
|
+
#
|
221
|
+
# >> server.backup_schedule
|
222
|
+
# => {"weekly"=>"THURSDAY", "daily"=>"H_0400_0600", "enabled"=>true}
|
223
|
+
def backup_schedule
|
224
|
+
response = @connection.csreq("GET",@svrmgmthost,"#{@svrmgmtpath}/servers/#{URI.encode(@id.to_s)}/backup_schedule",@svrmgmtport,@svrmgmtscheme)
|
225
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
226
|
+
JSON.parse(response.body)['backupSchedule']
|
227
|
+
end
|
228
|
+
|
229
|
+
# Updates the backup schedule for the server. Takes a hash of the form: {:weekly => state, :daily => state, :enabled => boolean} as an argument.
|
230
|
+
# All three keys (:weekly, :daily, :enabled) must be provided or an exception will get raised.
|
231
|
+
#
|
232
|
+
# >> server.backup_schedule=({:weekly=>"THURSDAY", :daily=>"H_0400_0600", :enabled=>true})
|
233
|
+
# => {:weekly=>"THURSDAY", :daily=>"H_0400_0600", :enabled=>true}
|
234
|
+
def backup_schedule=(options)
|
235
|
+
data = JSON.generate('backupSchedule' => options)
|
236
|
+
response = @connection.csreq("POST",@svrmgmthost,"#{@svrmgmtpath}/servers/#{URI.encode(self.id.to_s)}/backup_schedule",@svrmgmtport,@svrmgmtscheme,{'content-type' => 'application/json'},data)
|
237
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
238
|
+
true
|
239
|
+
end
|
240
|
+
|
241
|
+
# Removes the existing backup schedule for the server, setting the backups to disabled.
|
242
|
+
#
|
243
|
+
# Returns true if the API call succeeds.
|
244
|
+
#
|
245
|
+
# >> server.disable_backup_schedule!
|
246
|
+
# => true
|
247
|
+
def disable_backup_schedule!
|
248
|
+
response = @connection.csreq("DELETE",@svrmgmthost,"#{@svrmgmtpath}/servers/#{URI.encode(self.id.to_s)}/backup_schedule",@svrmgmtport,@svrmgmtscheme)
|
249
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
250
|
+
true
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module CloudServers
|
2
|
+
class SharedIPGroup
|
3
|
+
|
4
|
+
attr_reader :id
|
5
|
+
attr_reader :name
|
6
|
+
attr_reader :servers
|
7
|
+
|
8
|
+
# Creates a new Shared IP Group object, with information on the group identified by the ID number. Will most likely be called
|
9
|
+
# by the get_shared_ip_group method on a CloudServers::Connection object.
|
10
|
+
#
|
11
|
+
# >> sig = cs.get_shared_ip_group(127)
|
12
|
+
# => #<CloudServers::SharedIPGroup:0x101513798 ...>
|
13
|
+
# >> sig.name
|
14
|
+
# => "New Group"
|
15
|
+
def initialize(connection,id)
|
16
|
+
@connection = connection
|
17
|
+
@id = id
|
18
|
+
populate
|
19
|
+
end
|
20
|
+
|
21
|
+
# Makes the API call that populates the CloudServers::SharedIPGroup object with information on the group. Can also be called directly on
|
22
|
+
# an existing object to update its information.
|
23
|
+
#
|
24
|
+
# Returns true if the API call succeeds.
|
25
|
+
#
|
26
|
+
# >> sig.populate
|
27
|
+
# => true
|
28
|
+
def populate
|
29
|
+
response = @connection.csreq("GET",@connection.svrmgmthost,"#{@connection.svrmgmtpath}/shared_ip_groups/#{URI.escape(self.id.to_s)}",@connection.svrmgmtport,@connection.svrmgmtscheme)
|
30
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
31
|
+
data = JSON.parse(response.body)['sharedIpGroup']
|
32
|
+
@id = data['id']
|
33
|
+
@name = data['name']
|
34
|
+
@servers = data['servers']
|
35
|
+
true
|
36
|
+
end
|
37
|
+
alias :refresh :populate
|
38
|
+
|
39
|
+
# Deletes the Shared IP Group identified by the current object.
|
40
|
+
#
|
41
|
+
# Returns true if the API call succeeds.
|
42
|
+
#
|
43
|
+
# >> sig.delete!
|
44
|
+
# => true
|
45
|
+
def delete!
|
46
|
+
response = @connection.csreq("DELETE",@connection.svrmgmthost,"#{@connection.svrmgmtpath}/shared_ip_groups/#{URI.escape(self.id.to_s)}",@connection.svrmgmtport,@connection.svrmgmtscheme)
|
47
|
+
CloudServers::Exception.raise_exception(response) unless response.code.match(/^20.$/)
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/cloudservers.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# == Cloud Servers API
|
4
|
+
# ==== Connects Ruby Applications to Rackspace's {Cloud Servers service}[http://www.rackspacecloud.com/cloud_hosting_products/servers]
|
5
|
+
# By H. Wade Minter <wade.minter@rackspace.com> and Mike Mayo <mike.mayo@rackspace.com>
|
6
|
+
#
|
7
|
+
# See COPYING for license information.
|
8
|
+
# Copyright (c) 2009, Rackspace US, Inc.
|
9
|
+
# ----
|
10
|
+
#
|
11
|
+
# === Documentation & Examples
|
12
|
+
# To begin reviewing the available methods and examples, peruse the README.rodc file, or begin by looking at documentation for the
|
13
|
+
# CloudServers::Connection class.
|
14
|
+
#
|
15
|
+
# The CloudServers class is the base class. Not much of note aside from housekeeping happens here.
|
16
|
+
# To create a new CloudServers connection, use the CloudServers::Connection.new('user_name', 'api_key') method.
|
17
|
+
|
18
|
+
module CloudServers
|
19
|
+
|
20
|
+
VERSION = IO.read(File.dirname(__FILE__) + '/../VERSION')
|
21
|
+
require 'net/http'
|
22
|
+
require 'net/https'
|
23
|
+
require 'uri'
|
24
|
+
require 'rubygems'
|
25
|
+
require 'json'
|
26
|
+
|
27
|
+
unless "".respond_to? :each_char
|
28
|
+
require "jcode"
|
29
|
+
$KCODE = 'u'
|
30
|
+
end
|
31
|
+
|
32
|
+
$:.unshift(File.dirname(__FILE__))
|
33
|
+
require 'cloudservers/authentication'
|
34
|
+
require 'cloudservers/connection'
|
35
|
+
require 'cloudservers/server'
|
36
|
+
require 'cloudservers/image'
|
37
|
+
require 'cloudservers/flavor'
|
38
|
+
require 'cloudservers/shared_ip_group'
|
39
|
+
require 'cloudservers/exception'
|
40
|
+
|
41
|
+
# Constants that set limits on server creation
|
42
|
+
MAX_PERSONALITY_ITEMS = 5
|
43
|
+
MAX_PERSONALITY_FILE_SIZE = 10240
|
44
|
+
MAX_SERVER_PATH_LENGTH = 255
|
45
|
+
MAX_PERSONALITY_METADATA_ITEMS = 5
|
46
|
+
|
47
|
+
# Helper method to recursively symbolize hash keys.
|
48
|
+
def self.symbolize_keys(obj)
|
49
|
+
case obj
|
50
|
+
when Array
|
51
|
+
obj.inject([]){|res, val|
|
52
|
+
res << case val
|
53
|
+
when Hash, Array
|
54
|
+
symbolize_keys(val)
|
55
|
+
else
|
56
|
+
val
|
57
|
+
end
|
58
|
+
res
|
59
|
+
}
|
60
|
+
when Hash
|
61
|
+
obj.inject({}){|res, (key, val)|
|
62
|
+
nkey = case key
|
63
|
+
when String
|
64
|
+
key.to_sym
|
65
|
+
else
|
66
|
+
key
|
67
|
+
end
|
68
|
+
nval = case val
|
69
|
+
when Hash, Array
|
70
|
+
symbolize_keys(val)
|
71
|
+
else
|
72
|
+
val
|
73
|
+
end
|
74
|
+
res[nkey] = nval
|
75
|
+
res
|
76
|
+
}
|
77
|
+
else
|
78
|
+
obj
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class CloudserversAuthenticationTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
|
6
|
+
def test_good_authentication
|
7
|
+
response = {'x-cdn-management-url' => 'http://cdn.example.com/path', 'x-storage-url' => 'http://cdn.example.com/storage', 'authtoken' => 'dummy_token'}
|
8
|
+
response.stubs(:code).returns('204')
|
9
|
+
server = mock(:use_ssl= => true, :verify_mode= => true, :start => true, :finish => true)
|
10
|
+
server.stubs(:get).returns(response)
|
11
|
+
Net::HTTP.stubs(:new).returns(server)
|
12
|
+
@connection = stub(:authuser => 'dummy_user', :authkey => 'dummy_key', :cdnmgmthost= => true, :cdnmgmtpath= => true, :cdnmgmtport= => true, :cdnmgmtscheme= => true, :storagehost= => true, :storagepath= => true, :storageport= => true, :storagescheme= => true, :authtoken= => true, :authok= => true)
|
13
|
+
result = CloudServers::Authentication.new(@connection)
|
14
|
+
assert_equal result.class, CloudServers::Authentication
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_bad_authentication
|
18
|
+
response = mock()
|
19
|
+
response.stubs(:code).returns('499')
|
20
|
+
server = mock(:use_ssl= => true, :verify_mode= => true, :start => true)
|
21
|
+
server.stubs(:get).returns(response)
|
22
|
+
Net::HTTP.stubs(:new).returns(server)
|
23
|
+
@connection = stub(:authuser => 'bad_user', :authkey => 'bad_key', :authok= => true, :authtoken= => true)
|
24
|
+
assert_raises(AuthenticationException) do
|
25
|
+
result = CloudServers::Authentication.new(@connection)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_bad_hostname
|
30
|
+
Net::HTTP.stubs(:new).raises(ConnectionException)
|
31
|
+
@connection = stub(:authuser => 'bad_user', :authkey => 'bad_key', :authok= => true, :authtoken= => true)
|
32
|
+
assert_raises(ConnectionException) do
|
33
|
+
result = CloudServers::Authentication.new(@connection)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cloudservers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: 0.2.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- H. Wade Minter
|
13
|
+
- Mike Mayo
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-05-06 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: json
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
description: A Ruby API to version 1.0 of the Rackspace Cloud Servers product.
|
34
|
+
email: wade.minter@rackspace.com
|
35
|
+
executables: []
|
36
|
+
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files:
|
40
|
+
- README.rdoc
|
41
|
+
- TODO
|
42
|
+
files:
|
43
|
+
- .gitignore
|
44
|
+
- COPYING
|
45
|
+
- README.rdoc
|
46
|
+
- Rakefile
|
47
|
+
- TODO
|
48
|
+
- VERSION
|
49
|
+
- cloudservers.gemspec
|
50
|
+
- lib/cloudservers.rb
|
51
|
+
- lib/cloudservers/authentication.rb
|
52
|
+
- lib/cloudservers/connection.rb
|
53
|
+
- lib/cloudservers/entity_manager.rb
|
54
|
+
- lib/cloudservers/exception.rb
|
55
|
+
- lib/cloudservers/flavor.rb
|
56
|
+
- lib/cloudservers/image.rb
|
57
|
+
- lib/cloudservers/server.rb
|
58
|
+
- lib/cloudservers/shared_ip_group.rb
|
59
|
+
- test/cloudservers_authentication_test.rb
|
60
|
+
- test/test_helper.rb
|
61
|
+
has_rdoc: true
|
62
|
+
homepage: http://github.com/rackspace/cloudservers
|
63
|
+
licenses: []
|
64
|
+
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options:
|
67
|
+
- --charset=UTF-8
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
segments:
|
82
|
+
- 0
|
83
|
+
version: "0"
|
84
|
+
requirements: []
|
85
|
+
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 1.3.6
|
88
|
+
signing_key:
|
89
|
+
specification_version: 3
|
90
|
+
summary: Rackspace Cloud Servers Ruby API
|
91
|
+
test_files:
|
92
|
+
- test/cloudservers_authentication_test.rb
|
93
|
+
- test/test_helper.rb
|