right_api_provision 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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