elevage 0.1.0

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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +21 -0
  4. data/.rubocop.yml +8 -0
  5. data/.ruby-version +1 -0
  6. data/.simplecov +8 -0
  7. data/.travis.yml +3 -0
  8. data/.yardoc/checksums +12 -0
  9. data/.yardoc/object_types +0 -0
  10. data/.yardoc/objects/root.dat +0 -0
  11. data/.yardoc/proxy_types +0 -0
  12. data/Gemfile +4 -0
  13. data/Guardfile +10 -0
  14. data/LICENSE.txt +203 -0
  15. data/README.md +112 -0
  16. data/Rakefile +20 -0
  17. data/bin/elevage +4 -0
  18. data/coverage/.resultset.json.lock +0 -0
  19. data/doc/Elevage/Build.html +435 -0
  20. data/doc/Elevage/CLI.html +282 -0
  21. data/doc/Elevage/Environment.html +950 -0
  22. data/doc/Elevage/Generate.html +346 -0
  23. data/doc/Elevage/Health.html +359 -0
  24. data/doc/Elevage/New.html +411 -0
  25. data/doc/Elevage/Platform.html +1119 -0
  26. data/doc/Elevage/Provisioner.html +804 -0
  27. data/doc/Elevage/ProvisionerRunQueue.html +765 -0
  28. data/doc/Elevage/Runner.html +319 -0
  29. data/doc/Elevage.html +501 -0
  30. data/doc/_index.html +239 -0
  31. data/doc/class_list.html +58 -0
  32. data/doc/css/common.css +1 -0
  33. data/doc/css/full_list.css +57 -0
  34. data/doc/css/style.css +339 -0
  35. data/doc/file.README.html +187 -0
  36. data/doc/file_list.html +60 -0
  37. data/doc/frames.html +26 -0
  38. data/doc/index.html +187 -0
  39. data/doc/js/app.js +219 -0
  40. data/doc/js/full_list.js +181 -0
  41. data/doc/js/jquery.js +4 -0
  42. data/doc/method_list.html +369 -0
  43. data/doc/top-level-namespace.html +112 -0
  44. data/elevage.gemspec +39 -0
  45. data/features/archive +314 -0
  46. data/features/build.feature +237 -0
  47. data/features/elevage.feature +24 -0
  48. data/features/generate.feature +235 -0
  49. data/features/health_env_failure.feature +292 -0
  50. data/features/health_failure.feature +291 -0
  51. data/features/health_success.feature +279 -0
  52. data/features/list.feature +315 -0
  53. data/features/new.feature +68 -0
  54. data/features/step_definitions/elevage_steps.rb +27 -0
  55. data/features/support/env.rb +9 -0
  56. data/lib/elevage/build.rb +109 -0
  57. data/lib/elevage/constants.rb +113 -0
  58. data/lib/elevage/environment.rb +223 -0
  59. data/lib/elevage/generate.rb +48 -0
  60. data/lib/elevage/health.rb +27 -0
  61. data/lib/elevage/new.rb +30 -0
  62. data/lib/elevage/platform.rb +105 -0
  63. data/lib/elevage/provisioner.rb +169 -0
  64. data/lib/elevage/provisionerrunqueue.rb +114 -0
  65. data/lib/elevage/runner.rb +39 -0
  66. data/lib/elevage/templates/compute.yml.tt +18 -0
  67. data/lib/elevage/templates/environment.yml.tt +20 -0
  68. data/lib/elevage/templates/network.yml.tt +16 -0
  69. data/lib/elevage/templates/platform.yml.tt +110 -0
  70. data/lib/elevage/templates/vcenter.yml.tt +77 -0
  71. data/lib/elevage/version.rb +4 -0
  72. data/lib/elevage.rb +45 -0
  73. data/spec/spec_helper.rb +4 -0
  74. metadata +357 -0
@@ -0,0 +1,223 @@
1
+ require 'yaml'
2
+ require 'resolv'
3
+
4
+ require_relative 'constants'
5
+ require_relative 'platform'
6
+ require_relative 'provisioner'
7
+
8
+ # rubocop:disable ClassLength
9
+ module Elevage
10
+ # Environment class
11
+ class Environment
12
+ attr_accessor :name
13
+ attr_accessor :vcenter
14
+ attr_accessor :components
15
+ attr_accessor :nodenameconvention
16
+
17
+ # rubocop:disable LineLength
18
+ def initialize(env)
19
+ # Confirm environment has been defined in the platform
20
+ platform = Elevage::Platform.new
21
+ fail(IOError, ERR[:env_not_defined]) unless platform.environments.include?(env)
22
+ # Confirm environment file exists
23
+ envfile = ENV_FOLDER + env.to_s + '.yml'
24
+ fail(IOError, ERR[:no_env_file]) unless env_file_exists?(envfile)
25
+ # Build environment hash from environment and platform defintion files
26
+ environment = build_env(env, YAML.load_file(envfile).fetch('environment'), platform)
27
+ # Populate class variables
28
+ @name = env
29
+ @vcenter = environment['vcenter']
30
+ @components = environment['components']
31
+ @nodenameconvention = platform.nodenameconvention
32
+ end
33
+ # rubocop:enable LineLength
34
+
35
+ # Public: Environment class method
36
+ # Returns multiline string = IP, fqdn, runlist
37
+ def list_nodes
38
+ nodes = @vcenter['destfolder'].to_s + "\n"
39
+ @components.each do |component, _config|
40
+ (1..@components[component]['count']).each do |i|
41
+ nodes += @components[component]['addresses'][i - 1].ljust(18, ' ') +
42
+ node_name(component, i) + @vcenter['domain'] + ' ' +
43
+ @components[component]['runlist'].to_s + "\n"
44
+ end
45
+ end
46
+ nodes
47
+ end
48
+
49
+ # rubocop:disable MethodLength, LineLength, CyclomaticComplexity, PerceivedComplexity
50
+ def healthy?
51
+ platform = Elevage::Platform.new
52
+ health = ''
53
+ health += MSG[:invalid_env_vcenter] if @vcenter.nil?
54
+ @components.each do |component, v|
55
+ health += MSG[:invalid_env_network] if v['network'].nil?
56
+ health += MSG[:invalid_env_count] unless (0..POOL_LIMIT).member?(v['count'])
57
+ health += MSG[:invalid_env_compute] if v['compute'].nil?
58
+ health += MSG[:invalid_env_ip] if v['count'] != v['addresses'].size
59
+ if v['addresses'].nil?
60
+ health += MSG[:invalid_env_ip]
61
+ else
62
+ v['addresses'].each { |ip| health += MSG[:invalid_env_ip] unless Resolv::IPv4::Regex.match(ip) }
63
+ end
64
+ health += MSG[:invalid_env_tier] unless platform.tiers.include?(v['tier'])
65
+ health += MSG[:invalid_env_image] if v['image'].nil?
66
+ health += MSG[:invalid_env_port] unless v['port'].is_a?(Integer) || v['port'].nil?
67
+ health += MSG[:invalid_env_runlist] if v['runlist'].nil? || v['runlist'].empty?
68
+ health += MSG[:invalid_env_componentrole] unless v['componentrole'].include?('#') if v['componentrole']
69
+ health += MSG[:env_component_mismatch] unless platform.components.include?(component)
70
+ end
71
+ health += MSG[:env_component_mismatch] unless platform.components.size == @components.size
72
+ if health.length > 0
73
+ puts health + "\n#{health.lines.count} environment offense(s) detected"
74
+ false
75
+ else
76
+ true
77
+ end
78
+ end
79
+ # rubocop:enable MethodLength, LineLength, CyclomaticComplexity, PerceivedComplexity
80
+
81
+ # Public: basic class puts string output
82
+ def to_s
83
+ puts @name
84
+ puts @vcenter.to_yaml
85
+ puts @components.to_yaml
86
+ puts @nodenameconvention.to_yaml
87
+ end
88
+
89
+ # Public: method to request provisioning of all or a portion
90
+ # of the environment
91
+ # rubocop:disable MethodLength, LineLength, CyclomaticComplexity, PerceivedComplexity
92
+ def provision(type: all, tier: nil, component: nil, instance: nil, options: nil)
93
+ # Create the ProvisionerRunQueue to batch up our tasks
94
+ runner = ProvisionerRunQueue.new
95
+
96
+ # Modify behavior for dry-run (no concurrency)
97
+ if !options['dry-run']
98
+ runner.max_concurrent = options[:concurrency]
99
+ else
100
+ puts "Dry run requested, forcing concurrency to '1'."
101
+ runner.max_concurrent = 1
102
+ end
103
+
104
+ @components.each do |component_name, component_data|
105
+ next unless type.eql?(:all) || component_data['tier'].match(/#{tier}/i) && component_name.match(/#{component}/i)
106
+
107
+ 1.upto(component_data['addresses'].count) do |component_instance|
108
+ next unless instance == component_instance || instance.nil?
109
+
110
+ instance_name = node_name(component_name, component_instance)
111
+
112
+ # Create the Provisioner
113
+ provisioner = Elevage::Provisioner.new(instance_name, component_data, component_instance, self, options)
114
+
115
+ # Add it to the queue
116
+ runner.provisioners << provisioner
117
+
118
+ end
119
+ end
120
+
121
+ runner.to_s if options['dry-run']
122
+
123
+ # Process the queue
124
+ runner.run
125
+ end
126
+ # rubocop:enable MethodLength, LineLength, CyclomaticComplexity, PerceivedComplexity
127
+
128
+ private
129
+
130
+ # Private: updates env hash with necessary info from Platform files.
131
+ # This is a blend of env and Platform info needed to construct
132
+ # Environment class object
133
+ #
134
+ # Params
135
+ # env: string passed from commend line, simple environment name
136
+ # env_yaml: hash from requested environment.yml
137
+ # platform: Platform class object built from standard
138
+ # platform definition files
139
+ #
140
+ # Returns Hash: updated env_yaml hash
141
+ # rubocop:disable MethodLength, LineLength
142
+ def build_env(env, env_yaml, platform)
143
+ # substitute vcenter resources from vcenter.yml for location defined in environment file
144
+ env_yaml['vcenter'] = platform.vcenter[env_yaml['vcenter']]
145
+ # merge component resources from environment file and platform definition
146
+ # platform info will be inserted where not found in env files, env overrides will be unchanged
147
+ #
148
+ # Note: this function does not do error checking for components that exist in env file but
149
+ # not in Platform definition. Such files will be retained but not have any Platform
150
+ # component info. The Build command will run error checking before building, but to support
151
+ # the debugging value of the list command only hash.merge! is performed at this point.
152
+ platform.components.each do |component, _config|
153
+ env_yaml['components'][component].merge!(platform.components[component]) { |_key, v1, _v2| v1 } unless env_yaml['components'][component].nil?
154
+ end
155
+ # substitute network and components for specified values from platform definition files
156
+ env_yaml['components'].each do |component, _config|
157
+ env_yaml['components'][component]['network'] = platform.network[env_yaml['components'][component]['network']]
158
+ env_yaml['components'][component]['compute'] = platform.compute[env_yaml['components'][component]['compute']]
159
+ unless env_yaml['components'][component]['runlist'].nil?
160
+ env_yaml['components'][component]['runlist'] = run_list(env_yaml['components'][component]['runlist'], env_yaml['components'][component]['componentrole'], component)
161
+ end
162
+ end
163
+ unless env_yaml['vcenter'].nil?
164
+ # append env name to destination folder if appendenv == true
165
+ env_yaml['vcenter']['destfolder'] += (env_yaml['vcenter']['appendenv'] ? '/' + env.to_s : '')
166
+ # prepend app name to domain if appenddomain == true
167
+ env_yaml['vcenter']['appenddomain'] ? env_yaml['vcenter']['domain'] = '.' + platform.name + '.' + env_yaml['vcenter']['domain'] : ''
168
+ end
169
+ env_yaml
170
+ end
171
+ # rubocop:enable MethodLength, LineLength
172
+
173
+ # Private: construct a node hostname from parameters
174
+ #
175
+ # Params
176
+ # component: Hash, environment components
177
+ # instance: integer, passed from loop iterator
178
+ #
179
+ # Returns hostname as String
180
+ # rubocop:disable MethodLength
181
+ def node_name(component, instance)
182
+ name = ''
183
+ @nodenameconvention.each do |i|
184
+ case i
185
+ when 'environment'
186
+ name += @name
187
+ when 'component'
188
+ name += component
189
+ when 'instance'
190
+ name += instance.to_s.rjust(2, '0')
191
+ when 'geo'
192
+ name += @vcenter['geo'].to_s[0]
193
+ else
194
+ name += i
195
+ end
196
+ end
197
+ name
198
+ end
199
+ # rubocop:enable MethodLength
200
+
201
+ # Private: Constructs the node runlist from parameters
202
+ #
203
+ # Params
204
+ # list: Array of strings from component runlist hash key value
205
+ # componentrole: String value from component, performs simple
206
+ # string substitution of for component string
207
+ # in component role string
208
+ # component: String, component name
209
+ #
210
+ # Returns runlist as String
211
+ # rubocop:disable LineLength
212
+ def run_list(list, componentrole, component)
213
+ list.join(',') + (componentrole ? ',' + componentrole.gsub('#', component) : '')
214
+ end
215
+ # rubocop:enable LineLength
216
+
217
+ def env_file_exists?(env_file)
218
+ fail(IOError, ERR[:no_environment_file]) unless File.file?(env_file)
219
+ true
220
+ end
221
+ end
222
+ end
223
+ # rubocop:enable ClassLength
@@ -0,0 +1,48 @@
1
+ require 'thor/group'
2
+
3
+ module Elevage
4
+ # Create new environment desired state files from platform template
5
+ class Generate < Thor::Group
6
+ include Thor::Actions
7
+ argument :env
8
+
9
+ def self.source_root
10
+ File.dirname(__FILE__)
11
+ end
12
+
13
+ # rubocop:disable MethodLength, CyclomaticComplexity, PerceivedComplexity
14
+ def create_environment
15
+ fail IOError, ERR[:env_exists] if File.file?(ENV_FOLDER + env + '.yml')
16
+ platform = Elevage::Platform.new
17
+ platformfile = File.open(YML_PLATFORM, 'r')
18
+ #
19
+ # The things from here forward I would rather have in the template file
20
+ # but that is even uglier, trying to get formatting correct
21
+ # will need to investigate some POWER erb skills to clean this up
22
+ @env_pools = ''
23
+ @env_components = ''
24
+ line = ''
25
+ line = platformfile.gets until line =~ /pools/
26
+ platform.pools.each do |k, _v|
27
+ line = platformfile.gets until line.include?(k)
28
+ @env_pools += line
29
+ next_line = platformfile.gets
30
+ @env_pools += "#{next_line}" if next_line.include?('<<')
31
+ @env_pools += " network:\n\n"
32
+ end
33
+ line = platformfile.gets until line =~ /components/
34
+ platform.components.each do |k, v|
35
+ line = platformfile.gets until line.include?(k)
36
+ @env_components += line
37
+ next_line = platformfile.gets
38
+ @env_components += "#{next_line}" if next_line.include?('<<')
39
+ @env_components += " addresses:\n"
40
+ (1..v['count']).each { @env_components += " -\n" }
41
+ @env_components += "\n"
42
+ end
43
+ template(TEMPLATE_ENV, ENV_FOLDER + env + '.yml')
44
+ puts "#{env}.yml added in environments folder"
45
+ end
46
+ # rubocop:enable MethodLength, CyclomaticComplexity, PerceivedComplexity
47
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ require 'thor/group'
2
+
3
+ module Elevage
4
+ # Evaluate health of platform definition files
5
+ class Health < Thor::Group
6
+ include Thor::Actions
7
+
8
+ def self.source_root
9
+ File.dirname(__FILE__)
10
+ end
11
+
12
+ # rubocop:disable LineLength
13
+ def check_platform
14
+ @platform = Elevage::Platform.new
15
+ puts @platform.healthy? ? MSG_HEALTHY : fail(IOError, ERR[:fail_health_check])
16
+ end
17
+ # rubocop:enable LineLength
18
+
19
+ # rubocop:disable LineLength
20
+ def check_environments
21
+ @platform.environments.each do |env|
22
+ puts Elevage::Environment.new(env).healthy? ? (env + MSG_ENV_HEALTHY) : fail(IOError, ERR[:fail_health_check])
23
+ end
24
+ end
25
+ # rubocop:enable LineLength
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ require 'thor/group'
2
+
3
+ module Elevage
4
+ # Create new platform definition files and environments folder structure
5
+ class New < Thor::Group
6
+ include Thor::Actions
7
+
8
+ argument :platform
9
+
10
+ def self.source_root
11
+ File.dirname(__FILE__)
12
+ end
13
+
14
+ # Confirm command is not being run in folder with
15
+ # existing platform definition
16
+ def already_exists?
17
+ File.file?(YML_PLATFORM) && fail(IOError, ERR[:platform_exists])
18
+ end
19
+
20
+ def create_platform_file
21
+ template(TEMPLATE_PLATFORM, YML_PLATFORM)
22
+ end
23
+
24
+ def create_infrastructure_files
25
+ template(TEMPLATE_VCENTER, YML_VCENTER)
26
+ template(TEMPLATE_NETWORK, YML_NETWORK)
27
+ template(TEMPLATE_COMPUTE, YML_COMPUTE)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,105 @@
1
+ # rubocop:disable LineLength
2
+ require 'yaml'
3
+ require 'resolv'
4
+ require 'English'
5
+
6
+ module Elevage
7
+ # Platform class
8
+ class Platform
9
+ attr_accessor :name, :description
10
+ attr_accessor :environments
11
+ attr_accessor :tiers
12
+ attr_accessor :nodenameconvention
13
+ attr_accessor :pools
14
+ attr_accessor :components
15
+ attr_accessor :vcenter
16
+ attr_accessor :network
17
+ attr_accessor :compute
18
+
19
+ # rubocop:disable MethodLength
20
+ def initialize
21
+ fail unless platform_files_exists?
22
+ platform = YAML.load_file(YML_PLATFORM).fetch('platform')
23
+ @name = platform['name']
24
+ @description = platform['description']
25
+ @environments = platform['environments']
26
+ @tiers = platform['tiers']
27
+ @nodenameconvention = platform['nodenameconvention']
28
+ @pools = platform['pools']
29
+ @components = platform['components']
30
+ @vcenter = YAML.load_file(YML_VCENTER).fetch('vcenter')
31
+ @network = YAML.load_file(YML_NETWORK).fetch('network')
32
+ @compute = YAML.load_file(YML_COMPUTE).fetch('compute')
33
+ end
34
+ # rubocop:enable MethodLength
35
+
36
+ # rubocop:disable MethodLength, LineLength, CyclomaticComplexity, PerceivedComplexity, AmbiguousOperator
37
+ def healthy?
38
+ health = ''
39
+ # Array of string checked for empty values
40
+ health += MSG[:empty_environments] unless @environments.all?
41
+ health += MSG[:empty_tiers] unless @tiers.all?
42
+ health += MSG[:empty_nodenameconvention] unless @nodenameconvention.all?
43
+ # Loop through all pool definitions, check for valid settings
44
+ @pools.each do |_pool, v|
45
+ health += MSG[:pool_count_size] unless (0..POOL_LIMIT).member?(v['count'])
46
+ health += MSG[:invalid_tiers] unless @tiers.include?(v['tier'])
47
+ health += MSG[:no_image_ref] if v['image'].nil?
48
+ health += MSG[:invalid_compute] unless @compute.key?(v['compute'])
49
+ health += MSG[:invalid_port] if v['port'].nil?
50
+ health += MSG[:invalid_runlist] unless v['runlist'].all?
51
+ health += MSG[:invalid_componentrole] unless v['componentrole'].include?('#') if v['componentrole']
52
+ end
53
+ # Loop through all vcenter definitions, check for valid settings
54
+ @vcenter.each do |_vcenter, v|
55
+ health += MSG[:invalid_geo] if v['geo'].nil?
56
+ health += MSG[:invalid_timezone] unless (0..TIMEZONE_LIMIT).member?(v['timezone'].to_i)
57
+ health += MSG[:invalid_host] if v['host'].nil?
58
+ health += MSG[:invalid_datacenter] if v['datacenter'].nil?
59
+ health += MSG[:invalid_imagefolder] if v['imagefolder'].nil?
60
+ health += MSG[:invalid_destfolder] if v['destfolder'].nil?
61
+ health += MSG[:invalid_appendenv] unless v['appendenv'] == true || v['appendenv'] == false
62
+ health += MSG[:invalid_appenddomain] unless v['appenddomain'] == true || v['appenddomain'] == false
63
+ health += MSG[:empty_datastores] unless v['datastores'].all?
64
+ health += MSG[:invalid_domain] if v['domain'].nil?
65
+ v['dnsips'].each { |ip| health += MSG[:invalid_ip] unless Resolv::IPv4::Regex.match(ip) }
66
+ end
67
+ # Loop through all network definitions, check for valid settings
68
+ @network.each do |_network, v|
69
+ health += MSG[:empty_network] if v.values.any? &:nil?
70
+ health += MSG[:invalid_gateway] unless Resolv::IPv4::Regex.match(v['gateway'])
71
+ end
72
+ # Loop through all compute definitions, check for valid settings
73
+ @compute.each do |_compute, v|
74
+ health += MSG[:invalid_cpu] unless (0..CPU_LIMIT).member?(v['cpu'])
75
+ health += MSG[:invalid_ram] unless (0..RAM_LIMIT).member?(v['ram'])
76
+ end
77
+ if health.length > 0
78
+ puts health + "\n#{health.lines.count} platform offense(s) detected"
79
+ false
80
+ else
81
+ true
82
+ end
83
+ end
84
+ # rubocop:enable MethodLength, LineLength, CyclomaticComplexity, PerceivedComplexity, AmbiguousOperator
85
+
86
+ private
87
+
88
+ # Private: confirms existence of the standard platform definition files
89
+ # Returns true if all standard files present
90
+ def platform_files_exists?
91
+ fail(IOError, ERR[:no_platform_file]) unless File.file?(YML_PLATFORM)
92
+ fail(IOError, ERR[:no_vcenter_file]) unless File.file?(YML_VCENTER)
93
+ fail(IOError, ERR[:no_network_file]) unless File.file?(YML_NETWORK)
94
+ fail(IOError, ERR[:no_compute_file]) unless File.file?(YML_COMPUTE)
95
+ true
96
+ end
97
+
98
+ # Unimplemented - part of future Communication health check option
99
+ # def valid_vcenter_host?(address)
100
+ # _result = `ping -q -c 3 #{address}`
101
+ # $CHILD_STATUS.exitstatus == 0 ? true : false
102
+ # true
103
+ # end
104
+ end
105
+ end
@@ -0,0 +1,169 @@
1
+ require 'thread'
2
+ require 'open4'
3
+ require_relative 'constants'
4
+ require_relative 'platform'
5
+ require_relative 'provisionerrunqueue'
6
+
7
+ module Elevage
8
+ # Provisioner class
9
+ class Provisioner
10
+ attr_accessor :name
11
+ attr_accessor :component
12
+ attr_accessor :instance
13
+ attr_accessor :environment
14
+ attr_accessor :vcenter
15
+
16
+ # Set us up to build the specified instance of component
17
+ def initialize(name, component, instance, environment, options)
18
+ @name = name
19
+ @component = component
20
+ @instance = instance
21
+ @environment = environment
22
+ @options = options
23
+ @vcenter = @environment.vcenter
24
+ end
25
+
26
+ def to_s
27
+ puts "Name: #{@name}"
28
+ puts "Instance: #{@instance}"
29
+ puts "Component: #{@component}"
30
+ puts @component.to_yaml
31
+ puts 'Environment:'
32
+ puts @environment.to_yaml
33
+ end
34
+
35
+ # Public: Build the node
36
+ # rubocop:disable MethodLength, LineLength, GlobalVars, CyclomaticComplexity
37
+ def build
38
+ knife_cmd = generate_knife_cmd
39
+
40
+ # Modify behavior for dry-run
41
+ # Echo command to stdout and logfile instead of executing command.
42
+ if @options['dry-run']
43
+ puts knife_cmd
44
+ knife_cmd = "echo #{knife_cmd}"
45
+ end
46
+
47
+ # Open the logfile for writing
48
+ logfile = File.new("#{@options[:logfiles]}/#{@name}.log", 'w')
49
+
50
+ stamp = @options['dry-run'] ? '' : "#{Time.now} [#{$$}]: "
51
+ puts "#{stamp}#{@name}: logging to #{logfile.path}"
52
+ logfile.puts "#{stamp}#{@name}: Provisioning."
53
+
54
+ # Execute the knife command, capturing stderr and stdout as they
55
+ # produce anything, and push it all into a Queue object, which we then
56
+ # write to the log file as things come available.
57
+ status = Open4.popen4(knife_cmd) do |_pid, _stdin, stdout, stderr|
58
+ sem = Mutex.new
59
+ # Set and forget the thread for stderr...
60
+ # err_thread = Thread.new do
61
+ Thread.new do
62
+ while (line = stderr.gets)
63
+ sem.synchronize { logfile.puts line }
64
+ end
65
+ end
66
+ out_thread = Thread.new do
67
+ while (line = stdout.gets)
68
+ sem.synchronize { logfile.puts line }
69
+ end
70
+ end
71
+ out_thread.join
72
+ # err_thread.exit
73
+ end
74
+
75
+ stamp = @options['dry-run'] ? '' : "#{Time.now} [#{$$}]: "
76
+ logfile.puts "#{stamp}#{@name}: exit status: #{status.exitstatus}"
77
+ logfile.close
78
+
79
+ # Inform our master whether we succeeded or failed. Any non-zero
80
+ # exit status is a failure, and the details will be in the logfile
81
+ status.exitstatus == 0 ? true : false
82
+ end
83
+ # rubocop:enable MethodLength, LineLength, GlobalVars, CyclomaticComplexity
84
+
85
+ private
86
+
87
+ # Private: Determine which datastore to use for this specific
88
+ # provisioning.
89
+ def select_datastore
90
+ if @options['dry-run']
91
+ @vcenter['datastores'][0]
92
+ else
93
+ @vcenter['datastores'][rand(@vcenter['datastores'].count)]
94
+ end
95
+ end
96
+
97
+ # Private: Build the knife command that will do the provisioning.
98
+ # rubocop:disable MethodLength, LineLength
99
+ def generate_knife_cmd
100
+ knife_cmd = 'knife vsphere vm clone --vsinsecure --start'
101
+
102
+ # Authentication and host
103
+ knife_cmd << " --vsuser #{@options[:vsuser]}"
104
+ knife_cmd << " --vspass #{@options[:vspass]}"
105
+ knife_cmd << " --vshost #{@vcenter['host']}"
106
+
107
+ # VM Template (what we're cloning)
108
+ knife_cmd << " --folder '#{@vcenter['imagefolder']}'"
109
+ knife_cmd << " --template '#{@component['image']}'"
110
+
111
+ # vSphere destination information (where the clone will end up)
112
+ knife_cmd << " --vsdc '#{@vcenter['datacenter']}'"
113
+ knife_cmd << " --dest-folder '#{@vcenter['destfolder']}'"
114
+ knife_cmd << " --resource-pool '#{@vcenter['resourcepool']}'"
115
+ knife_cmd << " --datastore '#{select_datastore}'"
116
+
117
+ # VM Hardware
118
+ knife_cmd << " --ccpu #{@component['compute']['cpu']}"
119
+ knife_cmd << " --cram #{@component['compute']['ram']}"
120
+
121
+ # VM Networking
122
+ knife_cmd << " --cvlan '#{@component['network']['vlanid']}'"
123
+ knife_cmd << " --cips #{@component['addresses'][@instance - 1]}/#{@component['network']['netmask']}"
124
+ knife_cmd << " --cdnsips #{@vcenter['dnsips'].join(',')}"
125
+ knife_cmd << " --cgw #{@component['network']['gateway']}"
126
+ knife_cmd << " --chostname #{@name}"
127
+ knife_cmd << " --ctz #{@vcenter['timezone']}"
128
+
129
+ # AD Domain and DNS Suffix
130
+ domain = @vcenter['domain']
131
+ domain = domain[1, domain.length] if domain.start_with?('.')
132
+ knife_cmd << " --cdomain #{domain}"
133
+ knife_cmd << " --cdnssuffix #{domain}"
134
+
135
+ # Knife Bootstrap options
136
+ knife_cmd << ' --bootstrap'
137
+ knife_cmd << " --template-file '#{@options['template-file']}'"
138
+
139
+ # knife fqdn specifies how knife will connect to the target (in this case by IP)
140
+ knife_cmd << " --fqdn #{@component['addresses'][@instance - 1]}"
141
+ knife_cmd << " --ssh-user #{@options['ssh-user']}"
142
+ knife_cmd << " --identity-file '#{@options['ssh-key']}'"
143
+
144
+ # What the node should be identified as in Chef
145
+ nodename = String.new(@name)
146
+ nodename << '.' << @environment.name if @vcenter['appendenv']
147
+ nodename << @vcenter['domain'] if @vcenter['appenddomain']
148
+ knife_cmd << " --node-name '#{nodename}'"
149
+
150
+ # Assign the run_list
151
+ knife_cmd << " --run-list '#{@component['runlist']}'"
152
+
153
+ # Assign the Chef environment
154
+ knife_cmd << " --environment '#{@environment.name}'"
155
+
156
+ # What version of chef-client are we bootstrapping (not sure this is necessary)
157
+ knife_cmd << " --bootstrap-version #{@options['bootstrap-version']}"
158
+
159
+ # Finally, the name of the VM as seen by vSphere.
160
+ # Whereas nodename will optionally append the domain name, VM names should *always* have the domain name
161
+ # appended. The only optional bit is including the chef environment in the name.
162
+ vmname = String.new(@name)
163
+ vmname << '.' << @environment.name if @vcenter['appendenv']
164
+ vmname << @vcenter['domain']
165
+ knife_cmd << " #{vmname}"
166
+ end
167
+ # rubocop:enable MethodLength, LineLength
168
+ end
169
+ end