houcho 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'houcho/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "houcho"
8
+ spec.version = Houcho::VERSION
9
+ spec.authors = ["Satoshi SUZUKI"]
10
+ spec.email = ["studio3104.com@gmail.com"]
11
+ spec.description = %q{covering to run serverspec}
12
+ spec.summary = %q{covering to run serverspec}
13
+ spec.homepage = "https://github.com/studio3104/houcho"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'rainbow'
22
+ spec.add_runtime_dependency 'parallel_tests'
23
+ spec.add_runtime_dependency 'systemu'
24
+ spec.add_runtime_dependency 'serverspec'
25
+ spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "rake"
27
+ end
@@ -0,0 +1,61 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ class CI
5
+ class UkigumoClient
6
+ def initialize(server, port = 80, url = "http://#{server}:#{port}")
7
+ @ukigumo_server = server
8
+ @ukigumo_listen_port = port
9
+ @ukigumo_base_url = url
10
+ end
11
+
12
+ def search(elements, limit = 1)
13
+ query_string =
14
+ URI.encode("project") + "=" + URI.encode(elements[:project].to_s) + "&" +
15
+ URI.encode("branch") + "=" + URI.encode(elements[:branch].to_s) + "&" +
16
+ URI.encode("revision") + "=" + URI.encode(elements[:revision].to_s) + "&" +
17
+ URI.encode("limit") + "=" + URI.encode(limit.to_s)
18
+
19
+ Net::HTTP.start(@ukigumo_server, @ukigumo_listen_port) do |http|
20
+ responce = http.get("/api/v1/report/search?#{query_string}")
21
+ responce.body
22
+ end
23
+ end
24
+
25
+ def post(elements)
26
+ Net::HTTP.post_form(
27
+ URI.parse("#{@ukigumo_base_url}/api/v1/report/add"),
28
+ {
29
+ :status => elements[:status],
30
+ :project => elements[:project],
31
+ :branch => elements[:branch],
32
+ :repo => elements[:repo],
33
+ :revision => elements[:revision],
34
+ :vc_log => elements[:vc_log],
35
+ :body => elements[:body],
36
+ }
37
+ ).body
38
+ end
39
+ end
40
+
41
+ class IkachanClient
42
+ def initialize(channel, server, port = 4979)
43
+ @ikachan_server = server
44
+ @ikachan_listen_port = port
45
+ @ikachan_channels = channel.instance_of?(Array) ? channel : [channel]
46
+ end
47
+
48
+ def post(message)
49
+ @ikachan_channels.each do |channel|
50
+ Net::HTTP.post_form(
51
+ URI.parse("http://#{@ikachan_server}:#{@ikachan_listen_port}/notice"),
52
+ {
53
+ :channel => channel,
54
+ :message => message,
55
+ }
56
+ ).body
57
+ end
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,610 @@
1
+ # -*- encoding: utf-8 -*-
2
+ def app_dir
3
+ File.expand_path("#{File.dirname(__FILE__)}/..")
4
+ end
5
+
6
+ require 'awesome_print'
7
+ require 'rainbow'
8
+ require 'parallel'
9
+ require 'systemu'
10
+ require 'tempfile'
11
+ require 'find'
12
+ require 'yaml'
13
+ require 'json'
14
+ require app_dir + "/lib/CI"
15
+ require app_dir + "/lib/RoleHandle"
16
+
17
+ class Conductor
18
+ def configure_houcho
19
+ cf_yamls = Tempfile.new('yaml')
20
+ File.open(cf_yamls,'a') do |t|
21
+ Find.find('./role/cloudforecast') do |f|
22
+ t.write File.read(f) if f =~ /\.yaml$/
23
+ end
24
+ end
25
+
26
+ cf = RoleHandle::CfLoader.new(cf_yamls)
27
+ File.write('./role/cloudforecast.yaml', cf.role_hosts.to_yaml)
28
+ end
29
+
30
+ def initialize_houcho
31
+ # ヒアドキュメントのところ、外部ファイルを置いてそれを参照するようにしたほうがよさそうね
32
+ %W{conf role/cloudforecast spec}.each do |d|
33
+ FileUtils.mkdir_p d if ! Dir.exists? d
34
+ end
35
+
36
+ File.write('./role/cloudforecast/houcho_sample.yaml', <<EOH
37
+ --- #houcho
38
+ servers:
39
+ - label: author
40
+ config: studio3104
41
+ hosts:
42
+ - studio3104.test
43
+ - studio3105.test
44
+ - studio3106.test
45
+ - studio3107.test
46
+ - studio3108.test
47
+ - studio3109.test
48
+ - studio3110.test
49
+ EOH
50
+ ) if ! File.exists? './role/cloudforecast/houcho_sample.yaml'
51
+
52
+ File.write('./spec/spec_helper.rb', <<EOH
53
+ require 'serverspec'
54
+ require 'pathname'
55
+ require 'net/ssh'
56
+
57
+ include Serverspec::Helper::Ssh
58
+ include Serverspec::Helper::DetectOS
59
+
60
+ RSpec.configure do |c|
61
+ if ENV['ASK_SUDO_PASSWORD']
62
+ require 'highline/import'
63
+ c.sudo_password = ask("Enter sudo password: ") { |q| q.echo = false }
64
+ else
65
+ c.sudo_password = ENV['SUDO_PASSWORD']
66
+ end
67
+ c.before :all do
68
+ block = self.class.metadata[:example_group_block]
69
+ if RUBY_VERSION.start_with?('1.8')
70
+ file = block.to_s.match(/.*@(.*):[0-9]+>/)[1]
71
+ else
72
+ file = block.source_location.first
73
+ end
74
+ c.ssh.close if c.ssh
75
+ c.host = ENV['TARGET_HOST']
76
+ options = Net::SSH::Config.for(c.host)
77
+ user = options[:user] || Etc.getlogin
78
+ c.ssh = Net::SSH.start(c.host, user, options)
79
+ c.os = backend(Serverspec::Commands::Base).check_os
80
+ end
81
+ end
82
+ EOH
83
+ ) if ! File.exists? './spec/spec_helper.rb'
84
+
85
+ File.write('./spec/houcho_sample_spec.rb', <<EOH
86
+ require 'spec_helper'
87
+
88
+ describe user('studio3104') do
89
+ it { should exist }
90
+ it { should have_uid 3104 }
91
+ it { should have_home_directory '/home/studio3104' }
92
+ it { should have_login_shell '/bin/zsh' }
93
+ it { should belong_to_group 'studio3104' }
94
+ end
95
+ describe group('studio3104') do
96
+ it { should exist }
97
+ it { should have_gid 3104 }
98
+ end
99
+ EOH
100
+ ) if ! File.exists? './spec/houcho_sample_spec.rb'
101
+
102
+ File.write('./conf/houcho.conf', {
103
+ 'ukigumo' => {'host' => nil, 'port' => nil,},
104
+ 'ikachan' => {'host' => nil, 'port' => nil, 'channel' => [nil],},
105
+ 'git' => {'uri' => nil,},
106
+ }.to_yaml) if ! File.exists? './conf/houcho.conf'
107
+
108
+ File.write('./conf/rspec.conf', '--format documentation') if ! File.exists? './conf/rspec.conf'
109
+ File.symlink('./conf/rspec.conf', './.rspec') if ! File.exists? './.rspec'
110
+
111
+ %w{
112
+ runlists.yaml
113
+ roles.yaml
114
+ hosts.yaml
115
+ specs.yaml
116
+ cf_roles.yaml
117
+ hosts_ignored.yaml
118
+ cloudforecast.yaml
119
+ }.each do |f|
120
+ f = 'role/' + f
121
+ File.write(f, '') if ! File.exists? f
122
+ end
123
+
124
+ File.open("./.houcho", "w").close()
125
+ `git init; git add .; git commit -a -m 'initial commit'` if ! Dir.exists?('./.git')
126
+ end
127
+
128
+ def show_all_cf_roles
129
+ puts cfload.keys.sort.join("\n")
130
+ end
131
+
132
+ def show_all_roles
133
+ puts RoleHandle::YamlLoader.new('./role/roles.yaml').data.values.sort.join("\n")
134
+ end
135
+
136
+ def show_all_hosts
137
+ puts (cfload.values.flatten.uniq + hosthandle.elements).join("\n")
138
+ end
139
+
140
+ def show_all_specs
141
+ puts spechandle.elements.sort.join("\n")
142
+ end
143
+
144
+
145
+ def show_cf_role_details(cf_role)
146
+ rh = cfload
147
+ abort("#{cf_role} does not exist in cloudforecast's yaml") if ! rh.keys.include?(cf_role)
148
+
149
+ puts '[host(s)]'
150
+ puts rh[cf_role].join("\n")
151
+
152
+ attached_role_indexes = cfrolehandle.indexes(cf_role)
153
+ if ! attached_role_indexes.empty?
154
+ r = rolehandle
155
+ puts ''
156
+ puts '[attached role(s)]'
157
+ attached_role_indexes.each do |index|
158
+ puts r.name(index)
159
+ end
160
+ end
161
+ end
162
+
163
+
164
+ def show_role_details(role)
165
+ puts_details(role_details(role))
166
+ end
167
+
168
+
169
+ def show_host_details(host)
170
+ h = hosthandle
171
+ r = rolehandle
172
+ indexes = h.indexes(host)
173
+ cfroles = cfload.select {|role, hosts|hosts.include?(host)}.keys
174
+
175
+ abort("#{host} has not attached to any roles") if indexes.empty? && cfroles.empty?
176
+
177
+ result = {host => {}}
178
+
179
+ if ! indexes.empty?
180
+ result[host]['[attached role(s)]'] = []
181
+ indexes.each do |index|
182
+ result[host]['[attached role(s)]'] << r.name(index)
183
+ end
184
+ end
185
+
186
+ if ! cfroles.empty?
187
+ cf = cfrolehandle
188
+ ih = ignorehosthandle
189
+ result[host]["[cloudforecast's role]"] = {}
190
+ cfroles.each do |cfrole|
191
+ result[host]["[cloudforecast's role]"][cfrole] = []
192
+ cf.indexes(cfrole).each do |index|
193
+ res = ih.data.include?(host) ? '<ignored>' + r.name(index) + '</ignored>' : r.name(index)
194
+ result[host]["[cloudforecast's role]"][cfrole] << res
195
+ end
196
+ end
197
+ end
198
+
199
+ puts_details(result)
200
+ end
201
+
202
+
203
+ def show_spec_details(spec)
204
+ s = spechandle
205
+ r = rolehandle
206
+ indexes = s.indexes(spec)
207
+
208
+ abort("#{spec} has not attached to any roles") if indexes.empty?
209
+
210
+ result = {spec => {}}
211
+
212
+ if ! indexes.empty?
213
+ result[spec]['[attached role(s)]'] = []
214
+ indexes.each do |index|
215
+ result[spec]['[attached role(s)]'] << r.name(index)
216
+ end
217
+ end
218
+
219
+ puts_details(result)
220
+ end
221
+
222
+
223
+ def create_runlist(runlist)
224
+ rlh = runlisthandle
225
+ abort("runlist(#{runlist}) already exist") if rlh.data.has_key?(runlist)
226
+ rlh.data[runlist] = []
227
+ rlh.save_to_file
228
+ end
229
+
230
+
231
+ def delete_runlist(runlist)
232
+ rlh = runlisthandle
233
+ abort("runlist(#{runlist}) does not exist") if ! rlh.data.has_key?(runlist)
234
+ abort("exclude role(s) from runlist before delete runlist") if ! rlh.data[runlist].empty?
235
+ rlh.data.delete(runlist)
236
+ rlh.save_to_file
237
+ end
238
+
239
+
240
+ def include_role_among_runlist(role, runlist)
241
+ rlh = runlisthandle
242
+ abort("runlist(#{runlist}) does not exist") if ! rlh.data.has_key?(runlist)
243
+ index = validate_role(role)
244
+ rlh.data[runlist] << index
245
+ rlh.save_to_file
246
+ end
247
+
248
+
249
+ def exclude_role_from_runlist(role, runlist)
250
+ rlh = runlisthandle
251
+ abort("runlist(#{runlist}) does not exist") if ! rlh.data.has_key?(runlist)
252
+ index = validate_role(role)
253
+ rlh.data[runlist].delete(index)
254
+ rlh.save_to_file
255
+ end
256
+
257
+
258
+ def rename_runlist(runlist, rename)
259
+ rlh = runlisthandle
260
+ abort("runlist(#{runlist}) does not exist") if ! rlh.data.has_key?(runlist)
261
+ abort("runlist(#{rename}) already exist") if rlh.data.has_key?(rename)
262
+ rlh.data[rename] = rlh.data[runlist]
263
+ rlh.data.delete(runlist)
264
+ rlh.save_to_file
265
+ end
266
+
267
+
268
+ def show_runlist_details(runlist)
269
+ rlh = runlisthandle
270
+ r = rolehandle
271
+ abort("runlist(#{runlist}) does not exist") if ! rlh.data.has_key?(runlist)
272
+
273
+ roledetails = {}
274
+ rlh.data[runlist].each do |roleindex|
275
+ roledetails.merge!(role_details(r.name(roleindex)))
276
+ end
277
+
278
+ puts_details({
279
+ runlist => {
280
+ '[role]' => roledetails
281
+ }
282
+ })
283
+ end
284
+
285
+
286
+ def show_all_runlists
287
+ puts runlisthandle.data.keys.sort.join("\n")
288
+ end
289
+
290
+
291
+ def create_role(role)
292
+ abort("#{role} is reserved by houcho") if %w{ManuallyRun}.include?(role)
293
+ r = rolehandle
294
+ index = r.index(role)
295
+ abort("role(#{role}) already exist") if index
296
+ r.create(role)
297
+ end
298
+
299
+
300
+ def delete_role(role)
301
+ r = rolehandle
302
+ index = r.index(role)
303
+ abort("role(#{role}) does not exist") if ! index
304
+ abort("detach host(s) from #{role} before delete #{role}") if hosthandle.has_data?(index)
305
+ abort("detach spec(s) from #{role} before delete #{role}") if spechandle.has_data?(index)
306
+ abort("detach cloudforecast's role(s) from #{role} before delete #{role}") if cfrolehandle.has_data?(index)
307
+ r.delete(index)
308
+ end
309
+
310
+
311
+ def rename_role(role, name)
312
+ r = rolehandle
313
+ index = r.index(role)
314
+ abort("#{role} does not exist") if ! index
315
+ abort("#{name} already exist") if r.index(name)
316
+ r.rename(index, name)
317
+ end
318
+
319
+
320
+ def attach_host_to_role(host, role)
321
+ index = validate_role(role)
322
+ h = hosthandle
323
+ abort("#{host} has already attached to #{role}") if h.attached?(index, host)
324
+ h.attach(index, host)
325
+ end
326
+
327
+
328
+ def detach_host_from_role(host, role)
329
+ index = validate_role(role)
330
+ h = hosthandle
331
+ abort("#{host} does not attach to #{role}") if ! h.attached?(index, host)
332
+ h.detach(index, host)
333
+ end
334
+
335
+
336
+ def ignore_host(host)
337
+ ih = ignorehosthandle
338
+ ih.data = Hash === ih.data ? [] : ih.data
339
+ abort("#{host} has already included into ignore list") if ih.data.include?(host)
340
+ ih.data << host
341
+ ih.save_to_file
342
+ end
343
+
344
+
345
+ def disignore_host(host)
346
+ ih = ignorehosthandle
347
+ ih.data = Hash === ih.data ? [] : ih.data
348
+ abort("#{host} does not include into ignore list") if ! ih.data.include?(host)
349
+ ih.data.delete(host)
350
+ ih.save_to_file
351
+ end
352
+
353
+
354
+ def attach_spec_to_role(spec, role)
355
+ index = validate_role(role)
356
+ s = spechandle
357
+ abort("#{spec} already attach to #{role}") if s.attached?(index, spec)
358
+ s.attach(index, spec)
359
+ end
360
+
361
+
362
+ def detach_spec_from_role(spec, role)
363
+ index = validate_role(role)
364
+ s = spechandle
365
+ abort("#{spec} does not attach to #{role}") if ! s.attached?(index, spec)
366
+ s.detach(index, spec)
367
+ end
368
+
369
+
370
+ def attach_cfrole_to_role(cf_role, role)
371
+ index = validate_role(role)
372
+ cr = cfrolehandle
373
+ abort("#{cf_role} does not exist in cloudforecast's yaml") if ! cfload.has_key?(cf_role)
374
+ abort("#{cf_role} already attach to #{role}") if cr.attached?(index, cf_role)
375
+ cr.attach(index, cf_role)
376
+ end
377
+
378
+
379
+ def detach_cfrole_from_role(cf_role, role)
380
+ index = validate_role(role)
381
+ cr = cfrolehandle
382
+ abort("#{cf_role} does not attach to #{role}") if ! cr.attached?(index, cf_role)
383
+ cr.detach(index, cf_role)
384
+ end
385
+
386
+
387
+ def check_specs(*args)
388
+ host_count = args.shift
389
+ specs = args.flatten
390
+ s = spechandle
391
+ h = hosthandle
392
+ cr = cfrolehandle
393
+ rh = cfload
394
+
395
+ specs.each do |spec|
396
+ hosts = []
397
+ indexes = s.indexes(spec)
398
+
399
+ if indexes.empty?
400
+ puts "#{spec} has not attached to any roles"
401
+ next
402
+ end
403
+
404
+ indexes.each do |index|
405
+ hosts += h.elements(index)
406
+ cr.elements(index).each do |cfrole|
407
+ hosts += rh[cfrole]
408
+ end
409
+ end
410
+ hosts.sample(host_count).each {|host| runspec(nil, host, [spec])}
411
+ end
412
+ end
413
+
414
+
415
+ def runspec_all(ci, dry)
416
+ roles = RoleHandle::YamlLoader.new('./role/roles.yaml').data.values.sort
417
+ end
418
+
419
+
420
+ def runspec_prepare(roles, hosts, specs, ci, dry)
421
+ rhs = prepare_list(roles, hosts, specs)
422
+
423
+ rhs.each do |role, host_specs|
424
+ host_specs.each do |host, specs|
425
+ runspec(role, host, specs, ci, dry)
426
+ end
427
+ end
428
+ end
429
+
430
+
431
+ private
432
+ def prepare_list(roles, hosts, specs)
433
+ role_host_specs = {}
434
+
435
+ rh = cfload
436
+ r = rolehandle
437
+
438
+ hosts.each do |host|
439
+ role_host_specs['ManuallyRun'][host] ||= []
440
+ role_host_specs['ManuallyRun'][host] = (role_host_specs['ManuallyRun'][host] + specs).uniqspecs
441
+ end
442
+
443
+ roles.each do |role|
444
+ validate_role(Regexp.new(role)).each do |index|
445
+ _role = r.name(index)
446
+ _hosts = hosthandle.elements(index)
447
+ _specs = spechandle.elements(index)
448
+
449
+ cfrolehandle.elements(index).each do |cf_role|
450
+ if rh[cf_role].nil?
451
+ p cf_role
452
+ next
453
+ end
454
+ _hosts += rh[cf_role]
455
+ end
456
+
457
+ _hosts = (hosts + _hosts).uniq - ignorehosthandle.data.to_a
458
+ role_host_specs[_role] = {}
459
+
460
+ _hosts.each do |host|
461
+ role_host_specs[_role][host] ||= []
462
+ role_host_specs[_role][host] = (role_host_specs[_role][host] + _specs).uniq
463
+ end
464
+ end
465
+ end
466
+
467
+ role_host_specs
468
+ end
469
+
470
+ def validate_role(role)
471
+ if Regexp === role
472
+ indexes = rolehandle.indexes_regexp(role)
473
+ abort if indexes.empty?
474
+ indexes
475
+ else
476
+ index = rolehandle.index(role)
477
+ abort("role(#{role}) does not exist") if ! index
478
+ index
479
+ end
480
+ end
481
+
482
+ def cfload
483
+ RoleHandle::YamlLoader.new('./role/cloudforecast.yaml').data
484
+ end
485
+
486
+ def runlisthandle
487
+ RoleHandle::YamlEditor.new('./role/runlists.yaml')
488
+ end
489
+
490
+ def rolehandle
491
+ RoleHandle::RoleHandler.new('./role/roles.yaml')
492
+ end
493
+
494
+ def cfrolehandle
495
+ RoleHandle::ElementHandler.new('./role/cf_roles.yaml')
496
+ end
497
+
498
+ def hosthandle
499
+ RoleHandle::ElementHandler.new('./role/hosts.yaml')
500
+ end
501
+
502
+ def ignorehosthandle
503
+ RoleHandle::YamlEditor.new('./role/hosts_ignored.yaml')
504
+ end
505
+
506
+ def spechandle
507
+ RoleHandle::ElementHandler.new('./role/specs.yaml')
508
+ end
509
+
510
+ def runspec(role, host, specs, ci = {}, dryrun = nil)
511
+ executable_specs = specs.map {|spec| 'spec/' + spec + '_spec.rb'}.join(' ')
512
+ command = "parallel_rspec #{executable_specs}"
513
+ if dryrun
514
+ puts 'TARGET_HOST=' + host + ' ' + command
515
+ return
516
+ end
517
+
518
+ ENV['TARGET_HOST'] = host
519
+ result = systemu command
520
+ result_status = result[0] == 0 ? 1 : 2
521
+ puts result[1].scan(/\d* examples?, \d* failures?\n/).first.chomp + "\t#{host}, #{executable_specs}\n"
522
+
523
+ if ci[:ukigumo]
524
+ @conf = YAML.load_file('conf/houcho.conf')
525
+ ukigumo_report = CI::UkigumoClient.new(@conf['ukigumo']['host'], @conf['ukigumo']['port']).post({
526
+ :status => result_status,
527
+ :project => role,
528
+ :branch => host.gsub(/\./, '-'),
529
+ :repo => @conf['git']['uri'],
530
+ :revision => `git log spec/| grep '^commit' | head -1 | awk '{print $2}'`.chomp,
531
+ :vc_log => command,
532
+ :body => result[1],
533
+ })
534
+ end
535
+
536
+ if ci[:ikachan] && result_status != 1
537
+ message = "[serverspec fail]\`TARGET_HOST=#{host} #{command}\` "
538
+ message += JSON.parse(ukigumo_report)['report']['url'] if ukigumo_report
539
+ @conf = YAML.load_file('conf/houcho.conf')
540
+ CI::IkachanClient.new(
541
+ @conf['ikachan']['channel'],
542
+ @conf['ikachan']['host'],
543
+ @conf['ikachan']['port']
544
+ ).post(message)
545
+ end
546
+
547
+ $fail_runspec = true if result_status != 1
548
+ end
549
+
550
+
551
+ def puts_details(e, indentsize = 0, cnt = 1)
552
+ case e
553
+ when Array
554
+ e.sort.each.with_index(1) do |v, i|
555
+ (indentsize-1).times {print ' '}
556
+ print i != e.size ? '├─ ' : '└─ '
557
+ puts v =~ /^<ignored>.*<\/ignored>$/ ? v.color(:red) : v.color(240,230,140)
558
+ end
559
+ puts ''
560
+ when Hash
561
+ e.each do |k,v|
562
+ if ! indentsize.zero?
563
+ (indentsize).times {print ' '}
564
+ end
565
+ puts k =~ /^\[.*\]$/ ? k : k.color(0,255,0)
566
+ puts '' if indentsize.zero?
567
+ puts_details(v, indentsize+1, cnt+1)
568
+ end
569
+ end
570
+ end
571
+
572
+
573
+ def role_details(role)
574
+ index = validate_role(role)
575
+
576
+ ih = ignorehosthandle
577
+
578
+ hosts = hosthandle.elements(index)
579
+ specs = spechandle.elements(index)
580
+ cfroles = cfrolehandle.elements(index)
581
+
582
+ result = {role => {}}
583
+
584
+ if ! hosts.empty?
585
+ hosts.each do |h|
586
+ host = ih.data.include?(h) ? '<ignored>' + h + '</ignored>' : h
587
+ result[role]['[host]'] ||= []
588
+ result[role]['[host]'] << host
589
+ end
590
+ end
591
+ result[role]['[spec]'] = specs if ! specs.empty?
592
+
593
+ if ! cfroles.empty?
594
+ rh = cfload
595
+ result[role]["[cloudforecast's]"] = {}
596
+ cfroles.each do |cr|
597
+ result[role]["[cloudforecast's]"][cr] = {}
598
+ if ! (rh[cr]||[]).empty?
599
+ result[role]["[cloudforecast's]"][cr]['[host]'] = []
600
+ rh[cr].each do |h|
601
+ host = ih.data.include?(h) ? '<ignored>' + h + '</ignored>' : h
602
+ result[role]["[cloudforecast's]"][cr]['[host]'] << host
603
+ end
604
+ end
605
+ end
606
+ end
607
+
608
+ result
609
+ end
610
+ end