right_api_provision 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ *.tmproj
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ -fs
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 caryp
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # RightApiProvision
2
+
3
+ Basic helper gem for provisioning IaaS servers using the RightScale API.
4
+
5
+ It is intended to be used by ruby applications that need to launch servers.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'right_api_provision'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ ## Usage
18
+
19
+ See lib/right_api_provisioner/provisioner.rb for all public methods.
20
+
21
+ Example:
22
+
23
+ require "right_api_provision"
24
+
25
+ # initialize rightscale provisioner
26
+ @rightscale =
27
+ RightApiProvision::Provisioner.new("my_user@somewhere.com", // user
28
+ "my_rightscale_password", // password
29
+ 12345) // rightscale account ID
30
+
31
+ # setup some inputs
32
+ server_inputs = {
33
+ # open up port 8000
34
+ "sys_firewall/rule/enable" => "text:enable",
35
+ "sys_firewall/rule/port" => "text:8000",
36
+ "sys_firewall/rule/ip_address" => "text:any",
37
+ "sys_firewall/rule/protocol" => "text:tcp"
38
+ }
39
+
40
+ # provision a RightScale managed server from a ServerTemplate
41
+ @rightscale.provision("My Cool Server",
42
+ "ServerTemplate for Linux (v13.5)", // name or ID
43
+ "AWS US-East",
44
+ "My Deployment",
45
+ server_inputs)
46
+
47
+ # wait for server to be ready
48
+ state = @rightscale.wait_for_state("operational", 30)
49
+ if state != "operational"
50
+ raise "Unexpected state. State: #{state}"
51
+ end
52
+
53
+ # Do stuff with your brand new server...
54
+
55
+
56
+ ## TODO
57
+
58
+ 1. document this readme
59
+ 2. add logger class
60
+
61
+
62
+ ## Contributing
63
+
64
+ 1. Fork it
65
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
66
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
67
+ 4. Push to the branch (`git push origin my-new-feature`)
68
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ task :default => :spec
7
+
8
+ desc "Run all specs in spec directory"
9
+ RSpec::Core::RakeTask.new(:spec) do |t|
10
+ t.pattern = 'spec/**/*_spec.rb'
11
+ end
12
+
13
+ rescue LoadError
14
+ STDERR.puts "\n*** RSpec not available. (sudo) gem install rspec to run unit tests. ***\n\n"
15
+ end
16
+
@@ -0,0 +1,227 @@
1
+ module RightApiProvision
2
+ class API15
3
+
4
+ attr_reader :client
5
+
6
+ def initialize
7
+ require "right_api_client"
8
+ end
9
+
10
+ def connection(email, password, account_id, api_url = nil)
11
+ begin
12
+ args = { :email => email, :password => password, :account_id => account_id }
13
+ @url = api_url
14
+ args[:api_url] = @url if @url
15
+ @connection ||= RightApi::Client.new(args)
16
+ @client = @connection
17
+ rescue Exception => e
18
+ args.delete(:password) # don't log password
19
+ puts "ERROR: could not connect to RightScale API. Params: #{args.inspect}"
20
+ puts e.message
21
+ puts e.backtrace
22
+ raise e
23
+ end
24
+ end
25
+
26
+ def user_data(server)
27
+ @user_data ||= server.show.current_instance(:view=>"extended").show.user_data
28
+ end
29
+
30
+ def data_request_url(userdata)
31
+ data_hash = {}
32
+ entry = userdata.split('&').select { |entry| entry =~ /RS_rn_auth/i }
33
+ raise "ERROR: user data token not found. " +
34
+ "Does your MCI have a provides:rs_agent_type=right_link tag?" unless entry
35
+ token = entry.first.split('=')[1]
36
+ "#{@url}/servers/data_injection_payload/#{token}"
37
+ end
38
+
39
+ def find_server_by_name(name)
40
+ server_list = @connection.servers.index(:filter => ["name==#{name}"])
41
+ raise "More than one server with the name of '#{name}'. " +
42
+ "Please fix via the RightScale dashboard and retry." if server_list.size > 1
43
+ server_list.first
44
+ end
45
+
46
+ def find_deployment_by_name(name)
47
+ deployment = nil
48
+ deployments_list = @connection.deployments.index(:filter => ["name==#{name}"])
49
+ raise "More than one deployment with the name of '#{name}'. " +
50
+ "Please fix via the RightScale dashboard and retry." if deployments_list.size > 1
51
+ deployment = deployments_list.first unless deployments_list.empty?
52
+ deployment
53
+ end
54
+
55
+ def list_clouds
56
+ @connection.clouds.index
57
+ end
58
+
59
+ # returns:: String if cloud is found, nil if not found
60
+ def find_cloud_by_name(name)
61
+ cloud = nil
62
+ cloud_list = @connection.clouds.index(:filter => ["name==#{name}"])
63
+ raise "More than one cloud with the name of '#{name}'. " +
64
+ "Please fix via the RightScale dashboard and retry." if cloud_list.size > 1
65
+ cloud = cloud_list.first unless cloud_list.empty?
66
+ cloud
67
+ end
68
+
69
+ def find_mci_by_name(mci_name)
70
+ mci = nil
71
+ mci_list = @connection.multi_cloud_images.index(:filter => ["name==#{mci_name}"])
72
+ raise "More than one MultiCloud image with the name of '#{mci_name}'. " +
73
+ "Please fix via the RightScale dashboard and retry." if mci_list.size > 1
74
+ mci = mci_list.first unless mci_list.empty?
75
+ mci
76
+ end
77
+
78
+ def find_servertemplate(name_or_id)
79
+ server_template = nil; id = nil; name = nil
80
+
81
+ # detect if user passed in a name or an id
82
+ # there is probably a cleaner way to do this, but I am lazy ATM.
83
+ begin
84
+ id = Integer(name_or_id)
85
+ rescue Exception => e
86
+ name = name_or_id # Cannot be case to integer, assume a name was passed
87
+ end
88
+
89
+ if name
90
+ # find ServerTemplate by name
91
+ st_list = @connection.server_templates.index(:filter => ["name==#{name}"])
92
+ num_matching_sts = 0
93
+ st_list.each do |st|
94
+ if st.name == name
95
+ server_template = st
96
+ num_matching_sts += 1
97
+ end
98
+ end
99
+ raise "ERROR: Unable to find ServerTemplate with the name of '#{name}' found " unless server_template
100
+ raise "ERROR: More than one ServerTemplate with the name of '#{name}' found " +
101
+ "in account. Please fix via the RightScale dashboard and retry." if num_matching_sts > 1
102
+
103
+ else
104
+ # find ServerTemplate by id
105
+ server_template = @connection.server_templates.index(:id => id)
106
+ end
107
+
108
+ server_template
109
+ end
110
+
111
+ def create_deployment(name)
112
+ @connection.deployments.create(:deployment => { :name => name, :decription => "Created by the Vagrant"})
113
+ end
114
+
115
+ def destroy_deployment(deployment)
116
+ deployment.destroy
117
+ end
118
+
119
+ def create_server(deployment, server_template, mci, cloud, name)
120
+ # check params
121
+ unless st_href = server_template.show.href
122
+ raise "ERROR: ServerTemplate parameter not initialized properly"
123
+ end
124
+
125
+ unless mci.nil?
126
+ unless mci_href = mci.show.href
127
+ raise "ERROR: Multi Cloud Image parameter not initialized properly"
128
+ end
129
+ end
130
+
131
+ unless d_href = deployment.show.href
132
+ raise "ERROR: Deployment parameter not initialized properly"
133
+ end
134
+
135
+ unless c_href = cloud.show.href
136
+ raise "ERROR: Deployment parameter not initialized properly"
137
+ end
138
+
139
+ # create server in deployment using specfied ST
140
+ create_params = {
141
+ :server => {
142
+ :name => name,
143
+ :decription => "Created by the Vagrant",
144
+ :deployment_href => d_href,
145
+ :instance => {
146
+ :cloud_href => c_href,
147
+ :server_template_href => st_href
148
+ }
149
+ }
150
+ }
151
+ # Use the MCI if provided otherwise let the API choose the default MCI
152
+ # in the ServerTemplate.
153
+ create_params[:server][:instance][:multi_cloud_image_href] = mci_href unless mci_href.nil?
154
+ server = @connection.servers.create(create_params)
155
+ end
156
+
157
+ def is_provisioned?(server)
158
+ server.show.api_methods.include?(:current_instance)
159
+ end
160
+
161
+ # @param(Hash) inputs Hash input name/value pairs i.e. { :name => "text:dummy"}
162
+ def launch_server(server, inputs = { :name => "text:dummy"})
163
+ server_name = server.show.name
164
+ server.launch(inputs) # TODO: parse inputs from Vagrantfile
165
+ # XXX: need to create a new server object after launch -- why? API bug?
166
+ find_server_by_name(server_name)
167
+ end
168
+
169
+ def terminate_server(server)
170
+ server.terminate
171
+ end
172
+
173
+ # Only use this *before* you launch the server
174
+ def set_server_inputs(server, inputs)
175
+ server.show.next_instance.show.inputs.multi_update({"inputs" => inputs})
176
+ end
177
+
178
+ def server_wait_for_state(server, target_state, delay = 10)
179
+ current_state = server_state(server)
180
+ while current_state != target_state
181
+ raise "Unexpected sever state: #{current_state}" if is_bad?(current_state)
182
+ puts "Server #{current_state}. Waiting for instance to be in #{target_state} state..."
183
+ sleep delay
184
+ current_state = server_state(server)
185
+ end
186
+ end
187
+
188
+ def set_bad_states(list_array)
189
+ @bad_states = list_array
190
+ end
191
+
192
+ def is_bad?(state)
193
+ @bad_states ||= []
194
+ @bad_states.select{|s| state =~ /#{s}/}.size > 0
195
+ end
196
+
197
+ def server_ready?(server)
198
+ server_state(server) == "operational"
199
+ end
200
+
201
+ def server_cloud_name(server)
202
+ instance = instance_from_server(server)
203
+ cloud = cloud_from_instance(instance)
204
+ cloud.show.name
205
+ end
206
+
207
+ private
208
+
209
+ def server_state(server)
210
+ instance_from_server(server).show.state
211
+ end
212
+
213
+ def instance_from_server(server)
214
+ server_data = server.show
215
+ if is_provisioned?(server)
216
+ server_data.current_instance
217
+ else
218
+ server_data.next_instance
219
+ end
220
+ end
221
+
222
+ def cloud_from_instance(instance)
223
+ instance.show.cloud
224
+ end
225
+
226
+ end
227
+ end
@@ -0,0 +1,31 @@
1
+ #
2
+ # Author:: Cary Penniman (<cary@rightscale.com>)
3
+ # Copyright:: Copyright (c) 2013 RightScale, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module RightApiProvision
20
+ class RightScaleError < StandardError
21
+ end
22
+
23
+ class MultipleMatchesFound < RightScaleError
24
+ def initialize(resource, key, value)
25
+ key = key.to_s.delete("by_") # remove the 'by_' prefix
26
+ msg = "More than one #{resource} with the #{key} of '#{value}'. " +
27
+ "Please resolve via the RightScale dashboard and retry."
28
+ super msg
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,193 @@
1
+ #
2
+ # Author:: Cary Penniman (<cary@rightscale.com>)
3
+ # Copyright:: Copyright (c) 2013 RightScale, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module RightApiProvision
20
+
21
+ #
22
+ # This is the main class to use to create a server on the RightScale platform.
23
+ # Use the {#provision} method to create and launch
24
+ # the server.
25
+ #
26
+ # The other methods are for checking server state and gathering information
27
+ # once the server is operational.
28
+ #
29
+ class Provisioner
30
+
31
+ require "logger"
32
+
33
+ RETRY_DELAY = 10 # seconds
34
+
35
+ def initialize(email, password, account_id, api_url = nil)
36
+ require_relative "exception"
37
+ require_relative "api15"
38
+ @logger = ::Logger.new(STDOUT)
39
+ @client = RightApiProvision::API15.new
40
+ @client.connection(email, password, account_id, api_url)
41
+ end
42
+
43
+ def logger(logger)
44
+ @logger = logger
45
+ end
46
+
47
+ def connection_url
48
+ raise "No server provisioned. No connection URL available." unless @server
49
+ unless @data_request_url
50
+ user_data = @server.current_instance.show(:view => "full").user_data
51
+ @data_request_url = @client.data_request_url(user_data)
52
+ @logger.debug "Data Request URL: #{@data_request_url}"
53
+ end
54
+ @data_request_url
55
+ end
56
+
57
+ def wait_for_state(desired_state, timeout_sec = RETRY_DELAY)
58
+ @client.server_wait_for_state(@server, desired_state, timeout_sec)
59
+ end
60
+
61
+ # Provision a server using RightScale
62
+ #
63
+ # @param server_name [String] the name to give the server that will be
64
+ # created.
65
+ # @param server_template [String] the name or ID of the ServerTemplate to
66
+ # create the server from.
67
+ # @param cloud_name [String] name of cloud to provision on.
68
+ # @param deployment_name [String] name of deployment to add the server to.
69
+ # This will be created if it does not exist.
70
+ # @param server_inputs [Array] An array of {Input} objects.
71
+ # @param ssh_key_id [String] The resource_uuid of an ssh key from the
72
+ # RightScale dashboard. Only required on EC2 and Eucalyptus.
73
+ # @param secgroup_id [Array] An array of security group IDs to place the
74
+ # server in.
75
+ #
76
+ # @raise {RightApiProvisionException} if anything
77
+ # goes wrong
78
+ def provision(
79
+ server_name,
80
+ server_template,
81
+ cloud_name,
82
+ deployment_name,
83
+ server_inputs,
84
+ multi_cloud_image_name,
85
+ ssh_key_id = nil, #TODO: support me
86
+ secgroup_id = nil, #TODO: support me
87
+ datacenter = nil) #TODO: support me
88
+
89
+
90
+ # fail if the requested cloud is not registered with RightScale account
91
+ @cloud = @client.find_cloud_by_name(cloud_name)
92
+ unless @cloud
93
+ clouds = @client.list_clouds.inject("") { |str, c| str == "" ? c.name : "#{str}, #{c.name}" }
94
+ raise RightScaleError, "ERROR: cannot find a cloud named: '#{cloud_name}'. " +
95
+ "Please check the spelling of the 'cloud_name' parameter in " +
96
+ "your Vagrant file and verify the cloud is registered with " +
97
+ "your RightScale account? Supported clouds: #{clouds}"
98
+ end
99
+
100
+ # check for existing deployment and server in RightScale account
101
+ @deployment = @client.find_deployment_by_name(deployment_name)
102
+ @logger.info "Deployment '#{deployment_name}' #{@deployment ? "found." : "not found."}"
103
+ @server = @client.find_server_by_name(server_name) if @deployment
104
+ @logger.info "Server '#{server_name}' #{@server ? "found." : "not found."}"
105
+
106
+ # XXX: fails if the server is not running -- fix me!
107
+ # if @server
108
+ # # verify existing server is on the cloud we are requesting, if not fail.
109
+ # cloud_name ||= Config::VAGRANT_CLOUD_NAME
110
+ # actual_cloud_name = @client.server_cloud_name(@server)
111
+ # raise RightScaleError, "ERROR: the server is in the '#{actual_cloud_name}' cloud, " +
112
+ # "and not in the requested '#{cloud_name}' cloud.\n" +
113
+ # "Please delete the server or pick and new server name." if cloud_name != actual_cloud_name
114
+ # end
115
+
116
+ unless @deployment && @server
117
+ # we need to create a server, can we find the servertemplate?
118
+ begin
119
+ @servertemplate = @client.find_servertemplate(server_template)
120
+ rescue
121
+ raise RightScaleError, "ERROR: cannot find ServerTemplate '#{server_template}'. Did you import it?\n" +
122
+ "Visit http://bit.ly/VnOiA7 for more info.\n\n"
123
+ # can we find the MCI?
124
+ end
125
+ end
126
+
127
+ unless @deployment && @server
128
+ # We need to find the to be used in the server if the MCI name is given
129
+ begin
130
+ @mci =
131
+ if multi_cloud_image_name.nil? || multi_cloud_image_name.empty?
132
+ nil
133
+ else
134
+ @client.find_mci_by_name(multi_cloud_image_name)
135
+ end
136
+ rescue Exception => e
137
+ raise RightScaleError, "ERROR: Cannot find the mci '#{multi_cloud_image_name}'. Please make sure" +
138
+ " that you have the MCI under the server template selected." +
139
+ " Exception: #{e.inspect}"
140
+ end
141
+ end
142
+
143
+ # create deployment and server as needed
144
+ unless @deployment
145
+ @deployment = @client.create_deployment(deployment_name)
146
+ @logger.info "Created deployment."
147
+ end
148
+
149
+ unless @server
150
+ @server = @client.create_server(@deployment, @servertemplate, @mci, @cloud, server_name)
151
+ @logger.info "Created server."
152
+ end
153
+
154
+ unless @client.is_provisioned?(@server)
155
+
156
+ # setup any inputs
157
+ begin
158
+ @client.set_server_inputs(@server, server_inputs) if server_inputs && ! server_inputs.empty?
159
+ rescue Exception => e
160
+ raise RightScaleError, "Problem setting inputs. \n #{e.message}\n\n"
161
+ # can we find the MCI?
162
+ #TODO: @mci = @client.find_multicloudimage_by_name(@servertemplate, multi_cloud_image_name)
163
+ end
164
+
165
+ # launch server
166
+ @logger.info "Launching server..."
167
+ @server = @client.launch_server(@server, server_inputs)
168
+ @client.server_wait_for_state(@server, "booting")
169
+ end
170
+
171
+ end
172
+
173
+ # Register a custom progress indicator
174
+ #
175
+ # @example
176
+ # do
177
+ # @TODO: add an example
178
+ # end
179
+ # @param progress_block [Block] block to execute before each loop iteration.
180
+ # @yield [Logger] A Logger object will be passed as a parameter into
181
+ # your block.
182
+ def register_progress_indicator(&progress_block)
183
+ @progress_indicator = progress_block
184
+ end
185
+
186
+ end
187
+ end
188
+
189
+
190
+
191
+
192
+
193
+
@@ -0,0 +1,3 @@
1
+ module RightApiProvision
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ require "right_api_provision/version"
2
+ require "right_api_provision/api15"
3
+ require "right_api_provision/provisioner"
4
+
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'right_api_provision/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "right_api_provision"
8
+ gem.version = RightApiProvision::VERSION
9
+ gem.authors = ["caryp"]
10
+ gem.email = ["cary@rightscale.com"]
11
+ gem.description = %q{Simple ruby API for provisioning servers on the cloud with RightScale}
12
+ gem.summary = %q{Simple ruby API for provisioning servers on the cloud with RightScale}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.add_dependency "right_api_client"
20
+ %w(rspec-core rspec-expectations rspec-mocks rspec_junit_formatter rake).each { |rspec_gem| gem.add_development_dependency rspec_gem }
21
+ end