houcho 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/README.md +47 -84
- data/bin/houcho +114 -133
- data/lib/houcho.rb +41 -2
- data/lib/{CI.rb → houcho/ci.rb} +6 -2
- data/lib/houcho/cloudforecast.rb +59 -0
- data/lib/houcho/cloudforecast/host.rb +23 -0
- data/lib/houcho/cloudforecast/role.rb +24 -0
- data/lib/houcho/element.rb +74 -0
- data/lib/houcho/host.rb +22 -0
- data/lib/houcho/initialize.rb +15 -0
- data/lib/houcho/role.rb +121 -0
- data/lib/houcho/spec.rb +6 -0
- data/lib/houcho/spec/runner.rb +116 -0
- data/lib/houcho/version.rb +1 -1
- data/lib/houcho/yamlhandle.rb +27 -0
- data/templates/conf/houcho.conf +10 -0
- data/templates/conf/kk.rb +14 -0
- data/templates/conf/rspec.conf +1 -0
- data/templates/master +94 -0
- data/templates/role/cf_roles.yaml +0 -0
- data/templates/role/cloudforecast.yaml +0 -0
- data/templates/role/hosts.yaml +0 -0
- data/templates/role/hosts_ignored.yaml +0 -0
- data/templates/role/roles.yaml +0 -0
- data/templates/role/specs.yaml +0 -0
- data/templates/spec/sample_spec.rb +13 -0
- data/templates/spec/spec_helper.rb +29 -0
- metadata +25 -5
- data/lib/Conductor.rb +0 -619
- data/lib/RoleHandle.rb +0 -155
data/lib/houcho.rb
CHANGED
@@ -1,5 +1,44 @@
|
|
1
|
-
|
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
|
-
|
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
|
data/lib/{CI.rb → houcho/ci.rb}
RENAMED
@@ -1,7 +1,12 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
require 'uri'
|
3
3
|
|
4
|
-
|
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
|
data/lib/houcho/host.rb
ADDED
@@ -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
|
data/lib/houcho/role.rb
ADDED
@@ -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
|
data/lib/houcho/spec.rb
ADDED
@@ -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
|