Module: Puppet::CloudPack

Defined in:
lib/puppet/cloudpack/progressbar.rb,
lib/puppet/cloudpack.rb

Overview

Ruby/ProgressBar - a text progress bar library

Copyright © 2001-2005 Satoru Takabayashi <satoru@namazu.org>

All rights reserved.
This is free software with ABSOLUTELY NO WARRANTY.

You can redistribute it and/or modify it under the terms of Ruby’s license.

Defined Under Namespace

Classes: HttpHeaders, InstanceErrorState, ProgressBar, ReversedProgressBar

Class Method Summary collapse

Class Method Details

.add_availability_zone_option(action) ⇒ Object



42
43
44
45
46
47
48
49
# File 'lib/puppet/cloudpack.rb', line 42

def add_availability_zone_option(action)
  action.option '--availability-zone=' do
    summary 'AWS availability zone.'
    description <<-EOT
      Specifies the availability zone into which the VM will be created
    EOT
 end
end

.add_bootstrap_options(action) ⇒ Object



298
299
300
301
# File 'lib/puppet/cloudpack.rb', line 298

def add_bootstrap_options(action)
  add_create_options(action)
  add_init_options(action)
end

.add_classify_options(action) ⇒ Object



520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
# File 'lib/puppet/cloudpack.rb', line 520

def add_classify_options(action)
  action.option '--enc-server=' do
    summary 'The external node classifier hostname.'
    description <<-EOT
      The hostname of the external node classifier.  This currently only
      supports Puppet Enterprise's console and Puppet Dashboard as external
      node classifiers.
    EOT
    default_to do
      Puppet[:server]
    end
  end

  action.option '--enc-port=' do
    summary 'The External Node Classifier Port.'
    description <<-EOT
      The port of the External Node Classifier.  This currently only
      supports Puppet Enterprise's console and Puppet Dashboard as external
      node classifiers.
    EOT
    default_to do 3000 end
  end

  action.option '--enc-auth-user=' do
    summary 'User name for authentication to the ENC.'
    description <<-EOT
      PE's console and Puppet Dashboard can be secured using HTTP
      authentication.  If the console or dashboard is configured with HTTP
      authentication, use this option to supply credentials for accessing it.

      Note: This option will default to the PUPPET_ENC_AUTH_USER
      environment variable.
    EOT
    default_to do ENV['PUPPET_ENC_AUTH_USER'] end
  end

  action.option '--enc-auth-passwd=' do
    summary 'Password for authentication to the ENC.'
    description <<-EOT
      PE's console and Puppet Dashboard can be secured using HTTP
      authentication.  If the console or dashboard is configured with HTTP
      authentication, use this option to supply credentials for accessing it.

      Note: This option will default to the PUPPET_ENC_AUTH_PASSWD
      environment variable.
    EOT
    default_to do ENV['PUPPET_ENC_AUTH_PASSWD'] end
  end

  action.option '--node-group=', '--as=' do
    summary 'The ENC node group to associate the node with.'
    description <<-'EOT'
      The PE console or Puppet Dashboard group to associate the node with.
      The group must already exist in the ENC, or an error will be
      returned.  If the node has not been registered with the ENC, it will
      automatically be registered when assigning it to a group.
    EOT
  end

  action.option '--insecure' do
    summary 'Don\'t perform ENC SSL certificate verification'
    description <<-'EOT'
      Don't verify the SSL certificate when connecting to an ENC.
      When connecting to the Puppet Enterprise Console, verification can be
      optionally skipped as older versions of the PE Console used to use
      certificates with a hardcoded Common Name which cannot be verified.
    EOT
    default_to { false }
  end
end

.add_create_options(action) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
# File 'lib/puppet/cloudpack.rb', line 143

def add_create_options(action)
  add_platform_option(action)
  add_region_option(action)
  add_availability_zone_option(action)
  add_tags_option(action)
  add_image_option(action)
  add_type_option(action)
  add_keyname_option(action)
  add_subnet_option(action)
  add_group_option(action)
end

.add_credential_option(action) ⇒ Object



78
79
80
81
82
83
84
85
86
87
# File 'lib/puppet/cloudpack.rb', line 78

def add_credential_option(action)
  action.option '--credentials=' do
    summary 'Cloud credentials to use from ~/.fog'
    description <<-EOT
      For accessing more than a single account, auxiliary credentials other
      than 'default' may be supplied in the credentials location (usually
      ~/.fog).
    EOT
  end
end

.add_fingerprint_options(action) ⇒ Object



280
281
282
283
# File 'lib/puppet/cloudpack.rb', line 280

def add_fingerprint_options(action)
  add_platform_option(action)
  add_region_option(action)
end

.add_group_option(action) ⇒ Object



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/puppet/cloudpack.rb', line 253

def add_group_option(action)
  action.option '--security-group=', '-g=' do
    summary "The instance's security group(s)."
    description <<-EOT
      The security group(s) that the machine will be associated with. A
      security group determines the rules for both inbound and outbound
      connections.

      Multiple groups can be specified as a colon-separated list. The
      groups can be specified by names or IDs.
    EOT
    before_action do |action, args, options|
      Puppet::CloudPack.group_option_before_action(options)
    end
  end
end

.add_image_option(action) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/puppet/cloudpack.rb', line 189

def add_image_option(action)
  action.option '--image=', '-i=' do
    summary 'AMI to use when creating the instance.'
    description <<-EOT
      The pre-configured operating system image to use when creating this
      machine instance. Currently, only AMI images are supported. Example
      of a Redhat 5.6 32bit image: ami-b241bfdb
    EOT
    required
    before_action do |action, args, options|
      if Puppet::CloudPack.create_connection(options).images.get(options[:image]).nil?
        raise ArgumentError, "Unrecognized image name: #{options[:image]}"
      end
    end
  end
end

.add_init_options(action) ⇒ Object



285
286
287
288
# File 'lib/puppet/cloudpack.rb', line 285

def add_init_options(action)
  add_install_options(action)
  add_classify_options(action)
end

.add_install_options(action) ⇒ Object



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/puppet/cloudpack.rb', line 303

def add_install_options(action)
  action.option '--facts=' do
    summary 'Set custom facts in format of fact1=value,fact2=value'
    description <<-'EOT'
      To install custom facts during install of a node, use the format
      fact1=value,fact2=value. Currently, there is no way to escape
      the ',' character so facts cannot contain this character.

      Requirements:
      For community installs of puppet, i.e. not Puppet Enterprise,
      the Puppet Labs' `stdlib` module will be required. It can be found
      at 'http://forge.puppetlabs.com/puppetlabs/stdlib' or installed
      with the command 'puppet-module install puppetlabs/stdlib'.

      For Puppet Enterprise installs, there are no extra requirements
      for this option to work
    EOT

    before_action do |action, arguments, options|
      ## This converts 'this=that,foo=bar,biz=baz=also' to
      ## { 'this' => 'that', 'foo' => 'barr', 'biz' => 'baz=also'}
      ##
      ## A regex is needed that will allow us to escape ',' characters
      ## from the CLI
      begin
        options[:facts] = Hash[ options[:facts].split(',').map do |fact|
          fact_array = fact.split('=',2)
          if fact_array.size != 2
            raise ArgumentError, 'Could not parse facts given. Please check your format'
          end
          if [nil,''].include? fact_array[0] or [nil,''].include? fact_array[1]
            raise ArgumentError, 'Could not parse facts given. Please check your format'
          end
          fact_array
        end ]
      rescue
        raise ArgumentError, 'Could not parse facts given. Please check your format'
      end
    end
  end

  action.option '--login=', '-l=', '--username=' do
    summary 'User to log in to the instance as.'
    description <<-EOT
      The name of the user Puppet should use when logging in to the node.
      This user should configured to allow passwordless access via the SSH
      key supplied in the --keyfile option.

      This is usually the root user.
    EOT
    required
  end

  action.option '--keyfile=' do
    summary "The path to a local SSH private key (or 'agent' if using an agent)."
    description <<-EOT
      The filesystem path to a local private key that can be used to SSH
      into the node. If the node was created with the `node_aws` `create`
      action, this should be the path to the private key file downloaded
      from the Amazon EC2 interface.

      Specify 'agent' if you have the key loaded in ssh-agent and
      available via the SSH_AUTH_SOCK variable.
    EOT
    required
    before_action do |action, arguments, options|
      # If the user specified --keyfile=agent, check for SSH_AUTH_SOCK
      if options[:keyfile].downcase == 'agent' then
        # Force the option value to lower case to make it easier to test
        # for 'agent' in all other sections of the code.
        options[:keyfile].downcase!
        # Check if the user actually has access to an Agent.
        if ! ENV['SSH_AUTH_SOCK'] then
          raise ArgumentError,
            "SSH_AUTH_SOCK environment variable is not set and you specified --keyfile agent.  Please check that ssh-agent is running correctly, and that SSH agent forwarding is not disabled."
        end
        # We break out of the before action block because we don't really
        # have anything else to do to support ssh agent authentication.
        break
      end

      keyfile = File.expand_path(options[:keyfile])
      unless test 'f', keyfile
        raise ArgumentError, "Could not find file '#{keyfile}'"
      end
      unless test 'r', keyfile
        raise ArgumentError, "Could not read from file '#{keyfile}'"
      end
    end
  end

  add_payload_options(action)
end

.add_keyname_option(action) ⇒ Object



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/puppet/cloudpack.rb', line 221

def add_keyname_option(action)
  action.option '--keyname=' do
    summary 'The AWS SSH key name as shown in the AWS console. See the list_keynames action.'
    description <<-EOT
      The name of the SSH key pair to use, as listed in the Amazon AWS
      console.  When creating the instance, Amazon will install the
      requested SSH public key into the instance's authorized_keys file.
      Not to be confused with the --keyfile option of the `node`
      subcommand's `install` action.

      You can use the `list_keynames` action to get a list of valid key
      pairs.
    EOT
    required
    before_action do |action, args, options|
      if Puppet::CloudPack.create_connection(options).key_pairs.get(options[:keyname]).nil?
        raise ArgumentError, "Unrecognized key name: #{options[:keyname]} (Suggestion: use the puppet node_aws list_keynames action to find a list of valid key names for your account.)"
      end
    end
  end
end

.add_list_keynames_options(action) ⇒ Object



275
276
277
278
# File 'lib/puppet/cloudpack.rb', line 275

def add_list_keynames_options(action)
  add_platform_option(action)
  add_region_option(action)
end

.add_list_options(action) ⇒ Object



270
271
272
273
# File 'lib/puppet/cloudpack.rb', line 270

def add_list_options(action)
  add_platform_option(action)
  add_region_option(action)
end

.add_payload_options(action) ⇒ Object



397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'lib/puppet/cloudpack.rb', line 397

def add_payload_options(action)
  action.option '--installer-payload=' do
    summary 'The location of the gzipped Puppet Enterprise install tarball.'
    description <<-EOT
      Location of the Puppet Enterprise install tarball to be used for
      the installation. Can be a local file path or a URL. This option is
      only required if Puppet should be installed on the machine. The
      specified tarball must be gzipped.
      Note that the specified Puppet install tarball must support the
      platform of the node on which the tarball is to be installed.
      If you are unsure about the node platform, use the universal install
      tarball. But be warned: it is huge.
    EOT
    before_action do |action, arguments, options|
      type = Puppet::CloudPack.payload_type(options[:installer_payload])
      case type
      when :invalid
        raise ArgumentError, "Invalid input '#{options[:installer_payload]}' for option installer-payload, should be a URL or a file path"
      when :file_path
        options[:installer_payload] = File.expand_path(options[:installer_payload])
        unless test 'f', options[:installer_payload]
          raise ArgumentError, "Could not find file '#{options[:installer_payload]}'"
        end
        unless test 'r', options[:installer_payload]
          raise ArgumentError, "Could not read from file '#{options[:installer_payload]}'"
        end
      end
      unless(options[:installer_payload] =~ /(tgz|gz)$/)
        Puppet.warning("Option: intaller-payload expects a .tgz or .gz file")
      end
    end
  end

  action.option '--installer-answers=' do
    summary 'Answers file to be used for PE installation.'
    description <<-EOT
      Location of the answers file that should be copied to the machine
      to install Puppet Enterprise.
    EOT
    before_action do |action, arguments, options|
      options[:installer_answers] = File.expand_path(options[:installer_answers])
      unless test 'f', options[:installer_answers]
        raise ArgumentError, "Could not find file '#{options[:installer_answers]}'"
      end
      unless test 'r', options[:installer_answers]
        raise ArgumentError, "Could not read from file '#{options[:installer_answers]}'"
      end
    end
  end

  action.option '--puppetagent-certname=' do
    summary 'The puppet agent certificate name to configure on the target system.'
    description <<-EOT
      This option allows you to specify an optional puppet agent
      certificate name to configure on the target system.  This option
      applies to the puppet-enterprise and puppet-enterprise-http
      installer scripts.  If provided, this option will replace any
      puppet agent certificate name provided in the puppet enterprise
      answers file.  This certificate name will show up in the console (or
      Puppet Dashboard) when the agent checks in for the first time.
    EOT
  end

  action.option '--install-script=' do
    summary 'The script to use when installing Puppet.'
    description <<-EOT
      Name of the installer script template to use when installing Puppet.
      The current list of supported scripts is:
        #{Puppet::CloudPack::Installer.find_builtin_templates.sort.join("\n            ")}
    EOT
    default_to { 'puppet-enterprise' }
    before_action do |action, arguments, options|
      # Check that the we can find the install script template.
      Puppet::CloudPack::Installer.find_template(options[:install_script])

      # Check that we have all the necessary inputs depending on the
      # install script specified.
      if options[:install_script].start_with?('puppet-enterprise')
        if options[:install_script].length == 'puppet-enterprise'.length
          # The canonical PE install script ('puppet-enterprise') needs both:
          # the installer payload and an answers file.
          unless options[:installer_payload] and options[:installer_answers]
            raise ArgumentError, 'Must specify installer payload and answers file if install script is puppet-enterprise'
          end
        elsif options[:install_script]['puppet-enterprise'.length, 1] == '-'
          # Other PE install scripts (those starting with 'puppet-enterprise-'),
          # need an answers file.
          unless options[:installer_answers]
            raise ArgumentError, "Must specify an answers file for install script #{options[:install_script]}"
          end
        end
      end
    end
  end

  action.option '--puppet-version=' do
    summary 'Version of Puppet to install.'
    description <<-EOT
      Version of Puppet to be installed. This version is
      passed to the Puppet installer script.
    EOT
    before_action do |action, arguments, options|
      unless options[:puppet_version] =~ /^(\d+)\.(\d+)(\.(\d+|x))?$|^(\d)+\.(\d)+\.(\d+)([a-zA-Z][a-zA-Z0-9-]*)|master$/
        raise ArgumentError, "Invaid Puppet version '#{options[:puppet_version]}'"
      end
    end
  end

  action.option '--facter-version=' do
    summary 'Version of facter to install.'
    description <<-EOT
      The version of facter that should be installed.
      This only makes sense in open source installation
      mode.
    EOT
    before_action do |action, arguments, options|
      unless options[:facter_version] =~ /\d+\.\d+\.\d+/
        raise ArgumentError, "Invaid Facter version '#{options[:facter_version]}'"
      end
    end
  end
end

.add_platform_option(action) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/puppet/cloudpack.rb', line 89

def add_platform_option(action)
  add_credential_option(action)

  action.option '--platform=' do
    summary 'Platform used to create machine instance (only supports AWS).'
    description <<-EOT
      The Cloud platform used to create new machine instances.
      Currently, AWS (Amazon Web Services) is the only supported platform.
    EOT

    default_to { 'AWS' }

    before_action do |action, args, options|
      supported_platforms = [ 'AWS' ]
      unless supported_platforms.include?(options[:platform])
        raise ArgumentError, "Platform must be one of the following: #{supported_platforms.join(', ')}"
      end
    end
  end
end

.add_region_option(action) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/puppet/cloudpack.rb', line 50

def add_region_option(action)
  action.option '--region=' do
    summary "The geographic region of the instance. Defaults to us-east-1."
    description <<-'EOT'
      The instance may run in any region EC2 operates within.  The regions at the
      time of this documentation are: US East (Northern Virginia), US West (Northern
      California), EU (Ireland), Asia Pacific (Singapore), and Asia Pacific (Tokyo).

      The region names for this command are: eu-west-1, us-east-1,
      ap-northeast-1, us-west-1, ap-southeast-1

      Note: to use another region, you will need to copy your keypair and reconfigure the
      security groups to allow SSH access.
    EOT

    default_to { 'us-east-1' }

    before_action do |action, args, options|

      regions_response = Puppet::CloudPack.create_connection(options).describe_regions
      region_names = regions_response.body["regionInfo"].collect { |r| r["regionName"] }.flatten
      unless region_names.include?(options[:region])
        raise ArgumentError, "Region must be one of the following: #{region_names.join(', ')}"
      end
    end
  end
end

.add_subnet_option(action) ⇒ Object



243
244
245
246
247
248
249
250
251
# File 'lib/puppet/cloudpack.rb', line 243

def add_subnet_option(action)
  action.option '--subnet=', '-s='  do
    summary "The subnet in which to deploy the VM (VPC only)"
    description <<-EOT
       This is the ID of the subnet in which you wish the vm to reside.
       This feature is only available when using EC2's VPC feature.
    EOT
  end
end

.add_tags_option(action) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/puppet/cloudpack.rb', line 155

def add_tags_option(action)
  action.option '--instance-tags=', '-t=' do
    summary 'The tags the instance should have in format tag1=value1,tag2=value2'
    description <<-EOT
      Instances may be tagged with custom tags. The tags should be in the
      format of tag1=value,tag2=value. Currently there is not way escape
      the ',' character so tags cannot contain this character.
    EOT

    before_action do |action, arguments, options|
      ## This converts 'this=that,foo=bar,biz=baz=also' to
      ## { 'this' => 'that', 'foo' => 'bar', 'biz' => 'baz=also'}
      ##
      ## A regex is needed that will allow us to escape ',' characters
      ## from the CLI
      begin
        options[:instance_tags] = Hash[ options[:instance_tags].split(',').map do |tag|
          tag_array = tag.split('=',2)
          if tag_array.size != 2
            raise ArgumentError, 'Could not parse tags given. Please check your format'
          end
          if [nil,''].include? tag_array[0] or [nil,''].include? tag_array[1]
            raise ArgumentError, 'Could not parse tags given. Please check your format'
          end
          tag_array
        end ]
      rescue
        raise ArgumentError, 'Could not parse tags given. Please check your format'
      end
    end

  end
end

.add_terminate_options(action) ⇒ Object



290
291
292
293
294
295
296
# File 'lib/puppet/cloudpack.rb', line 290

def add_terminate_options(action)
  add_platform_option(action)
  add_region_option(action)
  action.option '--force', '-f' do
    summary 'Forces termination of an instance.'
  end
end

.add_type_option(action) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/puppet/cloudpack.rb', line 206

def add_type_option(action)
  action.option '--type=' do
    summary 'Type of instance.'
    description <<-EOT
      Type of instance to be launched. The type specifies characteristics that
      a machine will have, such as architecture, memory, processing power, storage,
      and IO performance. The type selected will determine the cost of a machine instance.

      All instance types available from EC2 are supported; see the EC2 documentation
      online at http://aws.amazon.com/ec2/instance-types/ for details.
    EOT
    required
  end
end

.bootstrap(options) ⇒ Object



591
592
593
594
595
# File 'lib/puppet/cloudpack.rb', line 591

def bootstrap(options)
  server = self.create(options)
  self.init(server, options)
  return nil
end

.classify(certname, options) ⇒ Object



597
598
599
600
601
602
603
# File 'lib/puppet/cloudpack.rb', line 597

def classify(certname, options)
  if options[:node_group]
    dashboard_classify(certname, options)
  else
    Puppet.notice('No classification method selected')
  end
end

.compile_template(options) ⇒ Object



1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
# File 'lib/puppet/cloudpack.rb', line 1009

def compile_template(options)
  Puppet.notice "Installing Puppet ..."
  options[:server] = Puppet[:server]
  options[:environment] = Puppet[:environment] || 'production'

  install_script = Puppet::CloudPack::Installer.build_installer_template(options[:install_script], options)
  Puppet.debug("Compiled installer script:")
  Puppet.debug(install_script)

  # create a temp file to write compiled script
  # return the path of the temp location of the script
  begin
    f = Tempfile.open('install_script')
    f.write(install_script)
    f.path
  ensure
    f.close
  end
end

.create(options) ⇒ Object



667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
# File 'lib/puppet/cloudpack.rb', line 667

def create(options)
  unless options.has_key? :_destroy_server_at_exit
    options[:_destroy_server_at_exit] = :create
  end

  Puppet.info("Connecting to #{options[:platform]} #{options[:region]} ...")
  connection = create_connection(options)
  Puppet.info("Connecting to #{options[:platform]} #{options[:region]} ... Done")
  Puppet.info("Instance Type: #{options[:type]}")

  # TODO: Validate that the security groups permit SSH access from here.
  # TODO: Can this throw errors?
  server = create_server(connection.servers,
    :image_id           => options[:image],
    :key_name           => options[:keyname],
    :security_group_ids => options[:security_group],
    :flavor_id          => options[:type],
    :subnet_id          => options[:subnet],
    :availability_zone  => options[:availability_zone]
  )

  # This is the earliest point we have knowledge of the instance ID
  Puppet.info("Instance identifier: #{server.id}")

  Signal.trap(:EXIT) do
    if options[:_destroy_server_at_exit]
      server.destroy rescue nil
      Puppet.err("Destroyed server #{server.id} because of an abnormal exit")
    end
  end

  unless (options[:tags_not_supported])
    tags = {'Created-By' => 'Puppet'}
    tags.merge! options[:instance_tags] if options[:instance_tags]

    Puppet.notice('Creating tags for instance ... ')
    create_tags(connection.tags, server.id, tags)
    Puppet.notice('Creating tags for instance ... Done')
  end

  Puppet.notice("Launching server #{server.id} ...")
  retries = 0
  begin
    server.wait_for do
      print '#'
      raise Puppet::CloudPack::InstanceErrorState if self.state == 'error'
      self.ready?
    end
    puts
    Puppet.notice("Server #{server.id} is now launched")
  rescue Puppet::CloudPack::InstanceErrorState
    puts
    Puppet.err "Launching machine instance #{server.id} Failed."
    Puppet.err "Instance has entered an error state"
    return nil
  rescue Fog::Errors::Error
    Puppet.err "Launching server #{server.id} Failed."
    Puppet.err "Could not connect to host"
    Puppet.err "Please check your network connection and try again"
    return nil
  end

  # This is the earliest point we have knowledge of the DNS name
  Puppet.notice("Server #{server.id} public dns name: #{server.dns_name}")

  if options[:_destroy_server_at_exit] == :create
    options.delete(:_destroy_server_at_exit)
  end

  return server.dns_name
end

.create_connection(options = {}) ⇒ Object



1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
# File 'lib/puppet/cloudpack.rb', line 1055

def create_connection(options = {})
  # We don't support more than AWS, but this satisfies the rspec tests
  # that pass in a provider string that does not match 'AWS'.  This makes
  # the test pass by preventing Fog from throwing an error when the region
  # option is not expected
  Fog.credential = options[:credentials].to_sym if options[:credentials]
  case options[:platform]
  when 'AWS'
    # fog is smart emough to pass options to that are set to nil
    Fog::Compute.new(
      :provider => options[:platform],
      :region => options[:region],
      :endpoint => options[:endpoint]
    )
  else
    Fog::Compute.new(:provider => options[:platform])
  end
end

.create_server(servers, options = {}) ⇒ Object



1074
1075
1076
1077
1078
1079
# File 'lib/puppet/cloudpack.rb', line 1074

def create_server(servers, options = {})
  Puppet.notice('Creating new instance ...')
  server = servers.create(options)
  Puppet.notice("Creating new instance ... Done")
  return server
end

.create_tags(t_connection, resource_id, tags) ⇒ Object

Raises:

  • (ArgumentError)


1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
# File 'lib/puppet/cloudpack.rb', line 1081

def create_tags(t_connection, resource_id, tags)
  raise(ArgumentError, 'tags must be a hash') unless tags.is_a? Hash

    tags.each do |tag,value|
      Puppet.info("Creating tag for #{tag} ... ")
      Puppet::CloudPack::Utils.retry_action( :timeout => 120 ) do
        t_connection.create(
          :key         => tag,
          :value       => value,
          :resource_id => resource_id
        )
      end
    Puppet.info("Creating tag for #{tag} ... Done")
  end
end

.dashboard_classify(certname, options) ⇒ Object



605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
# File 'lib/puppet/cloudpack.rb', line 605

def dashboard_classify(certname, options)
  # The Puppet::Network::HTTP::Connection instance
  http = Puppet::Network::HttpPool.http_instance(options[:enc_server], options[:enc_port], true, !options[:insecure])

  Puppet.notice "Contacting https://#{options[:enc_server]}:#{options[:enc_port]}/ to classify #{certname}"

  # This block create the node and returns it to the caller
  notfound_register_the_node = lambda do
    data = { 'node' => { 'name' => certname } }
    http_request(http, '/nodes.json', options, 'Register Node', '201', data)
  end
  # Get a list of nodes to check if we need to create the node or not
  nodes = http_request(http, '/nodes.json', options, 'List nodes')
  # Find an existing node or register the node using the lambda block
  node = nodes.find(notfound_register_the_node) do |node|
    node['name'] == certname
  end
  node_id = node['id']

  # checking if the specified group even exists
  notfound_group_dne_error = lambda do
    raise Puppet::Error, "Group #{options[:node_group]} does not exist in the console/Dashboard. Groups must exist before they can be assigned to nodes."
  end
  node_groups = http_request(http, '/node_groups.json', options, 'List Groups')
  node_group_info = node_groups.find(notfound_group_dne_error) do |group|
    group['name'] == options[:node_group]
  end
  node_group_id = node_group_info['id']

  # Finally add the node to the group.
  notfound_associate_node = lambda do
    data = { 'node_name' => certname, 'group_name' => options[:node_group] }
    http_request(http, '/memberships.json', options, 'Classify node', '201', data)
  end

  memberships = http_request(http, '/memberships.json', options, 'List group members')
  response = memberships.find(notfound_associate_node) do |members|
    members['node_group_id'] == node_group_id and members['node_id'] == node_id
  end

  return { 'status' => 'complete' }
end

.do_in_progress_bar(options = {}, &blk) ⇒ Object

Take a block and a timeout and display a progress bar while we’re doing our thing



1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
# File 'lib/puppet/cloudpack.rb', line 1144

def do_in_progress_bar(options = {}, &blk)
  timeout = options[:timeout].to_i
  start_time = Time.now
  abort_time = start_time + timeout

  Puppet.notice "#{options[:notice]} (Started at #{start_time.strftime("%I:%M:%S %p")})"
  eta_msg = if (timeout <= 120) then
              "#{timeout} seconds at #{abort_time.strftime("%I:%M:%S %p")}"
            else
              "#{timeout / 60} minutes at #{abort_time.strftime("%I:%M %p")}"
            end
  Puppet.notice "Control will be returned to you in #{eta_msg} if #{options[:message].downcase} is unfinished."

  progress_bar = Puppet::CloudPack::ProgressBar.new(options[:message], timeout)
  progress_mutex = Mutex.new

  progress_thread = Thread.new do
    loop do
      progress = Time.now - start_time
      progress_mutex.synchronize { progress_bar.set progress }
      sleep 0.5
    end
  end

  block_return_value = nil
  begin
    Timeout.timeout(timeout) do
      block_return_value = blk.call
    end
  ensure
    progress_mutex.synchronize { progress_bar.finish; progress_thread.kill }
  end
  end_time = Time.now
  block_return_value
end

.fingerprint(server, options) ⇒ Object



773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
# File 'lib/puppet/cloudpack.rb', line 773

def fingerprint(server, options)
  connection = create_connection(options)
  servers = connection.servers.all('dns-name' => server)

  # Our hash for output.  We'll collect into this data structure.
  output_hash = {}
  output_array = servers.collect do |myserver|
    # TODO: Find a better way of getting the Fingerprints
    # The current method scrapes the AWS console looking for an ^ec2: pattern
    # This is not robust or ideal.  We make a "best effort" to find the fingerprint
    begin
      # Is there any console output yet?
      if myserver.console_output.body['output'].nil? then
        Puppet.info("Waiting for instance console output to become available ...")
        Fog.wait_for do
          print "#"
          not myserver.console_output.body['output'].nil?
        end or raise Fog::Errors::Error, "Waiting for console output timed out"
        puts "# Console output is ready"
      end
      # FIXME Where is the fingerprint?  Do we output it ever?
      { "#{myserver.id}" => myserver.console_output.body['output'].grep(/^ec2:/) }
    rescue Fog::Errors::Error => e
      Puppet.warning("Waiting for SSH host key fingerprint from #{options[:platform]} ... Failed")
      Puppet.warning "Could not read the host's fingerprints"
      Puppet.warning "Please verify the host's fingerprints through the AWS console output"
    end
  end
  output_array.each { |hsh| output_hash = hsh.merge(output_hash) }
  # Check to see if we got anything back
  if output_hash.collect { |k,v| v }.flatten.empty? then
    Puppet.warning "We could not securely find a fingerprint because the image did not print the fingerprint to the console."
    Puppet.warning "Please use an AMI that prints the fingerprint to the console in order to connect to the instance more securely."
    Puppet.info "The system is ready.  Please add the host key to your known hosts file."
    Puppet.info "For example: ssh root@#{server} and respond yes."
  end
  output_hash
end

.group_option_before_action(options) ⇒ Object

JJM This method is separated from the before_action block to aid testing.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/puppet/cloudpack.rb', line 111

def group_option_before_action(options)
  if not options[:security_group].is_a? Array
    options[:security_group] = options[:security_group].split(File::PATH_SEPARATOR)
  end

  known_by_id = {}
  known_by_name = {}
  Puppet::CloudPack.create_connection(options).security_groups.each do |g|
    known_by_id[g.group_id] = g
    known_by_name[g.name] = g
  end
  known = {}
  unknown = []
  options[:security_group].each do |g|
    # look up the group by its ID first, if not found then by name
    if sg = known_by_id[g] || sg = known_by_name[g]
      # store the group_id as a key in a hash to eliminate duplicates
      known[sg.group_id] = known.size unless known.include?(sg.group_id)
    else
      unknown << g
    end
  end
  unless unknown.empty?
    raise ArgumentError, "Unrecognized security groups: #{unknown.join(', ')}"
  end
  # now rebuild the security_group option argument array from the 'known' hash
  # so that every group in the array is specified by its ID, there are no
  # duplicates and the order of the groups in the array is the same as the order
  # in which the groups were specified on the command line
  options[:security_group] = known.keys.sort { |l, r| known[l] <=> known[r] }
end

.handle_json_response(response, action, expected_code = '200') ⇒ Object



648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
# File 'lib/puppet/cloudpack.rb', line 648

def handle_json_response(response, action, expected_code='200')
  if response.code == expected_code
    Puppet.info "#{action} ... Done"
    PSON.parse response.body
  else
    # I should probably raise an exception!
    Puppet.warning "#{action} ... Failed"
    Puppet.info("Body: #{response.body}")
    Puppet.warning "Server responded with a #{response.code} status"
    case response.code
    when /401/
      Puppet.notice "A 401 response is the HTTP code for an Unauthorized request"
      Puppet.notice "This error likely means you need to supply the --enc-auth-user and --enc-auth-passwd options"
      Puppet.notice "Alternatively, use the PUPPET_ENC_AUTH_PASSWD environment variable"
    end
    raise Puppet::Error, "Could not: #{action}, got #{response.code} expected #{expected_code}"
  end
end

.http_request(http, path, options = {}, action = nil, expected_code = '200', data = nil) ⇒ Object

Method to make generic, SSL, Authenticated HTTP requests and parse the JSON response. Primarily for #10377 and #10197



1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
# File 'lib/puppet/cloudpack.rb', line 1113

def http_request(http, path, options = {}, action = nil, expected_code = '200', data = nil)
  headers = HttpHeaders.new
  # Authentication information
  headers.basic_auth(options[:enc_auth_user], options[:enc_auth_passwd]) unless options[:enc_auth_user].nil?
  # Content Type of the request
  headers.set_content_type('application/json')
  # Convert the headers to a plain hash
  headers = headers.to_hash

  # Prepare the arguments of the request call
  args = data.nil? \
    ? [:get, path, headers] \
    : [:post, path, data.to_pson, headers]

  # Wrap the request in an exception handler
  begin
    response = http.request(*args)
  rescue Errno::ECONNREFUSED => e
    Puppet.warning 'Registering node ... Error'
    Puppet.err "Could not connect to host #{options[:enc_server]} on port #{options[:enc_port]}"
    Puppet.err "This could be because a local host firewall is blocking the connection"
    Puppet.err "Please check your --enc-server and --enc-port options"
    ex = Puppet::Error.new(e)
    ex.set_backtrace(e.backtrace)
    raise ex
  end
  # Return the parsed JSON response
  handle_json_response(response, action || path, expected_code)
end

.init(server, options) ⇒ Object



812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
# File 'lib/puppet/cloudpack.rb', line 812

def init(server, options)
  install_status = install(server, options)
  certname = install_status['puppetagent_certname']
  options.delete(:_destroy_server_at_exit)

  Puppet.notice "Puppet is now installed on: #{server}"

  classify(certname, options)

  # HACK: This should be reconciled with the Certificate Face.
  cert_options = {:ca_location => :remote}

  # TODO: Wait for C.S.R.?

  Puppet.notice "Signing certificate ..."
  begin
    Puppet::Face[:certificate, '0.0.1'].sign(certname, cert_options)
    Puppet.notice "Signing certificate ... Done"
  rescue Puppet::Error => e
    # TODO: Write useful next steps.
    Puppet.err "Signing certificate ... Failed"
    Puppet.err "Signing certificate error: #{e}"
    exit(1)
  rescue Net::HTTPError => e
    # TODO: Write useful next steps
    Puppet.warning "Signing certificate ... Failed"
    Puppet.err "Signing certificate error: #{e}"
    exit(1)
  end
end

.install(server, options) ⇒ Object



843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
# File 'lib/puppet/cloudpack.rb', line 843

def install(server, options)

  # If the end user wants to use their agent, we need to set keyfile to nil
  if options[:keyfile] == 'agent' then
    options[:keyfile] = nil
  end

  #Figure out our puppetagent-certname value
  if not options[:puppetagent_certname]
    options[:puppetagent_certname] = "#{server}-#{Guid.new.to_s}"
    options[:autogenerated_certname] = true
  end

  # Figure out if we need to be root
  cmd_prefix = options[:login] == 'root' ? '' : 'sudo '

  # FIXME: This appears to be an AWS assumption.  What about VMware with a plain IP?
  # (Not necessarily a bug, just a yak to shave...)
  options[:public_dns_name] = server

  # FIXME We shouldn't try to connect if the answers file hasn't been provided
  # for the installer script matching puppet-enterprise-* (e.g. puppet-enterprise-s3)
  connections = ssh_connect(server, options[:login], options[:keyfile])

  options[:tmp_dir] = File.join('/', 'tmp', Guid.new.to_s)
  create_tmpdir_cmd = "bash -c 'umask 077; mkdir #{options[:tmp_dir]}'"
  ssh_remote_execute(server, options[:login], create_tmpdir_cmd, options[:keyfile])

  upload_payloads(connections[:scp], options)

  tmp_script_path = compile_template(options)

  remote_script_path = File.join(options[:tmp_dir], "#{options[:install_script]}.sh")
  connections[:scp].upload(tmp_script_path, remote_script_path)

  # Finally, execute the installer script
  install_command = "#{cmd_prefix}bash -c 'chmod u+x #{remote_script_path}; #{remote_script_path}'"
  results = ssh_remote_execute(server, options[:login], install_command, options[:keyfile])
  if results[:exit_code] != 0 then
    raise Puppet::Error, "The installer script exited with a non-zero exit status, indicating a failure.  It may help to run with --debug to see the script execution or to check the installation log file on the remote system in #{options[:tmp_dir]}."
  end

  # At this point we may assume installation of Puppet succeeded since the
  # install script returned with a zero exit code.

  # Determine the certificate name as reported by the remote system.
  certname_command = "#{cmd_prefix}puppet agent --configprint certname"
  results = ssh_remote_execute(server, options[:login], certname_command, options[:keyfile])

  if results[:exit_code] == 0 then
    puppetagent_certname = results[:stdout].strip
  else
    Puppet.warning "Could not determine the remote puppet agent certificate name using #{certname_command}"
    puppetagent_certname = nil
  end

  # Return value
  {
    'status'               => 'success',
    'puppetagent_certname' => puppetagent_certname,
    'stdout'               => results[:stdout],
  }
end

.list(options) ⇒ Object



754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
# File 'lib/puppet/cloudpack.rb', line 754

def list(options)
  connection = create_connection(options)
  servers = connection.servers
  # Convert the Fog object into a simple hash.
  # And return the array to the Faces API for rendering
  hsh = {}
  servers.each do |s|
    hsh[s.id] = {
      "id"         => s.id,
      "state"      => s.state,
      "keyname"    => s.key_name,
      "dns_name"   => s.dns_name,
      "created_at" => s.created_at,
      "tags"       => s.tags.inspect
    }
  end
  hsh
end

.list_keynames(options = {}) ⇒ Object



739
740
741
742
743
744
745
746
747
748
749
750
751
752
# File 'lib/puppet/cloudpack.rb', line 739

def list_keynames(options = {})
  connection = create_connection(options)
  keys_array = connection.key_pairs.collect do |key|
    key.attributes.inject({}) { |memo,(k,v)| memo[k.to_s] = v; memo }
  end
  # Covert the array into a Hash
  keys_hash = Hash.new
  keys_array.each { |key| keys_hash.merge!({key['name'] => key['fingerprint']}) }
  # Get a sorted list of the names
  sorted_names = keys_hash.keys.sort
  sorted_names.collect do |name|
    { 'name' => name, 'fingerprint' => keys_hash[name] }
  end
end

.payload_type(payload) ⇒ Object



1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
# File 'lib/puppet/cloudpack.rb', line 1097

def payload_type(payload)
  uri = begin
    URI.parse(payload)
  rescue URI::InvalidURIError => e
    return :invalid
  end
  if uri.class.to_s =~ /URI::(FTP|HTTPS?)/
    $1.downcase.to_sym
  else
    # assuming that everything else is a valid filepath
    :file_path
  end
end

.ssh_connect(server, login, keyfile = nil) ⇒ Object



982
983
984
985
986
987
988
989
990
991
992
993
# File 'lib/puppet/cloudpack.rb', line 982

def ssh_connect(server, , keyfile = nil)
  opts = {}
  # This allows SSH_AUTH_SOCK agent usage if keyfile is nil
  opts[:key_data] = [File.read(File.expand_path(keyfile))] if keyfile

  ssh_test_connect(server, , keyfile)

  ssh = Fog::SSH.new(server, , opts)
  scp = Fog::SCP.new(server, , opts)

  {:ssh => ssh, :scp => scp}
end

.ssh_remote_execute(server, login, command, keyfile = nil) ⇒ Object

This is the single place to make SSH calls. It will handle collecting STDOUT in a line oriented manner, printing it to debug log destination and checking the exit code of the remote call. This should also make it much easier to do unit testing on all of the other methods that need this functionality. Finally, it should provide one place to swap out the back end SSH implementation if need be.



912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
# File 'lib/puppet/cloudpack.rb', line 912

def ssh_remote_execute(server, , command, keyfile = nil)
  Puppet.info "Executing remote command ..."
  Puppet.debug "Command: #{command}"
  buffer = String.new
  stdout = String.new
  exit_code = nil
  # Figure out the options we need to pass to start.  This allows us to use SSH_AUTH_SOCK
  # if the end user specifies --keyfile=agent
  ssh_opts = keyfile ? { :keys => [ keyfile ] } : { }
  # Start
  begin
    Net::SSH.start(server, , ssh_opts) do |session|
      session.open_channel do |channel|
        channel.request_pty
        channel.on_data do |ch, data|
          buffer << data
          stdout << data
          if buffer =~ /\n/
            lines = buffer.split("\n")
            buffer = lines.length > 1 ? lines.pop : String.new
            lines.each do |line|
              Puppet.debug(line)
            end
          end
        end
        channel.on_eof do |ch|
          # Display anything remaining in the buffer
          unless buffer.empty?
            Puppet.debug(buffer)
          end
        end
        channel.on_request("exit-status") do |ch, data|
          exit_code = data.read_long
          Puppet.debug("SSH Command Exit Code: #{exit_code}")
        end
        # Finally execute the command
        channel.exec(command)
      end
    end
  rescue Net::SSH::AuthenticationFailed => user
    raise Net::SSH::AuthenticationFailed, "Authentication failure for user #{user}. Please check the keyfile and try again."
  end

  Puppet.info "Executing remote command ... Done"
  { :exit_code => exit_code, :stdout => stdout }
end

.ssh_test_connect(server, login, keyfile = nil) ⇒ Object



959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
# File 'lib/puppet/cloudpack.rb', line 959

def ssh_test_connect(server, , keyfile = nil)
  Puppet.notice "Waiting for SSH response ..."

  retry_exceptions = {
      Net::SSH::AuthenticationFailed => "Failed to connect. This may be because the machine is booting.\nRetrying the connection...",
      Errno::EHOSTUNREACH            => "Failed to connect. This may be because the machine is booting.  Retrying the connection..",
      Errno::ECONNREFUSED            => "Failed to connect. This may be because the machine is booting.  Retrying the connection...",
      Errno::ETIMEDOUT               => "Failed to connect. This may be because the machine is booting.  Retrying the connection..",
      Errno::ECONNRESET              => "Connection reset. Retrying the connection...",
      Timeout::Error                 => "Connection test timed-out. This may be because the machine is booting.  Retrying the connection...",
      Errno::ENETUNREACH             => "Network unreachable.  Retrying the connection...",
  }

  Puppet::CloudPack::Utils.retry_action( :timeout => 250, :retry_exceptions => retry_exceptions ) do
    Timeout::timeout(25) do
      ssh_remote_execute(server, , "date", keyfile)
    end
  end

  Puppet.notice "Waiting for SSH response ... Done"
  true
end

.terminate(server, options) ⇒ Object



1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
# File 'lib/puppet/cloudpack.rb', line 1029

def terminate(server, options)
  # set the default id used for termination to dns_name
  options[:terminate_id] ||= 'dns-name'

  Puppet.info "Connecting to #{options[:platform]} ..."
  connection = create_connection(options)
  Puppet.info "Connecting to #{options[:platform]} ... Done"

  servers = connection.servers.all(options[:terminate_id] => server)
  if servers.length == 1 || options[:force]
    # We're using myserver rather than server to prevent ruby 1.8 from
    # overwriting the server method argument
    servers.each do |myserver|
      Puppet.notice "Destroying #{myserver.id} (#{myserver.dns_name}) ..."
      myserver.destroy()
      Puppet.notice "Destroying #{myserver.id} (#{myserver.dns_name}) ... Done"
    end
  elsif servers.empty?
    Puppet.warning "Could not find server with DNS name '#{server}'"
  else
    Puppet.err "More than one server with DNS name '#{server}'; aborting"
  end

  return nil
end

.upload_payloads(scp, options) ⇒ Object



995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
# File 'lib/puppet/cloudpack.rb', line 995

def upload_payloads(scp, options)
  if options[:installer_payload] and payload_type(options[:installer_payload]) == :file_path
    Puppet.notice "Uploading Puppet Enterprise tarball ..."
    scp.upload(options[:installer_payload], "#{options[:tmp_dir]}/puppet.tar.gz")
    Puppet.notice "Uploading Puppet Enterprise tarball ... Done"
  end

  if options[:installer_answers]
    Puppet.info "Uploading Puppet Answer File ..."
    scp.upload(options[:installer_answers], "#{options[:tmp_dir]}/puppet.answers")
    Puppet.info "Uploading Puppet Answer File ... Done"
  end
end