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.
- 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
|