elevage 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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