knife-clc 0.0.1.pre

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,595 @@
1
+ require 'chef/knife/clc_base'
2
+ require 'chef/knife/clc_server_show'
3
+ require 'chef/knife/bootstrap'
4
+ require 'chef/node'
5
+
6
+ class Chef
7
+ class Knife
8
+ class ClcServerCreate < Knife
9
+ include Knife::ClcBase
10
+
11
+ banner 'knife clc server create (options)'
12
+
13
+ option :clc_name,
14
+ :long => '--name NAME',
15
+ :description => 'Name of the server to create',
16
+ :on => :head
17
+
18
+ option :clc_description,
19
+ :long => '--description DESCRIPTION',
20
+ :description => 'User-defined description of this server',
21
+ :on => :head
22
+
23
+ option :clc_group,
24
+ :long => '--group ID',
25
+ :description => 'ID of the parent group',
26
+ :on => :head
27
+
28
+ option :clc_source_server,
29
+ :long => '--source-server ID',
30
+ :description => 'ID of the server to use a source. May be the ID of a template, or when cloning, an existing server ID',
31
+ :on => :head
32
+
33
+ option :clc_managed,
34
+ :long => '--managed',
35
+ :boolean => true,
36
+ :description => 'Whether to create the server as managed or not',
37
+ :on => :head
38
+
39
+ option :clc_managed_backup,
40
+ :long => '--managed-backup',
41
+ :boolean => true,
42
+ :description => 'Whether to add managed backup to the server',
43
+ :on => :head
44
+
45
+ option :clc_primary_dns,
46
+ :long => '--primary-dns ADDRESS',
47
+ :description => 'Primary DNS to set on the server',
48
+ :on => :head
49
+
50
+ option :clc_secondary_dns,
51
+ :long => '--secondary-dns ADDRESS',
52
+ :description => 'Secondary DNS to set on the server',
53
+ :on => :head
54
+
55
+ option :clc_network,
56
+ :long => '--network ID',
57
+ :description => 'ID of the network to which to deploy the server',
58
+ :on => :head
59
+
60
+ option :clc_ip,
61
+ :long => '--ip ADDRESS',
62
+ :description => 'IP address to assign to the server',
63
+ :on => :head
64
+
65
+ option :clc_server_password,
66
+ :long => '--server-password PASSWORD',
67
+ :description => 'Password of administrator or root user on server',
68
+ :on => :head
69
+
70
+ option :clc_source_server_password,
71
+ :long => '--source-server-password PASSWORD',
72
+ :description => 'Password of the source server, used only when creating a clone from an existing server',
73
+ :on => :head
74
+
75
+ option :clc_cpu,
76
+ :long => '--cpu COUNT',
77
+ :description => 'Number of processors to configure the server with',
78
+ :on => :head
79
+
80
+ option :clc_cpu_autoscale_policy,
81
+ :long => '--cpu-autoscale-policy ID',
82
+ :description => 'ID of the vertical CPU Autoscale policy to associate the server with',
83
+ :on => :head
84
+
85
+ option :clc_memory,
86
+ :long => '--memory COUNT',
87
+ :description => 'Number of GB of memory to configure the server with',
88
+ :on => :head
89
+
90
+ option :clc_type,
91
+ :long => '--type TYPE',
92
+ :description => 'Whether to create a standard or hyperscale server',
93
+ :on => :head
94
+
95
+ option :clc_storage_type,
96
+ :long => '--storage-type TYPE',
97
+ :description => 'For standard servers, whether to use standard or premium storage',
98
+ :on => :head
99
+
100
+ option :clc_anti_affinity_policy,
101
+ :long => '--anti-affinity-policy ID',
102
+ :description => 'ID of the Anti-Affinity policy to associate the server with',
103
+ :on => :head
104
+
105
+ option :clc_custom_fields,
106
+ :long => '--custom-field KEY=VALUE',
107
+ :description => 'Custom field key-value pair',
108
+ :on => :head,
109
+ :proc => ->(param) do
110
+ Chef::Config[:knife][:clc_custom_fields] ||= []
111
+ Chef::Config[:knife][:clc_custom_fields] << param
112
+ end
113
+
114
+ option :clc_disks,
115
+ :long => '--disk PATH,SIZE,TYPE',
116
+ :description => 'Configuration for an additional server disk',
117
+ :on => :head,
118
+ :proc => ->(param) do
119
+ Chef::Config[:knife][:clc_disks] ||= []
120
+ Chef::Config[:knife][:clc_disks] << param
121
+ end
122
+
123
+ option :clc_ttl,
124
+ :long => '--ttl DATETIME',
125
+ :description => 'Date/time that the server should be deleted',
126
+ :on => :head
127
+
128
+ option :clc_packages,
129
+ :long => '--package ID,KEY_1=VALUE[,KEY_2=VALUE]',
130
+ :description => 'Package to run on the server after it has been built',
131
+ :on => :head,
132
+ :proc => ->(param) do
133
+ Chef::Config[:knife][:clc_packages] ||= []
134
+ Chef::Config[:knife][:clc_packages] << param
135
+ end
136
+
137
+ option :clc_configuration,
138
+ :long => '--configuration ID',
139
+ :description => 'Specifies the identifier for the specific configuration type of bare metal server to deploy',
140
+ :on => :head
141
+
142
+ option :clc_os_type,
143
+ :long => '--os-type TYPE',
144
+ :description => 'Specifies the OS to provision with the bare metal server',
145
+ :on => :head
146
+
147
+ option :clc_allowed_protocols,
148
+ :long => '--allow PROTOCOL:FROM[-TO]',
149
+ :description => 'Assigns public IP with permissions for specified protocol',
150
+ :on => :head,
151
+ :proc => ->(param) do
152
+ Chef::Config[:knife][:clc_allowed_protocols] ||= []
153
+ Chef::Config[:knife][:clc_allowed_protocols] << param
154
+ end
155
+
156
+ option :clc_sources,
157
+ :long => '--source CIDR',
158
+ :description => 'The source IP address range allowed to access the new public IP address',
159
+ :on => :head,
160
+ :proc => ->(param) do
161
+ Chef::Config[:knife][:clc_sources] ||= []
162
+ Chef::Config[:knife][:clc_sources] << param
163
+ end
164
+
165
+ option :clc_wait,
166
+ :long => '--wait',
167
+ :description => 'Wait for operation completion',
168
+ :boolean => true,
169
+ :default => false,
170
+ :on => :head
171
+
172
+ option :clc_bootstrap,
173
+ :long => '--bootstrap',
174
+ :description => 'Bootstrap launched server using standard `knife bootstrap` command',
175
+ :boolean => true,
176
+ :default => false,
177
+ :on => :head
178
+
179
+ option :clc_bootstrap_private,
180
+ :long => '--bootstrap-private',
181
+ :description => 'Bootstrap from private network. Requires client or SSH gateway to have an access to private network of the server',
182
+ :boolean => true,
183
+ :default => false,
184
+ :on => :head
185
+
186
+ def parse_and_validate_parameters
187
+ unless config[:clc_name]
188
+ errors << 'Name is required'
189
+ end
190
+
191
+ unless config[:clc_group]
192
+ errors << 'Group ID is required'
193
+ end
194
+
195
+ unless config[:clc_source_server]
196
+ errors << 'Source server ID is required'
197
+ end
198
+
199
+ unless config[:clc_cpu]
200
+ errors << 'Number of CPUs is required'
201
+ end
202
+
203
+ unless config[:clc_memory]
204
+ errors << 'Number of memory GBs is required'
205
+ end
206
+
207
+ unless config[:clc_type]
208
+ errors << 'Type is required'
209
+ end
210
+
211
+ custom_fields = config[:clc_custom_fields]
212
+ if custom_fields && custom_fields.any?
213
+ parse_custom_fields(custom_fields)
214
+ end
215
+
216
+ disks = config[:clc_disks]
217
+ if disks && disks.any?
218
+ parse_disks(disks)
219
+ end
220
+
221
+ packages = config[:clc_packages]
222
+ if packages && packages.any?
223
+ parse_packages(packages)
224
+ end
225
+
226
+ permissions = config[:clc_allowed_protocols]
227
+ if permissions && permissions.any?
228
+ parse_protocol_permissions(permissions)
229
+ end
230
+
231
+ sources = config[:clc_sources]
232
+ if sources && sources.any?
233
+ parse_sources(sources)
234
+ end
235
+
236
+ bootstrap = config[:clc_bootstrap]
237
+ if bootstrap
238
+ check_chef_server_connectivity
239
+ check_server_platform
240
+ if config[:clc_wait]
241
+ check_bootstrap_connectivity_params
242
+ else
243
+ check_bootstrap_node_connectivity_params
244
+ end
245
+ end
246
+ end
247
+
248
+ def check_chef_server_connectivity
249
+ Chef::Node.list
250
+ rescue Exception => e
251
+ errors << 'Could not connect to Chef Server: ' + e.message
252
+ end
253
+
254
+ def check_bootstrap_node_connectivity_params
255
+ command = bootstrap_command
256
+ # Chef 12.0 does not have bootstrap context accessor and validates key by itself
257
+ return unless command.respond_to?(:bootstrap_context)
258
+
259
+ context = command.bootstrap_context
260
+ unless context.validation_key
261
+ errors << "Validatorless async bootstrap is not supported. Validation key #{Chef::Config[:validation_key]} not found"
262
+ end
263
+ end
264
+
265
+ def check_bootstrap_connectivity_params
266
+ return if indirect_bootstrap?
267
+
268
+ if public_ip_requested?
269
+ errors << 'Bootstrapping requires SSH access to the server' unless ssh_access_requested?
270
+ else
271
+ errors << 'Bootstrapping requires public IP access to the server. Ignore this check with --bootstrap-private'
272
+ end
273
+ end
274
+
275
+ def check_server_platform
276
+ return unless config[:clc_group] && config[:clc_source_server]
277
+
278
+ if template = find_source_template
279
+ windows_platform = template['osType'] =~ /windows/
280
+ elsif server = find_source_server
281
+ windows_platform = server['os'] =~ /windows/
282
+ end
283
+
284
+ if windows_platform
285
+ errors << 'Bootstrapping is available for Linux platform only'
286
+ end
287
+ rescue Clc::CloudExceptions::Error => e
288
+ errors << "Could not derive server bootstrap platform: #{e.message}"
289
+ end
290
+
291
+ def find_source_template
292
+ group = connection.show_group(config[:clc_group])
293
+ datacenter_id = group['locationId']
294
+ connection.list_templates(datacenter_id).find do |template|
295
+ template['name'] == config[:clc_source_server]
296
+ end
297
+ end
298
+
299
+ def find_source_server
300
+ connection.show_server(config[:clc_source_server])
301
+ end
302
+
303
+ def public_ip_requested?
304
+ config[:clc_allowed_protocols] && config[:clc_allowed_protocols].any?
305
+ end
306
+
307
+ def ssh_access_requested?
308
+ ssh_port = requested_ssh_port
309
+
310
+ config[:clc_allowed_protocols].find do |permission|
311
+ protocol, from, to = permission.values_at('protocol', 'port', 'portTo')
312
+ next unless protocol == 'tcp'
313
+ next unless from
314
+
315
+ to ||= from
316
+
317
+ Range.new(from, to).include? ssh_port
318
+ end
319
+ end
320
+
321
+ def requested_ssh_port
322
+ (config[:ssh_port] && Integer(config[:ssh_port])) || 22
323
+ end
324
+
325
+ def parse_custom_fields(custom_fields)
326
+ custom_fields.map! do |param|
327
+ key, value = param.split('=', 2)
328
+
329
+ unless key && value
330
+ errors << "Custom field definition #{param} is malformed"
331
+ next
332
+ end
333
+
334
+ { 'id' => key, 'value' => value }
335
+ end
336
+ end
337
+
338
+ def parse_disks(disks)
339
+ disks.map! do |param|
340
+ path, size, type = param.split(',', 3)
341
+
342
+ unless path && size && type
343
+ errors << "Disk definition #{param} is malformed"
344
+ end
345
+
346
+ { 'path' => path, 'sizeGB' => size, 'type' => type }
347
+ end
348
+ end
349
+
350
+ def parse_packages(packages)
351
+ packages.map! do |param|
352
+ begin
353
+ id, package_params = param.split(',', 2)
354
+ package_params = package_params.split(',').map { |pair| Hash[*pair.split('=', 2)] }
355
+ { 'packageId' => id, 'parameters' => package_params }
356
+ rescue Exception => e
357
+ errors << "Package definition #{param} is malformed"
358
+ end
359
+ end
360
+ end
361
+
362
+ def parse_protocol_permissions(permissions)
363
+ permissions.map! do |param|
364
+ protocol, port_range = param.split(':', 2)
365
+
366
+ case protocol.downcase
367
+ when 'ssh', 'sftp' then { 'protocol' => 'tcp', 'port' => 22 }
368
+ when 'rdp' then { 'protocol' => 'tcp', 'port' => 3389 }
369
+ when 'icmp' then { 'protocol' => 'icmp' }
370
+ when 'http' then [{ 'protocol' => 'tcp', 'port' => 80 }, { 'protocol' => 'tcp', 'port' => 8080 }]
371
+ when 'https' then { 'protocol' => 'tcp', 'port' => 443 }
372
+ when 'ftp' then { 'protocol' => 'tcp', 'port' => 21 }
373
+ when 'ftps' then { 'protocol' => 'tcp', 'port' => 990 }
374
+ when 'udp', 'tcp'
375
+ unless port_range
376
+ errors << "No ports specified for #{param}"
377
+ else
378
+ ports = port_range.split('-').map do |port_string|
379
+ Integer(port_string) rescue nil
380
+ end
381
+
382
+ if ports.any?(&:nil?) || ports.size > 2 || ports.size < 1
383
+ errors << "Malformed port range for #{param}"
384
+ end
385
+
386
+ {
387
+ 'protocol' => protocol.downcase,
388
+ 'port' => ports[0],
389
+ 'portTo' => ports[1]
390
+ }.keep_if { |_, value| value }
391
+ end
392
+ else
393
+ errors << "Unsupported protocol for #{param}"
394
+ end
395
+ end
396
+
397
+ permissions.flatten!
398
+ end
399
+
400
+ def parse_sources(sources)
401
+ sources.map! do |cidr|
402
+ { 'cidr' => cidr }
403
+ end
404
+ end
405
+
406
+ def prepare_launch_params
407
+ {
408
+ 'name' => config[:clc_name],
409
+ 'description' => config[:clc_description],
410
+ 'groupId' => config[:clc_group],
411
+ 'sourceServerId' => config[:clc_source_server],
412
+ 'isManagedOS' => config[:clc_managed],
413
+ 'isManagedBackup' => config[:clc_managed_backup],
414
+ 'primaryDns' => config[:clc_primary_dns],
415
+ 'secondaryDns' => config[:clc_secondary_dns],
416
+ 'networkId' => config[:clc_network],
417
+ 'ipAddress' => config[:clc_ip],
418
+ 'password' => config[:clc_server_password],
419
+ 'sourceServerPassword' => config[:clc_source_server_password],
420
+ 'cpu' => config[:clc_cpu].to_i,
421
+ 'cpuAutoscalePolicyId' => config[:clc_cpu_autoscale_policy],
422
+ 'memoryGB' => config[:clc_memory].to_i,
423
+ 'type' => config[:clc_type],
424
+ 'storageType' => config[:clc_storage_type],
425
+ 'antiAffinityPolicyId' => config[:clc_anti_affinity_policy],
426
+ 'customFields' => config[:clc_custom_fields],
427
+ 'additionalDisks' => config[:clc_disks],
428
+ 'ttl' => config[:clc_ttl],
429
+ 'packages' => config[:clc_packages],
430
+ }.delete_if { |_, value| !value.kind_of?(Integer) && (value.nil? || value.empty?) }
431
+ end
432
+
433
+ def prepare_ip_params
434
+ {
435
+ 'ports' => config[:clc_allowed_protocols],
436
+ 'sourceRestrictions' => config[:clc_sources]
437
+ }.delete_if { |_, value| value.nil? || value.empty? }
438
+ end
439
+
440
+ def execute
441
+ config[:clc_wait] ? sync_create_server : async_create_server
442
+ end
443
+
444
+ def sync_create_server
445
+ ui.info 'Requesting server launch...'
446
+ links = connection.create_server(prepare_launch_params)
447
+ connection.wait_for(links['operation']['id']) { putc '.' }
448
+ ui.info "\n"
449
+ ui.info "Server has been launched"
450
+
451
+ if config[:clc_allowed_protocols]
452
+ ui.info 'Requesting public IP...'
453
+ server = connection.follow(links['resource'])
454
+ ip_links = connection.create_ip_address(server['id'], prepare_ip_params)
455
+ connection.wait_for(ip_links['operation']['id']) { putc '.' }
456
+ ui.info "\n"
457
+ ui.info 'Public IP has been assigned'
458
+ end
459
+
460
+ if config[:clc_bootstrap]
461
+ sync_bootstrap(links['resource']['id'])
462
+ end
463
+
464
+ argv = [links['resource']['id'], '--uuid', '--creds']
465
+ if config[:clc_allowed_protocols]
466
+ argv << '--ports'
467
+ end
468
+
469
+ if (username = config[:clc_username]) && (password = config[:clc_password])
470
+ argv.concat(['--username', username, '--password', password])
471
+ end
472
+
473
+ Chef::Knife::ClcServerShow.new(argv).run
474
+ end
475
+
476
+ def async_create_server
477
+ launch_params = prepare_launch_params
478
+
479
+ if config[:clc_bootstrap]
480
+ add_bootstrapping_params(launch_params)
481
+ ui.info 'Bootstrap has been scheduled'
482
+ end
483
+
484
+ ui.info 'Requesting server launch...'
485
+ links = connection.create_server(launch_params)
486
+ ui.info 'Launch request has been sent'
487
+ ui.info "You can check launch operation status with 'knife clc operation show #{links['operation']['id']}'"
488
+
489
+ if config[:clc_allowed_protocols]
490
+ ui.info 'Requesting public IP...'
491
+ server = connection.follow(links['resource'])
492
+ ip_links = connection.create_ip_address(server['id'], prepare_ip_params)
493
+ ui.info 'Public IP request has been sent'
494
+ ui.info "You can check assignment operation status with 'knife clc operation show #{ip_links['operation']['id']}'"
495
+ end
496
+
497
+ argv = [links['resource']['id'], '--uuid', '--creds']
498
+ argv << '--ports' if config[:clc_allowed_protocols]
499
+
500
+ ui.info "You can check server status later with 'knife clc server show #{argv.join(' ')}'"
501
+ end
502
+
503
+ def sync_bootstrap(uuid)
504
+ server = connection.show_server(uuid, true)
505
+
506
+ ensure_server_powered_on(server)
507
+
508
+ command = bootstrap_command
509
+
510
+ command.name_args = [get_server_fqdn(server)]
511
+
512
+ username, password = config.values_at(:ssh_user, :ssh_password)
513
+ unless username && password
514
+ creds = get_server_credentials(server)
515
+ command.config.merge!(:ssh_user => creds['userName'], :ssh_password => creds['password'])
516
+ end
517
+
518
+ command.config[:chef_node_name] ||= server['name']
519
+
520
+ retry_on_timeouts { command.run }
521
+ end
522
+
523
+ def retry_on_timeouts(tries = 2, &block)
524
+ yield
525
+ rescue Errno::ETIMEDOUT => e
526
+ tries -= 1
527
+
528
+ if tries > 0
529
+ ui.info 'Retrying host connection...'
530
+ retry
531
+ else
532
+ raise
533
+ end
534
+ end
535
+
536
+ def ensure_server_powered_on(server)
537
+ return unless server['details']['powerState'] == 'stopped'
538
+ ui.info 'Requesting server power on...'
539
+ links = connection.power_on_server(server['id'])
540
+ connection.wait_for(links['operation']['id']) { putc '.' }
541
+ ui.info "\n"
542
+ ui.info 'Server has been powered on'
543
+ end
544
+
545
+ def add_bootstrapping_params(launch_params)
546
+ launch_params['packages'] ||= []
547
+ launch_params['packages'] << package_for_async_bootstrap
548
+ end
549
+
550
+ def package_for_async_bootstrap
551
+ {
552
+ 'packageId' => 'a5d9d04369df4276a4f98f2ca7f7872b',
553
+ 'parameters' => {
554
+ 'Mode' => 'Ssh',
555
+ 'Script' => bootstrap_command.render_template
556
+ }
557
+ }
558
+ end
559
+
560
+ def get_server_fqdn(server)
561
+ if indirect_bootstrap?
562
+ private_ips = server['details']['ipAddresses'].map { |addr| addr['internal'] }.compact
563
+ private_ips.first
564
+ else
565
+ public_ips = server['details']['ipAddresses'].map { |addr| addr['public'] }.compact
566
+ public_ips.first
567
+ end
568
+ end
569
+
570
+ def indirect_bootstrap?
571
+ config[:clc_bootstrap_private] || config[:ssh_gateway]
572
+ end
573
+
574
+ def get_server_credentials(server)
575
+ creds_link = server['links'].find { |link| link['rel'] == 'credentials' }
576
+ connection.follow(creds_link) if creds_link
577
+ end
578
+
579
+ def self.bootstrap_command_class
580
+ Chef::Knife::Bootstrap
581
+ end
582
+
583
+ def bootstrap_command
584
+ command_class = self.class.bootstrap_command_class
585
+ command_class.load_deps
586
+ command = command_class.new
587
+ command.config.merge!(config)
588
+ command.configure_chef
589
+ command
590
+ end
591
+
592
+ self.options.merge!(bootstrap_command_class.options)
593
+ end
594
+ end
595
+ end
@@ -0,0 +1,38 @@
1
+ require 'chef/knife/clc_base'
2
+
3
+ class Chef
4
+ class Knife
5
+ class ClcServerDelete < Knife
6
+ include Knife::ClcBase
7
+
8
+ banner 'knife clc server delete ID (options)'
9
+
10
+ option :clc_wait,
11
+ :long => '--wait',
12
+ :description => 'Wait for operation completion',
13
+ :boolean => true,
14
+ :default => false,
15
+ :on => :head
16
+
17
+ def parse_and_validate_parameters
18
+ unless name_args[0]
19
+ errors << 'Server ID is required'
20
+ end
21
+ end
22
+
23
+ def execute
24
+ ui.info 'Requesting server deletion...'
25
+ links = connection.delete_server(name_args[0])
26
+
27
+ if config[:clc_wait]
28
+ connection.wait_for(links['operation']['id']) { putc '.' }
29
+ ui.info "\n"
30
+ ui.info 'Server has been deleted'
31
+ else
32
+ ui.info 'Deletion request has been sent'
33
+ ui.info "You can check deletion operation status with 'knife clc operation show #{links['operation']['id']}'"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end