houcho 0.0.2 → 0.0.3

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.
@@ -1,5 +1,44 @@
1
- require 'houcho/version'
1
+ # -*- encoding: utf-8 -*-
2
+ require 'awesome_print'
3
+ require 'rainbow'
4
+ require 'parallel'
5
+ require 'systemu'
6
+ require 'tempfile'
7
+ require 'find'
8
+ require 'yaml'
9
+ require 'json'
10
+ require 'houcho/initialize'
11
+ require 'houcho/yamlhandle'
12
+ require 'houcho/element'
13
+ require 'houcho/role'
14
+ require 'houcho/host'
15
+ require 'houcho/spec'
16
+ require 'houcho/spec/runner'
17
+ require 'houcho/cloudforecast'
18
+ require 'houcho/cloudforecast/role'
19
+ require 'houcho/cloudforecast/host'
20
+ require 'houcho/ci'
2
21
 
3
22
  module Houcho
4
- # Your code goes here...
23
+ def puts_details(e, indentsize = 0, cnt = 1)
24
+ case e
25
+ when Array
26
+ e.sort.each.with_index(1) do |v, i|
27
+ (indentsize-1).times {print ' '}
28
+ print i != e.size ? '├─ ' : '└─ '
29
+ puts v
30
+ end
31
+ puts ''
32
+ when Hash
33
+ e.each do |k,v|
34
+ if ! indentsize.zero?
35
+ (indentsize).times {print ' '}
36
+ end
37
+ k = k.color(0,255,0)
38
+ k = '[' + k.color(219,112,147) + ']' if indentsize.zero?
39
+ puts k
40
+ puts_details(v, indentsize+1, cnt+1)
41
+ end
42
+ end
43
+ end
5
44
  end
@@ -1,7 +1,12 @@
1
1
  require 'net/http'
2
2
  require 'uri'
3
3
 
4
- class CI
4
+ module Houcho
5
+ module CI
6
+ end
7
+ end
8
+
9
+ module Houcho::CI
5
10
  class UkigumoClient
6
11
  def initialize(server, port = 80, url = "http://#{server}:#{port}")
7
12
  @ukigumo_server = server
@@ -57,5 +62,4 @@ class CI
57
62
  end
58
63
  end
59
64
  end
60
-
61
65
  end
@@ -0,0 +1,59 @@
1
+ module Houcho
2
+ module CloudForecast
3
+ module_function
4
+
5
+ def load_yaml
6
+ yaml_file = Tempfile.new('yaml')
7
+ File.open(yaml_file,'a') do |t|
8
+ Find.find('./role/cloudforecast') do |f|
9
+ t.write File.read(f) if f =~ /\.yaml$/
10
+ end
11
+ end
12
+
13
+ role_hosts = {}
14
+ elements = {}
15
+ group = []
16
+ File.open(yaml_file) do |f|
17
+ f.each do |l|
18
+ if l =~ /^---/
19
+ if l =~ /^---\s+#(.+)$/
20
+ group << $1.gsub(/\s/, '_')
21
+ else
22
+ group << 'NOGROUPNAME'
23
+ end
24
+ end
25
+ end
26
+ end
27
+ File.open(yaml_file) do |f|
28
+ i=0
29
+ YAML.load_documents(f) do |data|
30
+ elements[group[i]] ||= []
31
+ elements[group[i]].concat data['servers']
32
+ i+=1
33
+ end
34
+ end
35
+
36
+ elements.each do |groupname, data|
37
+ current_label = 'NOCATEGORYNAME'
38
+
39
+ data.each do |d|
40
+ if ! d['label'].nil?
41
+ label = d['label'].gsub(/\s/, '_')
42
+ current_label = label if current_label != label
43
+ end
44
+
45
+ d['hosts'].map! do |host|
46
+ host = host.split(' ')
47
+ host = host.size == 1 ? host[0] : host[1]
48
+ end
49
+
50
+ r = groupname + '::' + current_label + '::' + d['config'].sub(/\.yaml$/, '')
51
+ ary = (role_hosts[r] || []) | d['hosts']
52
+ role_hosts[r] = ary.uniq
53
+ end
54
+ end
55
+
56
+ File.write('./role/cloudforecast.yaml', role_hosts.to_yaml)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,23 @@
1
+ module Houcho
2
+ module CloudForecast::Host
3
+ @cfdata = YamlHandle::Loader.new('./role/cloudforecast.yaml').data
4
+
5
+ module_function
6
+
7
+ def roles(host)
8
+ @cfdata.select {|cfrole, cfhosts|cfhosts.include?(host)}.keys
9
+ end
10
+
11
+ def hosts(role)
12
+ @cfdata[role] || []
13
+ end
14
+
15
+ def details(host)
16
+ CloudForecast::Role.details(roles(host))
17
+ end
18
+
19
+ def all
20
+ @cfdata.values.flatten.uniq
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ module Houcho
2
+ module CloudForecast
3
+ class Role
4
+ @elements = YamlHandle::Editor.new('./role/cf_roles.yaml')
5
+ extend Element
6
+
7
+ def self.details(cfroles)
8
+ result = {}
9
+ cfroles.each do |cfrole|
10
+ hosts = CloudForecast::Host.hosts(cfrole)
11
+ if ! hosts.empty?
12
+ result[cfrole] = {}
13
+ result[cfrole]['host'] = hosts
14
+ end
15
+ end
16
+ result
17
+ end
18
+
19
+ def self.all
20
+ YamlHandle::Loader.new('./role/cloudforecast.yaml').data.keys
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,74 @@
1
+ module Houcho
2
+ module Element
3
+
4
+ def elements(index = nil)
5
+ if index
6
+ (@elements.data[index]||[]).uniq
7
+ else
8
+ @elements.data.values.flatten.uniq
9
+ end
10
+ end
11
+
12
+
13
+ def attach(elements, roles)
14
+ invalid_roles = []
15
+ roles.each do |role|
16
+ index = Role.index(role)
17
+ if ! index
18
+ invalid_roles << role
19
+ next
20
+ end
21
+
22
+ @elements.data[index] ||= []
23
+ @elements.data[index] = (@elements.data[index] + elements).sort.uniq
24
+ end
25
+
26
+ @elements.save_to_file
27
+ abort("role(#{invalid_roles.join(',')}) does not exist") if ! invalid_roles.size.zero?
28
+ end
29
+
30
+
31
+ def detach(elements, roles)
32
+ invalid_roles = []
33
+ roles.each do |role|
34
+ index = Role.index(role)
35
+ if ! index
36
+ invalid_roles << role
37
+ next
38
+ end
39
+
40
+ @elements.data[index] -= elements
41
+ end
42
+
43
+ @elements.save_to_file
44
+ abort("role(#{invalid_roles.join(',')}) does not exist") if ! invalid_roles.size.zero?
45
+ end
46
+
47
+
48
+ def attached?(index, element)
49
+ return false if ! @elements.data.has_key?(index)
50
+ @elements.data[index].include?(element)
51
+ end
52
+
53
+
54
+ def has_data?(index)
55
+ return false if ! @elements.data.has_key?(index)
56
+ @elements.data[index].size != 0
57
+ end
58
+
59
+
60
+ def indexes(element)
61
+ return [] if ! @elements.data.values.flatten.include?(element)
62
+ @elements.data.select {|index, elems|elems.include?(element)}.keys
63
+ end
64
+
65
+
66
+ def details(elements)
67
+ result = {}
68
+ elements.each do |element|
69
+ result[element] = { 'role' => self.indexes(element).map {|index|Role.name(index)} }
70
+ end
71
+ result
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,22 @@
1
+ module Houcho
2
+ class Host
3
+ @elements = YamlHandle::Editor.new('./role/hosts.yaml')
4
+ extend Element
5
+
6
+ def self.details(hosts)
7
+ result = {}
8
+
9
+ hosts.each do |host|
10
+ roles = self.indexes(host).map {|index|Role.name(index)}
11
+ cfroles = CloudForecast::Host.roles(host)
12
+
13
+ result[host] = {}
14
+ result[host]['role'] = roles if ! roles.empty?
15
+ result[host]['cf'] = cfroles if ! cfroles.empty?
16
+
17
+ result.delete(host) if result[host].keys.empty?
18
+ end
19
+ result
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ require 'fileutils'
2
+
3
+ module Houcho
4
+ module Initialize
5
+ templates = File.expand_path("#{File.dirname(__FILE__)}/../../templates")
6
+
7
+ %W{conf role spec}.each do |d|
8
+ FileUtils.cp_r("#{templates}/#{d}", d) if ! Dir.exist?(d)
9
+ end
10
+
11
+ File.symlink('./conf/rspec.conf', './.rspec') if ! File.exists? '.rspec'
12
+
13
+ `git init; git add .; git commit -a -m 'initialized houcho repository'` if ! Dir.exist?('.git')
14
+ end
15
+ end
@@ -0,0 +1,121 @@
1
+ module Houcho
2
+ module Role
3
+ @roles = YamlHandle::Editor.new('./role/roles.yaml')
4
+
5
+ module_function
6
+
7
+ def create(role, exists = [])
8
+ target = role.shift
9
+
10
+ if self.index(target)
11
+ exists << target
12
+ else
13
+ @roles.data[(@roles.data.keys.max||0) + 1] = target
14
+ end
15
+
16
+ if role.size == 0
17
+ @roles.save_to_file
18
+ abort("role(#{exists.join(',')}) already exist.") if exists.size != 0
19
+ else
20
+ self.create(role, exists)
21
+ end
22
+ end
23
+
24
+
25
+ def delete(role, errors = {exists:[], hosts:[], specs:[], cf:[]})
26
+ target = role.shift
27
+ index = self.index(target)
28
+ del = true
29
+
30
+ if ! index
31
+ errors[:exists] << target; del = false
32
+ end
33
+ if Host.has_data?(index)
34
+ errors[:hosts] << target if Host.has_data?(index); del = false
35
+ end
36
+ if Spec.has_data?(index)
37
+ errors[:specs] << target if Spec.has_data?(index); del = false
38
+ end
39
+ if CloudForecast::Role.has_data?(index)
40
+ errors[:cf] << target if CloudForecast::Role.has_data?(index); del = false
41
+ end
42
+
43
+ @roles.data.delete(index) if del
44
+
45
+ if role.size.zero?
46
+ @roles.save_to_file
47
+ e = []
48
+ e << "role(#{errors[:exists].join(',')}) does not exist" if ! errors[:exists].size.zero?
49
+ e << "detach host from #{errors[:hosts].join(',')} before delete" if ! errors[:hosts].size.zero?
50
+ e << "detach spec from #{errors[:specs].join(',')} before delete" if ! errors[:specs].size.zero?
51
+ e << "detach cloudforecast's role from #{errors[:cf].join(',')} before delete" if ! errors[:cf].size.zero?
52
+ abort("#{e.join(', ')}.") if ! e.size.zero?
53
+ else
54
+ self.delete(role, errors)
55
+ end
56
+ end
57
+
58
+
59
+ def rename(role, name)
60
+ index = self.index(role)
61
+ abort("#{role} does not exist") if ! index
62
+ abort("#{name} already exist") if self.index(name)
63
+
64
+ @roles.data[index] = name
65
+ @roles.save_to_file
66
+ end
67
+
68
+
69
+ def all
70
+ YamlHandle::Loader.new('./role/roles.yaml').data.values.sort
71
+ end
72
+
73
+
74
+ def details(roles)
75
+ result = {}
76
+
77
+ # too lengthy implementation... I think necessary to change...
78
+ roles = roles.map do |role|
79
+ if self.index(role)
80
+ role
81
+ else
82
+ self.indexes_regexp(Regexp.new(role)).map {|index|self.name(index)}
83
+ end
84
+ end.flatten.sort.uniq
85
+
86
+ roles.each do |role|
87
+ index = self.index(role)
88
+ next if ! index
89
+
90
+ hosts = Host.elements(index)
91
+ specs = Spec.elements(index)
92
+ cfroles = CloudForecast::Role.elements(index)
93
+ cfhosts = CloudForecast::Role.details(cfroles)
94
+
95
+ r = {}
96
+ r['host'] = hosts if ! hosts.empty?
97
+ r['spec'] = specs if ! specs.empty?
98
+ r['cf'] = cfhosts if ! cfhosts.empty?
99
+
100
+ result[role] = r
101
+ end
102
+
103
+ result
104
+ end
105
+
106
+
107
+ def index(role)
108
+ @roles.data.invert[role]
109
+ end
110
+
111
+
112
+ def indexes_regexp(role)
113
+ @roles.data.select {|index, rolename| rolename =~ role }.keys
114
+ end
115
+
116
+
117
+ def name(index)
118
+ @roles.data[index]
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,6 @@
1
+ module Houcho
2
+ class Spec
3
+ @elements = YamlHandle::Editor.new('./role/specs.yaml')
4
+ extend Element
5
+ end
6
+ end
@@ -0,0 +1,116 @@
1
+ module Houcho
2
+ module Spec::Runner
3
+ module_function
4
+
5
+ def prepare(roles, ex_hosts, hosts, specs)
6
+ runlist = {}
7
+ spec_not_exist = []
8
+
9
+ spec_check = Proc.new do |specs|
10
+ p = specs.map {|spec|'spec/' + spec + '_spec.rb'}.partition {|spes|File.exist?(spes)}
11
+ spec_not_exist += p[1].map {|e|e.sub(/^spec\//,'').sub(/_spec.rb$/,'')}
12
+ p[0]
13
+ end
14
+
15
+ Role.details(roles).each do |role, detail|
16
+ r = {}
17
+ r['spec'] = spec_check.call(detail['spec']||[])
18
+ r['host'] = detail['host']||[]
19
+
20
+ (detail['cf']||{}).each do |cfrole, value|
21
+ r['host'] += value['host']
22
+ end
23
+ r['host'] -= ex_hosts
24
+
25
+ runlist[role] = r
26
+ end
27
+
28
+ m = {}
29
+ m['host'] = hosts
30
+ m['spec'] = spec_check.call(specs)
31
+
32
+ runlist[:'run manually'] = m if m != {}
33
+
34
+ if ! spec_not_exist.empty?
35
+ $houcho_fail = true
36
+ puts "spec(#{spec_not_exist.join(',')}) file not exist in ./spec directory."
37
+ end
38
+ runlist
39
+ end
40
+
41
+
42
+ def exec(roles, ex_hosts, hosts, specs, ci = {}, dryrun = nil)
43
+ self.prepare(roles, ex_hosts, hosts, specs).each do |role, v|
44
+ next if v['spec'].empty?
45
+ command = "parallel_rspec #{v['spec'].sort.uniq.join(' ')}"
46
+
47
+ if dryrun
48
+ v['host'].each do |host|
49
+ puts "TARGET_HOST=#{host} #{command}"
50
+ end
51
+ next
52
+ end
53
+
54
+ v['host'].each do |host|
55
+ ENV['TARGET_HOST'] = host
56
+ result = systemu command
57
+ puts result[1].scan(/\d* examples?, \d* failures?\n/).first.chomp + "\t#{host}, #{command}\n"
58
+
59
+ post_result(result, role, host, command, ci) if ci != {}
60
+ $houcho_fail = true if result[0] != 0
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ def post_result(result, role, host, command, ci)
67
+ conf = YAML.load_file('conf/houcho.conf')
68
+ result_status = result[0] == 0 ? 1 : 2
69
+
70
+ if ci[:ukigumo]
71
+ ukigumo_report = CI::UkigumoClient.new(conf['ukigumo']['host'], conf['ukigumo']['port']).post({
72
+ :status => result_status,
73
+ :project => role,
74
+ :branch => host.gsub(/\./, '-'),
75
+ :repo => conf['git']['uri'],
76
+ :revision => `git log spec/| grep '^commit' | head -1 | awk '{print $2}'`.chomp,
77
+ :vc_log => command,
78
+ :body => result[1],
79
+ })
80
+ end
81
+
82
+ if ci[:ikachan] && result_status != 1
83
+ message = "[serverspec fail]\`TARGET_HOST=#{host} #{command}\` "
84
+ message += JSON.parse(ukigumo_report)['report']['url'] if ukigumo_report
85
+ CI::IkachanClient.new(
86
+ conf['ikachan']['channel'],
87
+ conf['ikachan']['host'],
88
+ conf['ikachan']['port']
89
+ ).post(message)
90
+ end
91
+ end
92
+
93
+
94
+ def check(specs, host_count)
95
+ specs = specs.flatten
96
+
97
+ specs.each do |spec|
98
+ hosts = []
99
+ indexes = Spec.indexes(spec)
100
+
101
+ if indexes.empty?
102
+ puts "#{spec} has not attached to any roles"
103
+ next
104
+ end
105
+
106
+ indexes.each do |index|
107
+ hosts += Host.elements(index)
108
+ CloudForecast::Role.elements(index).each do |cfrole|
109
+ hosts += CloudForecast::Host(cfrole)
110
+ end
111
+ end
112
+ hosts.sample(host_count).each {|host| exec([], [], [host], [spec])}
113
+ end
114
+ end
115
+ end
116
+ end