knife-rightscale 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,372 @@
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 API15
21
+
22
+ attr_reader :client
23
+
24
+ def initialize
25
+ require "right_api_client"
26
+ end
27
+
28
+ def connection(email, password, account_id, api_url = nil)
29
+ begin
30
+ args = { :email => email, :password => password, :account_id => account_id }
31
+ @url = api_url
32
+ args[:api_url] = @url if @url
33
+ @connection ||= RightApi::Client.new(args)
34
+ #@logger = Logger.new(STDOUT)
35
+ #@logger.level = Logger::DEBUG
36
+ #@connection.log(@logger)
37
+ @client = @connection
38
+ rescue Exception => e
39
+ args.delete(:password) # don't log password
40
+ puts "ERROR: could not connect to RightScale API. Params: #{args.inspect}"
41
+ puts e.message
42
+ puts e.backtrace
43
+ raise e
44
+ end
45
+ end
46
+
47
+ # If the cloud reports ssh keys, then we assume it requires them to launch
48
+ # servers.
49
+ def requires_ssh_keys?(cloud)
50
+ begin
51
+ cloud.show.ssh_keys
52
+ true
53
+ rescue RightApi::Exceptions::ApiException => e
54
+ false # assume cloud does not require them
55
+ end
56
+ end
57
+
58
+ # Find SSH key
59
+ #
60
+ # EC2 and Eucalyptus require an SSH key to launch a server. RightScale
61
+ # manages SSH keys for each user so just grabbing the first one is fine,
62
+ # however older configurations might relay on specific keys. You will
63
+ # need to grab the resource UUID from the RightScale dashboard for the key
64
+ # that you want to use.
65
+ def find_ssh_key_by_uuid_or_first(cloud, ssh_uuid = nil)
66
+ ssh_key = nil
67
+ if ssh_uuid
68
+ # grab specific ssh key
69
+ sshkey = find_resource(:ssh_keys, :by_resource_uid, uuid)
70
+ else
71
+ # grab first key found
72
+ keys = cloud.show.ssh_keys
73
+ ssh_key = keys.index.first if keys
74
+ end
75
+ ssh_key
76
+ end
77
+
78
+ # If the cloud reports security groups then we assume it requires them to launch
79
+ # servers.
80
+ def requires_security_groups?(cloud)
81
+ begin
82
+ cloud.show.security_groups
83
+ true
84
+ rescue RightApi::Exceptions::ApiException => e
85
+ false # assume cloud does not require them
86
+ end
87
+ end
88
+
89
+ def user_data
90
+ @user_data ||= @server.show.current_instance(:view=>"extended").show.user_data
91
+ end
92
+
93
+ def data_request_url(userdata)
94
+ data_hash = {}
95
+ entry = userdata.split('&').select { |entry| entry =~ /RS_rn_auth/i }
96
+ raise "ERROR: user data token not found. " +
97
+ "Does your MCI have a provides:rs_agent_type=right_link tag?" unless entry
98
+ token = entry.first.split('=')[1]
99
+ "#{@url}/servers/data_injection_payload/#{token}"
100
+ end
101
+
102
+ def delete_server(name)
103
+ server = find_server_by_name(name)
104
+ server.terminate
105
+ begin
106
+ server_wait_for_state(server, "terminated")
107
+ rescue Exception => e
108
+
109
+ end
110
+ server.destroy
111
+ end
112
+
113
+ def list_servers(filter_by, filter_value)
114
+ list_resources(:servers, filter_by, filter_value)
115
+ end
116
+
117
+ def list_deployments(filter_by, filter_value)
118
+ list_resources(:deployments, filter_by, filter_value)
119
+ end
120
+
121
+ def list_clouds(filter_by, filter_value)
122
+ list_resources(:clouds, filter_by, filter_value)
123
+ end
124
+
125
+ def list_servertemplates(filter_by, filter_value)
126
+ list_resources(:server_templates, filter_by, filter_value)
127
+ end
128
+
129
+ def list_security_groups(cloud, filter_by, filter_value)
130
+ list_subresources(cloud, :security_groups, filter_by, filter_value)
131
+ end
132
+
133
+ def list_multi_cloud_images(server_template, filter_by, filter_value)
134
+ list_subresources(server_template, :multi_cloud_images, filter_by, filter_value)
135
+ end
136
+
137
+ def find_security_group_by_name(cloud, security_group_name)
138
+ find_cloud_resource(cloud, :security_groups, :by_name, security_group_name)
139
+ end
140
+
141
+ def find_server_by_name(name)
142
+ find_resource(:servers, :by_name, name)
143
+ end
144
+
145
+ def find_deployment_by_name(name)
146
+ find_resource(:deployments, :by_name, name)
147
+ end
148
+
149
+ # returns:: String if cloud is found, nil if not found
150
+ def find_cloud_by_name(name)
151
+ find_resource(:clouds, :by_name, name)
152
+ end
153
+
154
+ def find_mci_by_name(server_template, mci_name)
155
+ find_resource(:mcis, :by_name, name)
156
+ end
157
+
158
+ def find_servertemplate(name_or_id)
159
+ server_template = nil; id = nil; name = nil
160
+
161
+ # detect if user passed in a name or an id
162
+ # there is probably a cleaner way to do this, but I am lazy ATM.
163
+ begin
164
+ id = Integer(name_or_id)
165
+ rescue Exception => e
166
+ name = name_or_id # Cannot be case to integer, assume a name was passed
167
+ end
168
+
169
+ if name
170
+ # find ServerTemplate by name
171
+ st_list = list_resources(:server_templates, :by_name, name)
172
+ revisions = st_list.map { |st| st.revision }
173
+
174
+ # check for duplicate revisions
175
+ duplicates = (revisions.size != revisions.uniq.size)
176
+ raise "ERROR: Duplicate ServerTemplate with the name of '#{name}' detected " +
177
+ "in account -- there can be only one. Please fix via the RightScale dashboard and retry." if duplicates
178
+
179
+ # always use latest revision
180
+ latest_rev = revisions.sort.last
181
+ server_template = st_list.select { |st| st.revision == latest_rev}.first
182
+ else
183
+ # find ServerTemplate by id
184
+ server_template = @connection.server_templates.index(:id => id)
185
+ end
186
+
187
+ server_template
188
+ end
189
+
190
+ def create_deployment(name)
191
+ @connection.deployments.create(:deployment => { :name => name, :decription => "Created by the Vagrant"})
192
+ end
193
+
194
+ def destroy_deployment(deployment)
195
+ deployment.destroy
196
+ end
197
+
198
+ def create_server(deployment, server_template, mci, cloud, name, ssh_key = nil, groups = nil)
199
+
200
+ #TODO: mci param not used yet
201
+
202
+ # check params
203
+ unless st_href = server_template.show.href
204
+ raise "ERROR: ServerTemplate parameter not initialized properly"
205
+ end
206
+
207
+ unless d_href = deployment.show.href
208
+ raise "ERROR: Deployment parameter not initialized properly"
209
+ end
210
+
211
+ unless c_href = cloud.show.href
212
+ raise "ERROR: Deployment parameter not initialized properly"
213
+ end
214
+
215
+ if ssh_key
216
+ unless ssh_key_href = ssh_key.show.href
217
+ raise "ERROR: ssh_key parameter not initialized properly"
218
+ end
219
+ end
220
+
221
+ security_group_hrefs = nil
222
+ if groups
223
+ security_group_hrefs = []
224
+ groups.each do |group|
225
+ unless group_href = group.show.href
226
+ raise "ERROR: ssh_key parameter not initialized properly"
227
+ end
228
+ security_group_hrefs << group_href
229
+ end
230
+ end
231
+
232
+ instance_hash = {
233
+ :cloud_href => c_href,
234
+ :server_template_href => st_href
235
+ }
236
+ instance_hash[:ssh_key_href] = ssh_key_href if ssh_key
237
+ instance_hash[:security_group_hrefs] = security_group_hrefs if security_group_hrefs
238
+
239
+
240
+ # create server in deployment using specfied ST
241
+ server =
242
+ @connection.servers.create({
243
+ :server => {
244
+ :name => name,
245
+ :decription => "Created by the Vagrant",
246
+ :deployment_href => d_href,
247
+ :instance => instance_hash
248
+ }
249
+ })
250
+ end
251
+
252
+ def is_provisioned?(server)
253
+ server.show.api_methods.include?(:current_instance)
254
+ end
255
+
256
+ # @param(Hash) inputs Hash input name/value pairs i.e. { :name => "text:dummy"}
257
+ def launch_server(server, inputs = { :name => "text:dummy"})
258
+ server_name = server.show.name
259
+ server.launch(inputs) # TODO: parse inputs from Vagrantfile
260
+ # XXX: need to create a new server object after launch -- why? API bug?
261
+ find_server_by_name(server_name)
262
+ end
263
+
264
+ def terminate_server(server)
265
+ server.terminate
266
+ end
267
+
268
+ # Only use this *before* you launch the server
269
+ def set_server_inputs(server, inputs)
270
+ server.show.next_instance.show.inputs.multi_update({"inputs" => inputs})
271
+ end
272
+
273
+ def server_wait_for_state(server, target_state, delay = 10)
274
+ current_state = server_state(server)
275
+ while current_state != target_state
276
+ raise "Unexpected sever state: #{current_state}" if is_bad?(current_state)
277
+ puts "Server #{current_state}. Waiting for instance to be in #{target_state} state..."
278
+ sleep delay
279
+ current_state = server_state(server)
280
+ end
281
+ end
282
+
283
+ def set_bad_states(list_array)
284
+ @bad_states = list_array
285
+ end
286
+
287
+ def is_bad?(state)
288
+ @bad_states ||= []
289
+ @bad_states.select{|s| state =~ /#{s}/}.size > 0
290
+ end
291
+
292
+ def server_ready?(server)
293
+ server_state(server) == "operational"
294
+ end
295
+
296
+ def server_cloud_name(server)
297
+ instance = instance_from_server(server)
298
+ cloud = cloud_from_instance(instance)
299
+ cloud.show.name
300
+ end
301
+
302
+ def server_info(server)
303
+ server.show.current_instance.show(:view => 'extended')
304
+ end
305
+
306
+ private
307
+
308
+ def server_state(server)
309
+ instance_from_server(server).show.state
310
+ end
311
+
312
+ def instance_from_server(server)
313
+ server_data = server.show
314
+ if is_provisioned?(server)
315
+ begin
316
+ server_data.current_instance
317
+ rescue
318
+ server_data.next_instance
319
+ end
320
+ else
321
+ server_data.next_instance
322
+ end
323
+ end
324
+
325
+ def cloud_from_instance(instance)
326
+ instance.show.cloud
327
+ end
328
+
329
+ def find_resource(api_resource, filter_key, filter_value)
330
+ resource = nil
331
+ list = list_resources(api_resource, filter_key, filter_value)
332
+ raise "More than one #{api_resource} with the #{filter_key} of '#{filter_value}'. " +
333
+ "Please resolve via the RightScale dashboard and retry." if list.size > 1
334
+ resource = list.first unless list.empty?
335
+ resource
336
+ end
337
+
338
+ def list_resources(api_resource, filter_key, filter_value)
339
+ raise ArgumentError.new("api_resource must be a symbol") unless api_resource.kind_of?(Symbol)
340
+ key = filter_key.to_s.delete("by_") # convert :by_name to "name"
341
+ filter = {}
342
+ filter = {:filter => ["#{key}==#{filter_value}"]} if filter_value
343
+ list = @connection.send(api_resource).index(filter)
344
+ list
345
+ end
346
+
347
+ def index_resource(api_resource, index_key, index_value)
348
+ raise ArgumentError.new("api_resource must be a symbol") unless api_resource.kind_of?(Symbol)
349
+ arry = @connection.send(api_resource).index(index_key => index_value)
350
+ arry
351
+ end
352
+
353
+ def find_cloud_resource(cloud, api_resource, filter_key, filter_value)
354
+ resource = nil
355
+ list = list_subresources(cloud, api_resource, filter_key, filter_value)
356
+ raise "More than one #{api_resource} with the #{filter_key} of '#{filter_value}'. " +
357
+ "Please resolve via the RightScale dashboard and retry." if list.size > 1
358
+ resource = list.first unless list.empty?
359
+ resource
360
+ end
361
+
362
+ def list_subresources(api_resource, subresource, filter_key, filter_value)
363
+ raise ArgumentError.new("subresource must be a symbol") unless subresource.kind_of?(Symbol)
364
+ key = filter_key.to_s.delete("by_") # convert :by_name to "name"
365
+ filter = {}
366
+ filter = {:filter => ["#{key}==#{filter_value}"]} if filter_value
367
+ list = api_resource.show.send(subresource).index(filter)
368
+ list
369
+ end
370
+
371
+ end
372
+ end
@@ -0,0 +1,23 @@
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 RightApiProvisionException
21
+ # TODO
22
+ end
23
+ end
@@ -0,0 +1,139 @@
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 Provisioner
21
+
22
+ BAD_STATES_UP = [ "stranded", "terminated"]
23
+
24
+ def initialize(rightscale_api_object)
25
+ raise "ERROR: you must supply an valid RightScale API object" unless rightscale_api_object
26
+ @rsapi = rightscale_api_object
27
+ end
28
+
29
+ def provision(servertemplate,
30
+ server_name = "default",
31
+ cloud_name = "ec2",
32
+ deployment_name = "default",
33
+ inputs = nil,
34
+ ssh_key_uuid = nil,
35
+ security_groups = nil)
36
+
37
+ # fail if the requested cloud is not registered with RightScale account
38
+ @cloud = @rsapi.find_cloud_by_name(cloud_name)
39
+ raise "ERROR: cannot find a cloud named: '#{cloud_name}'. " +
40
+ "Please check the spelling of the 'cloud_name' parameter " +
41
+ "and verify the cloud is registered with " +
42
+ "your RightScale account?" unless @cloud
43
+
44
+ # Verify ssh key uuid, if required by cloud
45
+ if @rsapi.requires_ssh_keys?(@cloud)
46
+ @ssh_key = @rsapi.find_ssh_key_by_uuid_or_first(@cloud, ssh_key_uuid)
47
+ raise "ERROR: cannot find an ssh_key named: #{ssh_key_uuid}" unless @ssh_key
48
+ end
49
+
50
+ # Verify security group, if required by cloud
51
+ if @rsapi.requires_security_groups?(@cloud)
52
+ @sec_groups = []
53
+ security_groups ||= ["default"]
54
+ security_groups.each do |name|
55
+ group = @rsapi.find_security_group_by_name(@cloud, name)
56
+ raise "ERROR: cannot find an security group named: #{name}" unless group
57
+ @sec_groups << group
58
+ end
59
+ end
60
+
61
+ # check for existing deployment and server in RightScale account
62
+ @deployment = @rsapi.find_deployment_by_name(deployment_name)
63
+ puts "Deployment '#{deployment_name}' #{@deployment ? "found." : "not found."}"
64
+ @server = @rsapi.find_server_by_name(server_name) if @deployment
65
+ puts "Server '#{server_name}' #{@server ? "found." : "not found."}"
66
+
67
+ if @server
68
+ # verify existing server is on the cloud we are requesting, if not fail.
69
+ actual_cloud_name = @rsapi.server_cloud_name(@server)
70
+ raise "ERROR: the server is in the '#{actual_cloud_name}' cloud, " +
71
+ "and not in the requested '#{cloud_name}' cloud.\n" +
72
+ "Please delete the server or pick and new server name." if cloud_name != actual_cloud_name
73
+ end
74
+
75
+ unless @deployment && @server
76
+ # we need to create a server, can we find the servertemplate?
77
+ @servertemplate = @rsapi.find_servertemplate(servertemplate)
78
+ raise "ERROR: cannot find ServerTemplate '#{servertemplate}'. Did you import it?\n" +
79
+ "Visit http://bit.ly/VnOiA7 for more info.\n\n" unless @servertemplate
80
+ # can we find the MCI?
81
+ #TODO: @mci = @rsapi.find_multicloudimage_by_name(@servertemplate, config.multi_cloud_image_name)
82
+ end
83
+
84
+ # create deployment and server as needed
85
+ unless @deployment
86
+ @deployment = @rsapi.create_deployment(deployment_name)
87
+ puts "Created deployment."
88
+ end
89
+
90
+ unless @server
91
+ @server = @rsapi.create_server(@deployment, @servertemplate, @mci, @cloud, server_name, @ssh_key, @sec_groups)
92
+ puts "Created server."
93
+ end
94
+
95
+ unless @rsapi.is_provisioned?(@server)
96
+
97
+ # setup any inputs
98
+ @rsapi.set_server_inputs(@server, inputs) if inputs
99
+
100
+ # launch server
101
+ puts "Launching server..."
102
+ @server = @rsapi.launch_server(@server, inputs)
103
+ @rsapi.set_bad_states(BAD_STATES_UP)
104
+ @rsapi.server_wait_for_state(@server, "booting", 30)
105
+ end
106
+
107
+ # if cloud_name == VAGRANT_CLOUD_NAME
108
+ # # Vagrant box: grab "Data request URL" from UserData
109
+ # user_data = @server.current_instance.show(:view => "full").user_data
110
+ # puts user_data.inspect
111
+ # @data_request_url = @rsapi.data_request_url(user_data)
112
+ # puts "Data Request URL: #{@data_request_url}"
113
+ # else
114
+ # @rsapi.server_wait_for_state(server_name, "operational", 30)
115
+ # end
116
+
117
+ end
118
+
119
+ def server_ready?
120
+ @rsapi.server_ready?(@server)
121
+ end
122
+
123
+ def wait_for_operational
124
+ @rsapi.set_bad_states(BAD_STATES_UP)
125
+ @rsapi.server_wait_for_state(@server, "operational", 30)
126
+ end
127
+
128
+ def server_info
129
+ info = @rsapi.server_info(@server)
130
+ while info.private_ip_addresses.empty?
131
+ puts "Waiting for cloud to provide IP address..."
132
+ sleep 30
133
+ info = @rsapi.server_info(@server)
134
+ end
135
+ info
136
+ end
137
+
138
+ end
139
+ end