knife-rightscale 0.0.2

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.
@@ -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