fog-softlayer 1.1.0 → 1.1.1

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.
@@ -1,628 +1,628 @@
1
- #
2
- # Author:: Matt Eldridge (<matt.eldridge@us.ibm.com>)
3
- # © Copyright IBM Corporation 2014.
4
- #
5
- # LICENSE: MIT (http://opensource.org/licenses/MIT)
6
- #
7
-
8
- require 'fog/compute/models/server'
9
-
10
- module Fog
11
- module Compute
12
- class Softlayer
13
-
14
- class Server < Fog::Compute::Server
15
-
16
- identity :id, :type => :integer
17
- attribute :name, :aliases => 'hostname'
18
- attribute :domain
19
- attribute :fqdn, :aliases => 'fullyQualifiedDomainName'
20
- attribute :cpu, :aliases => ['startCpus', 'processorCoreAmount']
21
- attribute :ram, :aliases => ['maxMemory', 'memory']
22
- attribute :disk, :aliases => ['blockDevices','hardDrives']
23
- attribute :private_ip_address, :aliases => 'primaryBackendIpAddress'
24
- attribute :public_ip_address, :aliases => 'primaryIpAddress'
25
- attribute :flavor_id
26
- attribute :bare_metal, :type => :boolean
27
- attribute :os_code
28
- attribute :image_id
29
- attribute :ephemeral_storage, :aliases => 'localDiskFlag'
30
- attribute :key_pairs, :aliases => 'sshKeys'
31
- attribute :network_components
32
- attribute :fixed_configuration_preset, :aliases => 'fixedConfigurationPreset'
33
-
34
- # Times
35
- attribute :created_at, :aliases => ['createDate', 'provisionDate'], :type => :time
36
- attribute :last_verified_date, :aliases => 'lastVerifiedDate', :type => :time
37
- attribute :metric_poll_date, :aliases => 'metricPollDate', :type => :time
38
- attribute :modify_date, :aliases => 'modifyDate', :type => :time
39
-
40
- # Metadata
41
- attribute :account_id, :aliases => 'accountId', :type => :integer
42
- attribute :datacenter, :aliases => 'datacenter'
43
- attribute :single_tenant, :aliases => 'dedicatedAccountHostOnlyFlag'
44
- attribute :global_identifier, :aliases => 'globalIdentifier'
45
- attribute :hourly_billing_flag, :aliases => 'hourlyBillingFlag'
46
- attribute :tags, :aliases => 'tagReferences'
47
- attribute :private_network_only, :aliases => 'privateNetworkOnlyFlag'
48
- attribute :user_data, :aliases => 'userData'
49
- attribute :uid, :aliases => 'globalIdentifier'
50
- attribute :provision_script, :aliases => 'postInstallScriptUri'
51
-
52
- def initialize(attributes = {})
53
- # Forces every request inject bare_metal parameter
54
- raise Exception if attributes[:collection].nil? and attributes['bare_metal'].nil?
55
- super(attributes)
56
- set_defaults
57
- end
58
-
59
- def add_tags(tags)
60
- requires :id
61
- raise ArgumentError, "Tags argument for #{self.class.name}##{__method__} must be Array." unless tags.is_a?(Array)
62
- tags.each do |tag|
63
- service.tags.new(:resource_id => self.id, :name => tag).save
64
- end
65
- self.reload
66
- true
67
- end
68
-
69
- def bare_metal?
70
- bare_metal
71
- end
72
-
73
- def bare_metal
74
- @bare_metal
75
- end
76
-
77
- def datacenter=(name)
78
- name = name['name'] if name.is_a?(Hash)
79
- attributes[:datacenter] = { :name => name }
80
- end
81
-
82
- def datacenter
83
- attributes[:datacenter][:name] unless attributes[:datacenter].nil?
84
- end
85
-
86
- def delete_tags(tags)
87
- requires :id
88
- raise ArgumentError, "Tags argument for #{self.class.name}##{__method__} must be Array." unless tags.is_a?(Array)
89
- tags.each do |tag|
90
- service.tags.new(:resource_id => self.id, :name => tag).destroy
91
- end
92
- self.reload
93
- true
94
- end
95
-
96
- def destroy
97
- requires :id
98
- request = bare_metal? ? :delete_bare_metal_server : :delete_vm
99
- response = service.send(request, self.id)
100
- response.body
101
- end
102
-
103
- def dns_name
104
- fqdn
105
- end
106
-
107
- def image_id=(uuid)
108
- attributes[:image_id] = {:globalIdentifier => uuid}
109
- end
110
-
111
- def image_id
112
- attributes[:image_id][:globalIdentifier] unless attributes[:image_id].nil?
113
- end
114
-
115
- def name=(set)
116
- attributes[:hostname] = set
117
- end
118
-
119
- def name
120
- attributes[:hostname]
121
- end
122
-
123
- def pre_save
124
- extract_flavor
125
- self.bare_metal = true if attributes[:fixed_configuration_preset] and not bare_metal?
126
- validate_attributes
127
- if self.vlan
128
- attributes[:vlan] = { :networkVlan => { :id => self.vlan.id } }
129
- end
130
- if self.private_vlan
131
- attributes[:private_vlan] = { :networkVlan => { :id => self.private_vlan.id } }
132
- end
133
- if self.key_pairs
134
- attributes[:key_pairs].map! { |key| { :id => key.id } }
135
- end
136
- if self.network_components
137
- self.network_components = self.network_components.map do |component|
138
- component[:maxSpeed] = component.delete(:speed) if component[:speed]
139
- component[:maxSpeed] = component.delete(:max_speed) if component[:max_speed]
140
- component
141
- end
142
- end
143
-
144
- if attributes[:fixed_configuration_preset].is_a? String
145
- attributes[:fixedConfigurationPreset] = {:keyName => attributes.delete(:fixed_configuration_preset)}
146
- end
147
-
148
- remap_attributes(attributes, attributes_mapping)
149
- clean_attributes
150
- end
151
-
152
- def private_ip # maintain backward compatibility with <0.3.13
153
- private_ip_address
154
- end
155
-
156
- def public_ip # maintain backward compatibility with <0.3.13
157
- public_ip_address
158
- end
159
-
160
- def os_code
161
- attributes['operatingSystem']['softwareLicense']['softwareDescription']['referenceCode'] if attributes['operatingSystem']
162
- end
163
-
164
- def private_vlan
165
- attributes[:private_vlan] ||= _get_private_vlan
166
- end
167
-
168
- def private_vlan=(value)
169
- unless value.is_a?(Integer) or value.is_a?(Fog::Network::Softlayer::Network)
170
- raise ArgumentError, "vlan argument for #{self.class.name}##{__method__} must be Integer or Fog::Network::Softlayer::Network."
171
- end
172
- value = network_connection.networks.get(value) if value.is_a?(Integer)
173
- attributes[:private_vlan] = value
174
- end
175
-
176
- # reload the OS on a server (method name reload was already taken)
177
- def relaunch!
178
- requires :id
179
- body = [ "FORCE", {}]
180
- body[1][:sshKeyIds] = key_pairs.map {|kp| kp.id} unless key_pairs.empty?
181
- type = bare_metal? ? :hardware_server : :virtual_guest
182
- status = service.request(type, "#{id}/reloadOperatingSystem", :body => body, :http_method => :post).status
183
- wait_for { not ready? } # block until the relaunch has begun
184
- [200, 201].include?(status)
185
- end
186
-
187
- def key_pairs
188
- attributes[:key_pairs]
189
- end
190
-
191
- def key_pairs=(keys)
192
- raise ArgumentError, "Argument #{local_variables.first.to_s} for #{self.class.name}##{__method__} must be Array." unless keys.is_a?(Array)
193
- attributes[:key_pairs] = []
194
- keys.map do |key|
195
- ## This was nice but causing an intolerable number of requests on an account with lots of keys.
196
- ## ToDo: something better...
197
- #key = self.symbolize_keys(key) if key.is_a?(Hash)
198
- #unless key.is_a?(Fog::Compute::Softlayer::KeyPair) or (key.is_a?(Hash) and key[:id])
199
- # raise ArgumentError, "Elements of keys array for #{self.class.name}##{__method__} must be a Hash with key 'id', or Fog::Compute::Softlayer::KeyPair"
200
- #end
201
- #key = service.key_pairs.get(key[:id]) unless key.is_a?(Fog::Compute::Softlayer::KeyPair)
202
- attributes[:key_pairs] << key
203
- end
204
- end
205
-
206
- def vlan
207
- attributes[:vlan] ||= _get_vlan
208
- end
209
-
210
- def vlan=(value)
211
- unless value.is_a?(Integer) or value.is_a?(Fog::Network::Softlayer::Network)
212
- raise ArgumentError, "vlan argument for #{self.class.name}##{__method__} must be Integer or Fog::Network::Softlayer::Network."
213
- end
214
- value = network_connection.networks.get(value) if value.is_a?(Integer)
215
- attributes[:vlan] = value
216
- end
217
-
218
- def ram=(set)
219
- if set.is_a?(Array) and set.first['hardwareComponentModel']
220
- set = 1024 * set.first['hardwareComponentModel']['capacity'].to_i
221
- end
222
- attributes[:ram] = set
223
- end
224
-
225
- # @params value [String]
226
- def user_data=(value)
227
- attributes[:user_data] = [{'value' => value}]
228
- end
229
-
230
- def user_data
231
- attributes[:user_data]
232
- end
233
-
234
- def provision_script=(value)
235
- attributes[:provision_script] = value
236
- end
237
-
238
- def provision_script
239
- attributes[:provision_script]
240
- end
241
-
242
- def network_components
243
- if id
244
- (public_network_components << private_network_components).flatten
245
- else
246
- attributes[:network_components]
247
- end
248
- end
249
-
250
- def public_network_components
251
- if attributes['frontendNetworkComponents']
252
- attributes['frontendNetworkComponents'].map { |n| Fog::Compute::Softlayer::NetworkComponent.new(n) }
253
- else
254
- []
255
- end
256
- end
257
-
258
- def private_network_components
259
- if attributes['backendNetworkComponents']
260
- attributes['backendNetworkComponents'].map { |n| Fog::Compute::Softlayer::NetworkComponent.new(n) }
261
- else
262
- []
263
- end
264
- end
265
-
266
- def ready?
267
- begin
268
- if bare_metal?
269
- state == "on"
270
- else
271
- state == "Running"
272
- end
273
- rescue Excon::Errors::InternalServerError => e
274
- false
275
- end
276
- end
277
-
278
- def reboot(use_hard_reboot = true)
279
- # requires :id # TODO: debug why this breaks the tests on bare metal and uncomment this
280
- if bare_metal?
281
- service.reboot_bare_metal_server(id, use_hard_reboot)
282
- else
283
- service.reboot_vm(id, use_hard_reboot)
284
- end
285
- true
286
- end
287
-
288
- def ssh_password
289
- requires :id
290
- service_path = bare_metal? ? :hardware_server : :virtual_guest
291
- @sshpass ||= service.request(service_path, id, :query => 'objectMask=mask[id,operatingSystem.passwords[password]]').body
292
- @sshpass['operatingSystem']['passwords'][0]['password'] unless @sshpass['operatingSystem'].nil? or @sshpass['operatingSystem']['passwords'].empty?
293
- end
294
-
295
- def snapshot
296
- # TODO: implement
297
- end
298
-
299
- def start
300
- # requires :id # TODO: debug why this breaks the tests on bare metal and uncomment this
301
- if bare_metal?
302
- service.power_on_bare_metal_server(id)
303
- else
304
- service.power_on_vm(id)
305
- end
306
- true
307
- end
308
-
309
- # Hard power off
310
- def stop
311
- # requires :id # TODO: debug why this breaks the tests on bare metal and uncomment this
312
- if bare_metal?
313
- service.power_off_bare_metal_server(id)
314
- else
315
- service.power_off_vm(id, true)
316
- end
317
- true
318
- end
319
-
320
- # Soft power off
321
- def shutdown
322
- # requires :id # TODO: debug why this breaks the tests on bare metal and uncomment this
323
- if bare_metal?
324
- raise Fog::Errors::Error.new('Shutdown not supported on baremetal servers. Use #stop.')
325
- else
326
- service.power_off_vm(id, false)
327
- end
328
- true
329
- end
330
-
331
- def state
332
- if bare_metal?
333
- service.request(:hardware_server, "#{id}/getServerPowerState").body
334
- else
335
- service.request(:virtual_guest, "#{id}/getPowerState").body['name']
336
- end
337
- end
338
-
339
- # Creates server
340
- # * requires attributes: :name, :domain, and :flavor_id OR (:cpu_count && :ram && :disks)
341
- #
342
- # @note You should use servers.create to create servers instead calling this method directly
343
- #
344
- # * State Transitions
345
- # * BUILD -> ACTIVE
346
- # * BUILD -> ERROR (on error)
347
- def save
348
- raise Fog::Errors::Error.new('Resaving an existing object may create a duplicate') if persisted?
349
- copy = self.dup
350
- copy.pre_save
351
-
352
- data = if copy.bare_metal?
353
- service.create_bare_metal_server(copy.attributes).body
354
- else
355
- service.create_vm(copy.attributes).body.first
356
- end
357
-
358
- data.delete("bare_metal")
359
- merge_attributes(data)
360
- true
361
- end
362
-
363
- def tags
364
- attributes[:tags].map { |i| i['tag']['name'] if i['tag'] }.compact if attributes[:tags]
365
- end
366
-
367
- def get_active_tickets
368
- return service.get_bare_metal_active_tickets(id).body if bare_metal?
369
- service.get_virtual_guest_active_tickets(id).body
370
- end
371
-
372
- def get_users
373
- return service.get_bare_metal_users(id).body if bare_metal?
374
- service.get_virtual_guest_users(id).body
375
- end
376
-
377
- def get_upgrade_options
378
- return service.get_bare_metal_upgrade_item_prices(id).body if bare_metal?
379
- service.get_virtual_guest_upgrade_item_prices(id).body
380
- end
381
-
382
- def update(update_attributes)
383
- raise ArgumentError if update_attributes.nil?
384
- product_connection
385
- prices = get_item_prices_id(update_attributes)
386
- order = generate_upgrade_order(prices, update_attributes[:time] || update_attributes[:maintenance_window])
387
- @product_conn.place_order(order).body
388
- end
389
-
390
- def generate_order_template
391
- copy = self.dup
392
- copy.pre_save
393
- return service.generate_bare_metal_order_template(copy.attributes).body if copy.bare_metal?
394
- service.generate_virtual_guest_order_template(copy.attributes).body
395
- end
396
-
397
- def wait_for_id(timeout=14400, delay=30)
398
- # Cannot use self.wait_for because it calls reload which requires
399
- # self.id which is not initially available for bare metal.
400
- filterStr = Fog::JSON.encode({
401
- "hardware" => {
402
- "hostname" => {
403
- "operation" => self.name,
404
- },
405
- "domain" => {
406
- "operation" => self.domain,
407
- },
408
- "globalIdentifier" => {
409
- "operation" => self.uid,
410
- },
411
- }
412
- })
413
-
414
- Fog.wait_for(timeout, delay) do
415
- res = service.request(:account, 'getHardware', :query => {
416
- :objectMask => 'mask[id,fullyQualifiedDomainName,provisionDate,hardwareStatus,lastTransaction[elapsedSeconds,transactionStatus[friendlyName]],operatingSystem[id,passwords[password,username]]]',
417
- :objectFilter => filterStr,
418
- })
419
-
420
- server = res.body.first
421
-
422
- yield server if block_given?
423
-
424
- if server and server["provisionDate"]
425
- attributes[:id] = server['id']
426
- true
427
- else
428
- false
429
- end
430
- end
431
-
432
- self.reload
433
- true
434
- end
435
-
436
- private
437
-
438
- def network_connection
439
- @network_conn ||= Fog::Network.new(
440
- :provider => :softlayer,
441
- :softlayer_username => service.instance_variable_get(:@softlayer_username),
442
- :softlayer_api_key => service.instance_variable_get(:@softlayer_api_key)
443
- )
444
- end
445
-
446
- def product_connection
447
- if Fog.mock?
448
- @product_conn = Fog::Softlayer::Product.new(
449
- :provider => :softlayer,
450
- :softlayer_username => service.instance_variable_get(:@credentials)[:username],
451
- :softlayer_api_key => service.instance_variable_get(:@credentials)[:api_key]
452
- )
453
- end
454
- @product_conn ||= Fog::Softlayer::Product.new(
455
- :provider => :softlayer,
456
- :softlayer_username => service.instance_variable_get(:@softlayer_username),
457
- :softlayer_api_key => service.instance_variable_get(:@softlayer_api_key)
458
- )
459
- end
460
-
461
- def _get_private_vlan
462
- if self.id
463
- vlan_id = if bare_metal?
464
- service.request(:hardware_server, "#{self.id}/get_private_vlan").body['id']
465
- else
466
- service.request(:virtual_guest, self.id, :query => 'objectMask=primaryBackendNetworkComponent.networkVlan').body['primaryBackendNetworkComponent']['networkVlan']['id']
467
- end
468
- network_connection.networks.get(vlan_id)
469
- end
470
- end
471
-
472
- def _get_vlan
473
- if self.id
474
- vlan_id = if bare_metal?
475
- service.request(:hardware_server, "#{self.id}/get_public_vlan").body['id']
476
- else
477
- service.request(:virtual_guest, self.id, :query => 'objectMask=primaryNetworkComponent.networkVlan').body['primaryNetworkComponent']['networkVlan']['id']
478
- end
479
- network_connection.networks.get(vlan_id)
480
- end
481
- end
482
-
483
- ##
484
- # Generate mapping for use with remap_attributes
485
- def attributes_mapping
486
- common = {
487
- :hourly_billing_flag => :hourlyBillingFlag,
488
- :os_code => :operatingSystemReferenceCode,
489
- :vlan => :primaryNetworkComponent,
490
- :private_vlan => :primaryBackendNetworkComponent,
491
- :key_pairs => :sshKeys,
492
- :private_network_only => :privateNetworkOnlyFlag,
493
- :user_data => :userData,
494
- :provision_script => :postInstallScriptUri,
495
- :network_components => :networkComponents,
496
- }
497
-
498
- conditional = if bare_metal?
499
- {
500
- :cpu => :processorCoreAmount,
501
- :ram => :memoryCapacity,
502
- :disk => :hardDrives,
503
- :bare_metal => :bareMetalInstanceFlag,
504
- :fixed_configuration_preset => :fixedConfigurationPreset,
505
- }
506
- else
507
- {
508
- :cpu => :startCpus,
509
- :ram => :maxMemory,
510
- :disk => :blockDevices,
511
- :image_id => :blockDeviceTemplateGroup,
512
- :ephemeral_storage => :localDiskFlag,
513
- }
514
- end
515
- common.merge(conditional)
516
- end
517
-
518
- def bare_metal=(set)
519
- return @bare_metal if set == @bare_metal
520
- raise Exception, "Bare metal flag has already been set" unless @bare_metal.nil?
521
- @bare_metal = case set
522
- when false, 'false', 0, nil, ''
523
- attributes[:bare_metal] = false
524
- else
525
- attributes[:bare_metal] = true
526
- end
527
- end
528
-
529
- ##
530
- # Remove model attributes that aren't expected by the SoftLayer API
531
- def clean_attributes
532
- attributes.delete(:bare_metal)
533
- attributes.delete(:flavor_id)
534
- attributes.delete(:ephemeral_storage)
535
- attributes.delete(:tags) if bare_metal?
536
- end
537
-
538
- ##
539
- # Expand a "flavor" into cpu, ram, and disk attributes
540
- def extract_flavor
541
- if attributes[:flavor_id]
542
- flavor = @service.flavors.get(attributes[:flavor_id])
543
- flavor.nil? and Fog::Errors::Error.new("Unrecognized flavor in #{self.class}##{__method__}")
544
- attributes[:cpu] = flavor.cpu
545
- attributes[:ram] = flavor.ram
546
- attributes[:disk] = flavor.disk unless attributes[:image_id]
547
- if bare_metal?
548
- value = flavor.disk.first['diskImage']['capacity'] < 500 ? 250 : 500
549
- attributes[:disk] = [{'capacity'=>value}]
550
- attributes[:ram] = attributes[:ram] / 1024 if attributes[:ram] > 64
551
- end
552
- end
553
- end
554
-
555
- def validate_attributes
556
- requires :name, :domain, :datacenter
557
- if attributes[:fixed_configuration_preset]
558
- requires :os_code
559
- else
560
- requires :cpu, :ram
561
- requires_one :os_code, :image_id
562
- requires_one :disk, :image_id
563
- end
564
- bare_metal? and image_id and raise ArgumentError, "Bare Metal Cloud does not support booting from Image"
565
- end
566
-
567
- def set_defaults
568
- attributes[:hourly_billing_flag] = true if attributes[:hourly_billing_flag].nil?
569
- attributes[:ephemeral_storage] = false if attributes[:ephemeral_storage].nil?
570
- attributes[:domain] = service.softlayer_default_domain if service.softlayer_default_domain and attributes[:domain].nil?
571
- self.datacenter = service.softlayer_default_datacenter if service.softlayer_default_datacenter and attributes[:datacenter].nil?
572
- end
573
-
574
- def get_item_prices_id_by_value(item_price_array, category, value)
575
- item_prices = item_price_array.select { |item_price| item_price["categories"].find { |category_hash| category_hash["categoryCode"] == category } }
576
- item_price = item_prices.find { |item_price| item_price['item']['capacity'] == value.to_s }
577
- item_price.nil? ? "" : item_price["id"]
578
- end
579
-
580
- def get_item_prices_id(update_attributes)
581
- item_price_array = get_upgrade_options
582
- update_attributes.delete(:time)
583
- update_attributes.delete(:maintenance_window)
584
- update_attributes.map { |key, value| { :id => get_item_prices_id_by_value(item_price_array, key.to_s, value) } }
585
- end
586
-
587
- def bm_upgrade_order_template(value)
588
- {
589
- :complexType => 'SoftLayer_Container_Product_Order_Hardware_Server_Upgrade',
590
- :hardware => [
591
- {
592
- :id => id
593
- }
594
- ],
595
- :properties => [
596
- {
597
- :name => 'MAINTENANCE_WINDOW_ID',
598
- :value => value
599
- }
600
- ]
601
- }
602
- end
603
-
604
- def vm_upgrade_order_template(time)
605
- {
606
- :complexType => 'SoftLayer_Container_Product_Order_Virtual_Guest_Upgrade',
607
- :virtualGuests => [
608
- {
609
- :id => id
610
- }
611
- ],
612
- :properties => [
613
- {
614
- :name => 'MAINTENANCE_WINDOW',
615
- :value => (time.nil? || time.empty?) ? Time.now.iso8601 : time.iso8601
616
- }
617
- ]
618
- }
619
- end
620
-
621
- def generate_upgrade_order(prices, value)
622
- return bm_upgrade_order_template(value).merge({ :prices => prices }) if bare_metal?
623
- vm_upgrade_order_template(value).merge({ :prices => prices })
624
- end
625
- end
626
- end
627
- end
628
- end
1
+ #
2
+ # Author:: Matt Eldridge (<matt.eldridge@us.ibm.com>)
3
+ # © Copyright IBM Corporation 2014.
4
+ #
5
+ # LICENSE: MIT (http://opensource.org/licenses/MIT)
6
+ #
7
+
8
+ require 'fog/compute/models/server'
9
+
10
+ module Fog
11
+ module Compute
12
+ class Softlayer
13
+
14
+ class Server < Fog::Compute::Server
15
+
16
+ identity :id, :type => :integer
17
+ attribute :name, :aliases => 'hostname'
18
+ attribute :domain
19
+ attribute :fqdn, :aliases => 'fullyQualifiedDomainName'
20
+ attribute :cpu, :aliases => ['startCpus', 'processorCoreAmount']
21
+ attribute :ram, :aliases => ['maxMemory', 'memory']
22
+ attribute :disk, :aliases => ['blockDevices','hardDrives']
23
+ attribute :private_ip_address, :aliases => 'primaryBackendIpAddress'
24
+ attribute :public_ip_address, :aliases => 'primaryIpAddress'
25
+ attribute :flavor_id
26
+ attribute :bare_metal, :type => :boolean
27
+ attribute :os_code
28
+ attribute :image_id
29
+ attribute :ephemeral_storage, :aliases => 'localDiskFlag'
30
+ attribute :key_pairs, :aliases => 'sshKeys'
31
+ attribute :network_components
32
+ attribute :fixed_configuration_preset, :aliases => 'fixedConfigurationPreset'
33
+
34
+ # Times
35
+ attribute :created_at, :aliases => ['createDate', 'provisionDate'], :type => :time
36
+ attribute :last_verified_date, :aliases => 'lastVerifiedDate', :type => :time
37
+ attribute :metric_poll_date, :aliases => 'metricPollDate', :type => :time
38
+ attribute :modify_date, :aliases => 'modifyDate', :type => :time
39
+
40
+ # Metadata
41
+ attribute :account_id, :aliases => 'accountId', :type => :integer
42
+ attribute :datacenter, :aliases => 'datacenter'
43
+ attribute :single_tenant, :aliases => 'dedicatedAccountHostOnlyFlag'
44
+ attribute :global_identifier, :aliases => 'globalIdentifier'
45
+ attribute :hourly_billing_flag, :aliases => 'hourlyBillingFlag'
46
+ attribute :tags, :aliases => 'tagReferences'
47
+ attribute :private_network_only, :aliases => 'privateNetworkOnlyFlag'
48
+ attribute :user_data, :aliases => 'userData'
49
+ attribute :uid, :aliases => 'globalIdentifier'
50
+ attribute :provision_script, :aliases => 'postInstallScriptUri'
51
+
52
+ def initialize(attributes = {})
53
+ # Forces every request inject bare_metal parameter
54
+ raise Exception if attributes[:collection].nil? and attributes['bare_metal'].nil?
55
+ super(attributes)
56
+ set_defaults
57
+ end
58
+
59
+ def add_tags(tags)
60
+ requires :id
61
+ raise ArgumentError, "Tags argument for #{self.class.name}##{__method__} must be Array." unless tags.is_a?(Array)
62
+ tags.each do |tag|
63
+ service.tags.new(:resource_id => self.id, :name => tag).save
64
+ end
65
+ self.reload
66
+ true
67
+ end
68
+
69
+ def bare_metal?
70
+ bare_metal
71
+ end
72
+
73
+ def bare_metal
74
+ @bare_metal
75
+ end
76
+
77
+ def datacenter=(name)
78
+ name = name['name'] if name.is_a?(Hash)
79
+ attributes[:datacenter] = { :name => name }
80
+ end
81
+
82
+ def datacenter
83
+ attributes[:datacenter][:name] unless attributes[:datacenter].nil?
84
+ end
85
+
86
+ def delete_tags(tags)
87
+ requires :id
88
+ raise ArgumentError, "Tags argument for #{self.class.name}##{__method__} must be Array." unless tags.is_a?(Array)
89
+ tags.each do |tag|
90
+ service.tags.new(:resource_id => self.id, :name => tag).destroy
91
+ end
92
+ self.reload
93
+ true
94
+ end
95
+
96
+ def destroy
97
+ requires :id
98
+ request = bare_metal? ? :delete_bare_metal_server : :delete_vm
99
+ response = service.send(request, self.id)
100
+ response.body
101
+ end
102
+
103
+ def dns_name
104
+ fqdn
105
+ end
106
+
107
+ def image_id=(uuid)
108
+ attributes[:image_id] = {:globalIdentifier => uuid}
109
+ end
110
+
111
+ def image_id
112
+ attributes[:image_id][:globalIdentifier] unless attributes[:image_id].nil?
113
+ end
114
+
115
+ def name=(set)
116
+ attributes[:hostname] = set
117
+ end
118
+
119
+ def name
120
+ attributes[:hostname]
121
+ end
122
+
123
+ def pre_save
124
+ extract_flavor
125
+ self.bare_metal = true if attributes[:fixed_configuration_preset] and not bare_metal?
126
+ validate_attributes
127
+ if self.vlan
128
+ attributes[:vlan] = { :networkVlan => { :id => self.vlan.id } }
129
+ end
130
+ if self.private_vlan
131
+ attributes[:private_vlan] = { :networkVlan => { :id => self.private_vlan.id } }
132
+ end
133
+ if self.key_pairs
134
+ attributes[:key_pairs].map! { |key| { :id => key.id } }
135
+ end
136
+ if self.network_components
137
+ self.network_components = self.network_components.map do |component|
138
+ component['maxSpeed'] = component.delete('speed') if component['speed']
139
+ component['maxSpeed'] = component.delete('max_speed') if component['max_speed']
140
+ component
141
+ end
142
+ end
143
+
144
+ if attributes[:fixed_configuration_preset].is_a? String
145
+ attributes[:fixedConfigurationPreset] = {:keyName => attributes.delete(:fixed_configuration_preset)}
146
+ end
147
+
148
+ remap_attributes(attributes, attributes_mapping)
149
+ clean_attributes
150
+ end
151
+
152
+ def private_ip # maintain backward compatibility with <0.3.13
153
+ private_ip_address
154
+ end
155
+
156
+ def public_ip # maintain backward compatibility with <0.3.13
157
+ public_ip_address
158
+ end
159
+
160
+ def os_code
161
+ attributes['operatingSystem']['softwareLicense']['softwareDescription']['referenceCode'] if attributes['operatingSystem']
162
+ end
163
+
164
+ def private_vlan
165
+ attributes[:private_vlan] ||= _get_private_vlan
166
+ end
167
+
168
+ def private_vlan=(value)
169
+ unless value.is_a?(Integer) or value.is_a?(Fog::Network::Softlayer::Network)
170
+ raise ArgumentError, "vlan argument for #{self.class.name}##{__method__} must be Integer or Fog::Network::Softlayer::Network."
171
+ end
172
+ value = network_connection.networks.get(value) if value.is_a?(Integer)
173
+ attributes[:private_vlan] = value
174
+ end
175
+
176
+ # reload the OS on a server (method name reload was already taken)
177
+ def relaunch!
178
+ requires :id
179
+ body = [ "FORCE", {}]
180
+ body[1][:sshKeyIds] = key_pairs.map {|kp| kp.id} unless key_pairs.empty?
181
+ type = bare_metal? ? :hardware_server : :virtual_guest
182
+ status = service.request(type, "#{id}/reloadOperatingSystem", :body => body, :http_method => :post).status
183
+ wait_for { not ready? } # block until the relaunch has begun
184
+ [200, 201].include?(status)
185
+ end
186
+
187
+ def key_pairs
188
+ attributes[:key_pairs]
189
+ end
190
+
191
+ def key_pairs=(keys)
192
+ raise ArgumentError, "Argument #{local_variables.first.to_s} for #{self.class.name}##{__method__} must be Array." unless keys.is_a?(Array)
193
+ attributes[:key_pairs] = []
194
+ keys.map do |key|
195
+ ## This was nice but causing an intolerable number of requests on an account with lots of keys.
196
+ ## ToDo: something better...
197
+ #key = self.symbolize_keys(key) if key.is_a?(Hash)
198
+ #unless key.is_a?(Fog::Compute::Softlayer::KeyPair) or (key.is_a?(Hash) and key[:id])
199
+ # raise ArgumentError, "Elements of keys array for #{self.class.name}##{__method__} must be a Hash with key 'id', or Fog::Compute::Softlayer::KeyPair"
200
+ #end
201
+ #key = service.key_pairs.get(key[:id]) unless key.is_a?(Fog::Compute::Softlayer::KeyPair)
202
+ attributes[:key_pairs] << key
203
+ end
204
+ end
205
+
206
+ def vlan
207
+ attributes[:vlan] ||= _get_vlan
208
+ end
209
+
210
+ def vlan=(value)
211
+ unless value.is_a?(Integer) or value.is_a?(Fog::Network::Softlayer::Network)
212
+ raise ArgumentError, "vlan argument for #{self.class.name}##{__method__} must be Integer or Fog::Network::Softlayer::Network."
213
+ end
214
+ value = network_connection.networks.get(value) if value.is_a?(Integer)
215
+ attributes[:vlan] = value
216
+ end
217
+
218
+ def ram=(set)
219
+ if set.is_a?(Array) and set.first['hardwareComponentModel']
220
+ set = 1024 * set.first['hardwareComponentModel']['capacity'].to_i
221
+ end
222
+ attributes[:ram] = set
223
+ end
224
+
225
+ # @params value [String]
226
+ def user_data=(value)
227
+ attributes[:user_data] = [{'value' => value}]
228
+ end
229
+
230
+ def user_data
231
+ attributes[:user_data]
232
+ end
233
+
234
+ def provision_script=(value)
235
+ attributes[:provision_script] = value
236
+ end
237
+
238
+ def provision_script
239
+ attributes[:provision_script]
240
+ end
241
+
242
+ def network_components
243
+ if id
244
+ (public_network_components << private_network_components).flatten
245
+ else
246
+ attributes[:network_components]
247
+ end
248
+ end
249
+
250
+ def public_network_components
251
+ if attributes['frontendNetworkComponents']
252
+ attributes['frontendNetworkComponents'].map { |n| Fog::Compute::Softlayer::NetworkComponent.new(n) }
253
+ else
254
+ []
255
+ end
256
+ end
257
+
258
+ def private_network_components
259
+ if attributes['backendNetworkComponents']
260
+ attributes['backendNetworkComponents'].map { |n| Fog::Compute::Softlayer::NetworkComponent.new(n) }
261
+ else
262
+ []
263
+ end
264
+ end
265
+
266
+ def ready?
267
+ begin
268
+ if bare_metal?
269
+ state == "on"
270
+ else
271
+ state == "Running"
272
+ end
273
+ rescue Excon::Errors::InternalServerError => e
274
+ false
275
+ end
276
+ end
277
+
278
+ def reboot(use_hard_reboot = true)
279
+ # requires :id # TODO: debug why this breaks the tests on bare metal and uncomment this
280
+ if bare_metal?
281
+ service.reboot_bare_metal_server(id, use_hard_reboot)
282
+ else
283
+ service.reboot_vm(id, use_hard_reboot)
284
+ end
285
+ true
286
+ end
287
+
288
+ def ssh_password
289
+ requires :id
290
+ service_path = bare_metal? ? :hardware_server : :virtual_guest
291
+ @sshpass ||= service.request(service_path, id, :query => 'objectMask=mask[id,operatingSystem.passwords[password]]').body
292
+ @sshpass['operatingSystem']['passwords'][0]['password'] unless @sshpass['operatingSystem'].nil? or @sshpass['operatingSystem']['passwords'].empty?
293
+ end
294
+
295
+ def snapshot
296
+ # TODO: implement
297
+ end
298
+
299
+ def start
300
+ # requires :id # TODO: debug why this breaks the tests on bare metal and uncomment this
301
+ if bare_metal?
302
+ service.power_on_bare_metal_server(id)
303
+ else
304
+ service.power_on_vm(id)
305
+ end
306
+ true
307
+ end
308
+
309
+ # Hard power off
310
+ def stop
311
+ # requires :id # TODO: debug why this breaks the tests on bare metal and uncomment this
312
+ if bare_metal?
313
+ service.power_off_bare_metal_server(id)
314
+ else
315
+ service.power_off_vm(id, true)
316
+ end
317
+ true
318
+ end
319
+
320
+ # Soft power off
321
+ def shutdown
322
+ # requires :id # TODO: debug why this breaks the tests on bare metal and uncomment this
323
+ if bare_metal?
324
+ raise Fog::Errors::Error.new('Shutdown not supported on baremetal servers. Use #stop.')
325
+ else
326
+ service.power_off_vm(id, false)
327
+ end
328
+ true
329
+ end
330
+
331
+ def state
332
+ if bare_metal?
333
+ service.request(:hardware_server, "#{id}/getServerPowerState").body
334
+ else
335
+ service.request(:virtual_guest, "#{id}/getPowerState").body['name']
336
+ end
337
+ end
338
+
339
+ # Creates server
340
+ # * requires attributes: :name, :domain, and :flavor_id OR (:cpu_count && :ram && :disks)
341
+ #
342
+ # @note You should use servers.create to create servers instead calling this method directly
343
+ #
344
+ # * State Transitions
345
+ # * BUILD -> ACTIVE
346
+ # * BUILD -> ERROR (on error)
347
+ def save
348
+ raise Fog::Errors::Error.new('Resaving an existing object may create a duplicate') if persisted?
349
+ copy = self.dup
350
+ copy.pre_save
351
+
352
+ data = if copy.bare_metal?
353
+ service.create_bare_metal_server(copy.attributes).body
354
+ else
355
+ service.create_vm(copy.attributes).body.first
356
+ end
357
+
358
+ data.delete("bare_metal")
359
+ merge_attributes(data)
360
+ true
361
+ end
362
+
363
+ def tags
364
+ attributes[:tags].map { |i| i['tag']['name'] if i['tag'] }.compact if attributes[:tags]
365
+ end
366
+
367
+ def get_active_tickets
368
+ return service.get_bare_metal_active_tickets(id).body if bare_metal?
369
+ service.get_virtual_guest_active_tickets(id).body
370
+ end
371
+
372
+ def get_users
373
+ return service.get_bare_metal_users(id).body if bare_metal?
374
+ service.get_virtual_guest_users(id).body
375
+ end
376
+
377
+ def get_upgrade_options
378
+ return service.get_bare_metal_upgrade_item_prices(id).body if bare_metal?
379
+ service.get_virtual_guest_upgrade_item_prices(id).body
380
+ end
381
+
382
+ def update(update_attributes)
383
+ raise ArgumentError if update_attributes.nil?
384
+ product_connection
385
+ prices = get_item_prices_id(update_attributes)
386
+ order = generate_upgrade_order(prices, update_attributes[:time] || update_attributes[:maintenance_window])
387
+ @product_conn.place_order(order).body
388
+ end
389
+
390
+ def generate_order_template
391
+ copy = self.dup
392
+ copy.pre_save
393
+ return service.generate_bare_metal_order_template(copy.attributes).body if copy.bare_metal?
394
+ service.generate_virtual_guest_order_template(copy.attributes).body
395
+ end
396
+
397
+ def wait_for_id(timeout=14400, delay=30)
398
+ # Cannot use self.wait_for because it calls reload which requires
399
+ # self.id which is not initially available for bare metal.
400
+ filterStr = Fog::JSON.encode({
401
+ "hardware" => {
402
+ "hostname" => {
403
+ "operation" => self.name,
404
+ },
405
+ "domain" => {
406
+ "operation" => self.domain,
407
+ },
408
+ "globalIdentifier" => {
409
+ "operation" => self.uid,
410
+ },
411
+ }
412
+ })
413
+
414
+ Fog.wait_for(timeout, delay) do
415
+ res = service.request(:account, 'getHardware', :query => {
416
+ :objectMask => 'mask[id,fullyQualifiedDomainName,provisionDate,hardwareStatus,lastTransaction[elapsedSeconds,transactionStatus[friendlyName]],operatingSystem[id,passwords[password,username]]]',
417
+ :objectFilter => filterStr,
418
+ })
419
+
420
+ server = res.body.first
421
+
422
+ yield server if block_given?
423
+
424
+ if server and server["provisionDate"]
425
+ attributes[:id] = server['id']
426
+ true
427
+ else
428
+ false
429
+ end
430
+ end
431
+
432
+ self.reload
433
+ true
434
+ end
435
+
436
+ private
437
+
438
+ def network_connection
439
+ @network_conn ||= Fog::Network.new(
440
+ :provider => :softlayer,
441
+ :softlayer_username => service.instance_variable_get(:@softlayer_username),
442
+ :softlayer_api_key => service.instance_variable_get(:@softlayer_api_key)
443
+ )
444
+ end
445
+
446
+ def product_connection
447
+ if Fog.mock?
448
+ @product_conn = Fog::Softlayer::Product.new(
449
+ :provider => :softlayer,
450
+ :softlayer_username => service.instance_variable_get(:@credentials)[:username],
451
+ :softlayer_api_key => service.instance_variable_get(:@credentials)[:api_key]
452
+ )
453
+ end
454
+ @product_conn ||= Fog::Softlayer::Product.new(
455
+ :provider => :softlayer,
456
+ :softlayer_username => service.instance_variable_get(:@softlayer_username),
457
+ :softlayer_api_key => service.instance_variable_get(:@softlayer_api_key)
458
+ )
459
+ end
460
+
461
+ def _get_private_vlan
462
+ if self.id
463
+ vlan_id = if bare_metal?
464
+ service.request(:hardware_server, "#{self.id}/get_private_vlan").body['id']
465
+ else
466
+ service.request(:virtual_guest, self.id, :query => 'objectMask=primaryBackendNetworkComponent.networkVlan').body['primaryBackendNetworkComponent']['networkVlan']['id']
467
+ end
468
+ network_connection.networks.get(vlan_id)
469
+ end
470
+ end
471
+
472
+ def _get_vlan
473
+ if self.id
474
+ vlan_id = if bare_metal?
475
+ service.request(:hardware_server, "#{self.id}/get_public_vlan").body['id']
476
+ else
477
+ service.request(:virtual_guest, self.id, :query => 'objectMask=primaryNetworkComponent.networkVlan').body['primaryNetworkComponent']['networkVlan']['id']
478
+ end
479
+ network_connection.networks.get(vlan_id)
480
+ end
481
+ end
482
+
483
+ ##
484
+ # Generate mapping for use with remap_attributes
485
+ def attributes_mapping
486
+ common = {
487
+ :hourly_billing_flag => :hourlyBillingFlag,
488
+ :os_code => :operatingSystemReferenceCode,
489
+ :vlan => :primaryNetworkComponent,
490
+ :private_vlan => :primaryBackendNetworkComponent,
491
+ :key_pairs => :sshKeys,
492
+ :private_network_only => :privateNetworkOnlyFlag,
493
+ :user_data => :userData,
494
+ :provision_script => :postInstallScriptUri,
495
+ :network_components => :networkComponents,
496
+ }
497
+
498
+ conditional = if bare_metal?
499
+ {
500
+ :cpu => :processorCoreAmount,
501
+ :ram => :memoryCapacity,
502
+ :disk => :hardDrives,
503
+ :bare_metal => :bareMetalInstanceFlag,
504
+ :fixed_configuration_preset => :fixedConfigurationPreset,
505
+ }
506
+ else
507
+ {
508
+ :cpu => :startCpus,
509
+ :ram => :maxMemory,
510
+ :disk => :blockDevices,
511
+ :image_id => :blockDeviceTemplateGroup,
512
+ :ephemeral_storage => :localDiskFlag,
513
+ }
514
+ end
515
+ common.merge(conditional)
516
+ end
517
+
518
+ def bare_metal=(set)
519
+ return @bare_metal if set == @bare_metal
520
+ raise Exception, "Bare metal flag has already been set" unless @bare_metal.nil?
521
+ @bare_metal = case set
522
+ when false, 'false', 0, nil, ''
523
+ attributes[:bare_metal] = false
524
+ else
525
+ attributes[:bare_metal] = true
526
+ end
527
+ end
528
+
529
+ ##
530
+ # Remove model attributes that aren't expected by the SoftLayer API
531
+ def clean_attributes
532
+ attributes.delete(:bare_metal)
533
+ attributes.delete(:flavor_id)
534
+ attributes.delete(:ephemeral_storage)
535
+ attributes.delete(:tags) if bare_metal?
536
+ end
537
+
538
+ ##
539
+ # Expand a "flavor" into cpu, ram, and disk attributes
540
+ def extract_flavor
541
+ if attributes[:flavor_id]
542
+ flavor = @service.flavors.get(attributes[:flavor_id])
543
+ flavor.nil? and Fog::Errors::Error.new("Unrecognized flavor in #{self.class}##{__method__}")
544
+ attributes[:cpu] = flavor.cpu
545
+ attributes[:ram] = flavor.ram
546
+ attributes[:disk] = flavor.disk unless attributes[:image_id]
547
+ if bare_metal?
548
+ value = flavor.disk.first['diskImage']['capacity'] < 500 ? 250 : 500
549
+ attributes[:disk] = [{'capacity'=>value}]
550
+ attributes[:ram] = attributes[:ram] / 1024 if attributes[:ram] > 64
551
+ end
552
+ end
553
+ end
554
+
555
+ def validate_attributes
556
+ requires :name, :domain, :datacenter
557
+ if attributes[:fixed_configuration_preset]
558
+ requires :os_code
559
+ else
560
+ requires :cpu, :ram
561
+ requires_one :os_code, :image_id
562
+ requires_one :disk, :image_id
563
+ end
564
+ bare_metal? and image_id and raise ArgumentError, "Bare Metal Cloud does not support booting from Image"
565
+ end
566
+
567
+ def set_defaults
568
+ attributes[:hourly_billing_flag] = true if attributes[:hourly_billing_flag].nil?
569
+ attributes[:ephemeral_storage] = false if attributes[:ephemeral_storage].nil?
570
+ attributes[:domain] = service.softlayer_default_domain if service.softlayer_default_domain and attributes[:domain].nil?
571
+ self.datacenter = service.softlayer_default_datacenter if service.softlayer_default_datacenter and attributes[:datacenter].nil?
572
+ end
573
+
574
+ def get_item_prices_id_by_value(item_price_array, category, value)
575
+ item_prices = item_price_array.select { |item_price| item_price["categories"].find { |category_hash| category_hash["categoryCode"] == category } }
576
+ item_price = item_prices.find { |item_price| item_price['item']['capacity'] == value.to_s }
577
+ item_price.nil? ? "" : item_price["id"]
578
+ end
579
+
580
+ def get_item_prices_id(update_attributes)
581
+ item_price_array = get_upgrade_options
582
+ update_attributes.delete(:time)
583
+ update_attributes.delete(:maintenance_window)
584
+ update_attributes.map { |key, value| { :id => get_item_prices_id_by_value(item_price_array, key.to_s, value) } }
585
+ end
586
+
587
+ def bm_upgrade_order_template(value)
588
+ {
589
+ :complexType => 'SoftLayer_Container_Product_Order_Hardware_Server_Upgrade',
590
+ :hardware => [
591
+ {
592
+ :id => id
593
+ }
594
+ ],
595
+ :properties => [
596
+ {
597
+ :name => 'MAINTENANCE_WINDOW_ID',
598
+ :value => value
599
+ }
600
+ ]
601
+ }
602
+ end
603
+
604
+ def vm_upgrade_order_template(time)
605
+ {
606
+ :complexType => 'SoftLayer_Container_Product_Order_Virtual_Guest_Upgrade',
607
+ :virtualGuests => [
608
+ {
609
+ :id => id
610
+ }
611
+ ],
612
+ :properties => [
613
+ {
614
+ :name => 'MAINTENANCE_WINDOW',
615
+ :value => (time.nil? || time.empty?) ? Time.now.iso8601 : time.iso8601
616
+ }
617
+ ]
618
+ }
619
+ end
620
+
621
+ def generate_upgrade_order(prices, value)
622
+ return bm_upgrade_order_template(value).merge({ :prices => prices }) if bare_metal?
623
+ vm_upgrade_order_template(value).merge({ :prices => prices })
624
+ end
625
+ end
626
+ end
627
+ end
628
+ end