rbcm 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/app/action/action.rb +75 -0
- data/app/action/command.rb +26 -0
- data/app/action/file.rb +38 -0
- data/app/action/list.rb +83 -0
- data/app/cli.rb +153 -0
- data/app/lib/array_hash.rb +9 -0
- data/app/lib/lib.rb +32 -0
- data/app/lib/options.rb +6 -0
- data/app/lib/params.rb +52 -0
- data/app/lib/quick_each.rb +61 -0
- data/app/node/file.rb +16 -0
- data/app/node/filesystem.rb +22 -0
- data/app/node/job.rb +16 -0
- data/app/node/node.rb +36 -0
- data/app/node/remote.rb +16 -0
- data/app/node/sandbox.rb +205 -0
- data/app/node/template.rb +49 -0
- data/app/project/capability.rb +16 -0
- data/app/project/definition.rb +13 -0
- data/app/project/file.rb +42 -0
- data/app/project/project.rb +21 -0
- data/app/project/sandbox.rb +5 -0
- data/app/rbcm.rb +69 -0
- data/bin/rbcm +5 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fa6e34e5939d408f93ecd34e461a369c1a7f5fd703a75cba0c1a29070873c58e
|
4
|
+
data.tar.gz: 124be8f09ba8c960d942daf7cc43c6b80daae64711fcc4de209c51ec8a533fda
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a8de4fcc12dea85e1d5ec40bdd96db45d546fbe002e438a8520d2392ee17cd2209f1ac90391955c3456f3e529233558d67a621ab1203fb3edfb4f823c406a3f5
|
7
|
+
data.tar.gz: 600bcbd768053eeee04d72b87477cc1128c34786656c44a8d14a3843a158a69157eee9b71b037ec3a0654fef164f0f798db534530dc4a4baf6ef703a4420405d
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class Action
|
2
|
+
attr_accessor :approved, :applied
|
3
|
+
attr_reader :node, :triggered_by, :trigger, :chain, :dependencies,
|
4
|
+
:capability, :obsolete, :job, :check, :triggered, :result,
|
5
|
+
:source, :path, :tags
|
6
|
+
|
7
|
+
def initialize job:, chain:, path: nil, params: nil, line: nil, check: nil,
|
8
|
+
dependencies: nil, trigger: nil, triggered_by: nil,
|
9
|
+
source: nil, tags: nil
|
10
|
+
@dependencies = [:file] + [dependencies].flatten - [chain.last]
|
11
|
+
@trigger = [trigger, chain.last].flatten.compact
|
12
|
+
@triggered_by = [triggered_by].flatten.compact
|
13
|
+
@triggered = []; @source = source
|
14
|
+
@node = job.node; @job = job
|
15
|
+
@chain = chain; @capability = chain.last
|
16
|
+
@obsolete = nil; @approved = nil
|
17
|
+
@tags = tags.compact.flatten
|
18
|
+
# command specific
|
19
|
+
@line = line; @check = check
|
20
|
+
# file specific
|
21
|
+
@path = path; @params = params
|
22
|
+
end
|
23
|
+
|
24
|
+
def checkable?
|
25
|
+
true if @check or self.class == Action::File
|
26
|
+
end
|
27
|
+
|
28
|
+
def neccessary?
|
29
|
+
check!
|
30
|
+
not obsolete
|
31
|
+
end
|
32
|
+
|
33
|
+
def approved?
|
34
|
+
@approved
|
35
|
+
end
|
36
|
+
|
37
|
+
def approvable?
|
38
|
+
neccessary? and triggered? and approved? == nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def applyable?
|
42
|
+
approved? and not applied?
|
43
|
+
end
|
44
|
+
|
45
|
+
def triggered?
|
46
|
+
triggered_by.empty? or triggered_by.one? do |triggered_by|
|
47
|
+
@node.triggered.flatten.include? triggered_by
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def applied?
|
52
|
+
@applied
|
53
|
+
end
|
54
|
+
|
55
|
+
def succeeded?
|
56
|
+
pp self.chain unless @result
|
57
|
+
@result.exitstatus == 0
|
58
|
+
end
|
59
|
+
|
60
|
+
def failed?
|
61
|
+
@result.exitstatus != 0
|
62
|
+
end
|
63
|
+
|
64
|
+
def approve! input=:y
|
65
|
+
if [:a, :y].include? input
|
66
|
+
@node.files[@path].content = content if self.class == Action::File
|
67
|
+
@approved = true
|
68
|
+
siblings.each.approve! if input == :a
|
69
|
+
@node.triggered << @trigger
|
70
|
+
@triggered = @trigger.compact - @node.triggered
|
71
|
+
else
|
72
|
+
@approved = false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Action::Command < Action
|
2
|
+
attr_reader :line
|
3
|
+
|
4
|
+
# determine wether the command is neccessary
|
5
|
+
def check!
|
6
|
+
return if @obsolete != nil
|
7
|
+
if @check
|
8
|
+
@obsolete = @node.remote.execute(@check).exitstatus == 0
|
9
|
+
else
|
10
|
+
@obsolete = false
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# matching commands on other nodes to be approved at once
|
15
|
+
def siblings
|
16
|
+
@node.rbcm.actions.select{ |action|
|
17
|
+
action.chain[1..-1] == @chain[1..-1] and action.line == @line
|
18
|
+
} - [self]
|
19
|
+
end
|
20
|
+
|
21
|
+
# execute the command remote
|
22
|
+
def apply!
|
23
|
+
@applied = true
|
24
|
+
@result = @node.remote.execute(@line)
|
25
|
+
end
|
26
|
+
end
|
data/app/action/file.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# ToDo: approve all changes to a spicific file at once
|
2
|
+
class Action::File < Action
|
3
|
+
attr_reader :path, :content
|
4
|
+
|
5
|
+
def check!
|
6
|
+
# compare
|
7
|
+
@node.files[path].content
|
8
|
+
end
|
9
|
+
|
10
|
+
def obsolete
|
11
|
+
@node.files[path].content.chomp.chomp == content.chomp.chomp
|
12
|
+
end
|
13
|
+
|
14
|
+
def siblings
|
15
|
+
[] # TODO
|
16
|
+
end
|
17
|
+
|
18
|
+
def apply!
|
19
|
+
@applied = true
|
20
|
+
@result = @node.remote.execute("echo #{Shellwords.escape content} > #{path}")
|
21
|
+
end
|
22
|
+
|
23
|
+
def content
|
24
|
+
@content ||= if @params[:content]
|
25
|
+
@params[:content].to_s
|
26
|
+
elsif @params[:template]
|
27
|
+
Node::Template.new(
|
28
|
+
name: @params[:template],
|
29
|
+
capability: @chain[-1],
|
30
|
+
context: @params[:context]
|
31
|
+
).render
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def same_file
|
36
|
+
@node.actions.file(path) - [self]
|
37
|
+
end
|
38
|
+
end
|
data/app/action/list.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
class ActionList < Array
|
2
|
+
def initialize array=[]
|
3
|
+
array.each do |element|
|
4
|
+
insert -1, element
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def resolve_dependencies
|
9
|
+
@actions = []
|
10
|
+
self.each do |action|
|
11
|
+
resolve_action_dependencies action
|
12
|
+
end
|
13
|
+
ActionList.new @actions
|
14
|
+
end
|
15
|
+
|
16
|
+
def resolve_triggers
|
17
|
+
@actions = []
|
18
|
+
self.each do |action|
|
19
|
+
resolve_action_triggers action
|
20
|
+
end
|
21
|
+
ActionList.new @actions
|
22
|
+
end
|
23
|
+
|
24
|
+
def file path
|
25
|
+
ActionList.new select{|action| action.path == path}
|
26
|
+
end
|
27
|
+
|
28
|
+
def checkable
|
29
|
+
ActionList.new select.checkable?
|
30
|
+
end
|
31
|
+
|
32
|
+
def unneccessary
|
33
|
+
ActionList.new (self - neccessary)
|
34
|
+
end
|
35
|
+
|
36
|
+
def neccessary
|
37
|
+
ActionList.new select.neccessary?
|
38
|
+
end
|
39
|
+
|
40
|
+
def approvable
|
41
|
+
ActionList.new select.approvable?
|
42
|
+
end
|
43
|
+
|
44
|
+
def approved
|
45
|
+
ActionList.new select.approved?
|
46
|
+
end
|
47
|
+
|
48
|
+
def applyable
|
49
|
+
ActionList.new select.applyable?
|
50
|
+
end
|
51
|
+
|
52
|
+
def applied
|
53
|
+
ActionList.new select.applied?
|
54
|
+
end
|
55
|
+
|
56
|
+
def succeeded
|
57
|
+
ActionList.new applied.select.succeeded?
|
58
|
+
end
|
59
|
+
|
60
|
+
def failed
|
61
|
+
ActionList.new applied.select.failed?
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def resolve_action_dependencies this
|
67
|
+
self.select{ |action|
|
68
|
+
this.dependencies.include? action.capability
|
69
|
+
}.each{ |action|
|
70
|
+
resolve_action_dependencies action
|
71
|
+
}
|
72
|
+
@actions << this unless @actions.include? this
|
73
|
+
end
|
74
|
+
|
75
|
+
def resolve_action_triggers this
|
76
|
+
self.select{ |action|
|
77
|
+
this.trigger.one?{|trigger| action.triggered_by.include? trigger}
|
78
|
+
}.each{ |action|
|
79
|
+
resolve_action_triggers action
|
80
|
+
}
|
81
|
+
@actions << this unless @actions.include? this
|
82
|
+
end
|
83
|
+
end
|
data/app/cli.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
class CLI
|
2
|
+
def initialize params
|
3
|
+
options = Options.new params
|
4
|
+
render section: "RBCM starting", first: true
|
5
|
+
# bootstrap
|
6
|
+
@rbcm = rbcm = RBCM.new params[0] || `pwd`.chomp
|
7
|
+
render :project
|
8
|
+
render :capabilities
|
9
|
+
# parse
|
10
|
+
rbcm.parse
|
11
|
+
render :nodes
|
12
|
+
# check
|
13
|
+
render section: "CHECKING #{rbcm.actions.checkable.count} actions on #{rbcm.nodes.count} nodes"
|
14
|
+
rbcm.actions.each do |action|
|
15
|
+
check action
|
16
|
+
end
|
17
|
+
# approve
|
18
|
+
render section: "APPROVING #{rbcm.actions.approvable.count}/#{rbcm.actions.unneccessary.count} actions"
|
19
|
+
approve rbcm.actions.unneccessary.resolve_triggers
|
20
|
+
while action = rbcm.actions.approvable.resolve_triggers.first
|
21
|
+
approve action
|
22
|
+
if action.approved?
|
23
|
+
approve action.siblings
|
24
|
+
approve action.same_file if action.class == Action::File
|
25
|
+
end
|
26
|
+
end
|
27
|
+
# apply
|
28
|
+
render section: "APPLYING #{rbcm.actions.approved.count} actions"
|
29
|
+
while action = rbcm.actions.applyable.resolve_dependencies.first
|
30
|
+
apply action
|
31
|
+
end
|
32
|
+
# finish
|
33
|
+
render :applied
|
34
|
+
puts "┗━━──"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def check action
|
40
|
+
@action = action
|
41
|
+
render checking: action.check
|
42
|
+
action.check!
|
43
|
+
end
|
44
|
+
|
45
|
+
def approve actions
|
46
|
+
[actions].flatten(1).each do |action|
|
47
|
+
@action = action
|
48
|
+
render :title, color: (action.obsolete ? :green : :yellow)
|
49
|
+
render :command if action.class == Action::Command
|
50
|
+
next if not action.approvable?
|
51
|
+
render :siblings if action.siblings.any?
|
52
|
+
render :source if action.source.any?
|
53
|
+
render :diff if action.class == Action::File
|
54
|
+
render :prompt
|
55
|
+
sleep 0.25 unless [:a,:y,:n].include? r = STDIN.getch.to_sym # avoid 'ctrl-c'-trap
|
56
|
+
action.approve! r
|
57
|
+
render :approved
|
58
|
+
render :triggered if action.triggered.any?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def apply actions
|
63
|
+
[actions].flatten(1).each do |action|
|
64
|
+
@action = action
|
65
|
+
response = action.apply!
|
66
|
+
render :title, color: response.exitstatus == 0 ? :green : :red
|
67
|
+
render :command if response.exitstatus != 0
|
68
|
+
render response: response if response.length > 0
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def render element=nil, section: nil, color: nil, first: false, response: nil, checking: nil
|
73
|
+
prefix = "┃ "
|
74
|
+
if section
|
75
|
+
out "#{first ? nil : "┗━━──"}\n\n┏━━#{format :invert, :bold}#{" "*16}#{section}#{" "*16}#{format}━──\n┃"
|
76
|
+
elsif element == :title
|
77
|
+
triggerd_by = "#{format :trigger, :bold} #{@action.triggered_by.join(", ")} " if @action.triggered_by.any?
|
78
|
+
out "┣━ #{triggerd_by}#{format color, :bold} #{(@action.chain).join(" > ")} " +
|
79
|
+
"#{format} #{format :params}#{@action.job.params}#{format}" +
|
80
|
+
" #{format :tag}#{"tags: " if @action.tags.any?}#{@action.tags.join(", ")}#{format}"
|
81
|
+
elsif element == :capabilities
|
82
|
+
out prefix + "capabilities: #{Node::Sandbox.capabilities.join(", ")}"
|
83
|
+
elsif element == :project
|
84
|
+
out prefix + "project: #{@rbcm.project.files.count} ruby files"
|
85
|
+
elsif element == :nodes
|
86
|
+
out prefix + @rbcm.nodes.values.collect{ |node|
|
87
|
+
name = node.name.+(":").ljust(@rbcm.nodes.keys.each.length.max+1, " ")
|
88
|
+
jobs = node.jobs.count.to_s.rjust(@rbcm.nodes.values.collect{|node| node.jobs.count}.max.digits.count, " ")
|
89
|
+
actions = node.actions.count.to_s.rjust(@rbcm.nodes.values.collect{|node| node.actions.count}.max.digits.count, " ")
|
90
|
+
"#{name} #{jobs} jobs, #{actions} actions"
|
91
|
+
}.flatten(1).join("\n#{prefix}")
|
92
|
+
elsif element == :command
|
93
|
+
check_string = " UNLESS #{@action.check}" if @action.check
|
94
|
+
out prefix + "$> #{@action.line}\e[2m#{check_string}\e[0m"
|
95
|
+
elsif element == :siblings
|
96
|
+
string = @action.siblings.collect do |sibling|
|
97
|
+
"#{sibling.neccessary? ? format(:yellow) : format(:green)} #{sibling.node.name} #{format}"
|
98
|
+
end.join
|
99
|
+
out prefix + "#{format :siblings}siblings:#{format} #{string}"
|
100
|
+
elsif element == :source
|
101
|
+
out prefix + "source: #{format :bold}#{@source.join("#{format}, #{format :bold}")}#{format}"
|
102
|
+
elsif element == :prompt
|
103
|
+
color = @action.siblings.any? ? :siblings : :light
|
104
|
+
print prefix + "APPROVE? #{format color}[a]ll#{format}, [y]es, [N]o: "
|
105
|
+
elsif element == :triggered
|
106
|
+
out prefix +
|
107
|
+
"triggered: #{format :trigger} #{@action.triggered.join(", ")} \e[0m;" +
|
108
|
+
" again: #{@action.trigger.-(@action.triggered).join(", ")}"
|
109
|
+
elsif element == :diff
|
110
|
+
out prefix[0..-2] + Diffy::Diff.new(
|
111
|
+
@action.node.files[@action.path].content,
|
112
|
+
@action.content
|
113
|
+
).to_s(:color).split("\n").join("\n#{prefix[0..-2]}")
|
114
|
+
elsif element == :approved
|
115
|
+
string = @action.approved? ? "#{format :green} APPROVED" : "#{format :red} DECLINED"
|
116
|
+
puts "#{string} #{format}"
|
117
|
+
elsif element.class == String
|
118
|
+
out prefix + "#{element}"
|
119
|
+
elsif checking
|
120
|
+
out prefix + "CHECKING #{@action.node.name}: #{checking}"
|
121
|
+
elsif response
|
122
|
+
out prefix + response.to_s.chomp.split("\n").join("\n#{prefix}")
|
123
|
+
elsif element == :applied
|
124
|
+
out prefix
|
125
|
+
out "┣━\ #{format :green, :bold} #{@rbcm.actions.succeeded.count} secceeded #{format}"
|
126
|
+
out "┣━\ #{format :red, :bold} #{@rbcm.actions.failed.count} failed #{format}"
|
127
|
+
else
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def out line
|
132
|
+
puts "\r#{line}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def format *params, **_
|
136
|
+
"\e[0m" + {
|
137
|
+
reset: "\e[0m",
|
138
|
+
bold: "\e[1m",
|
139
|
+
light: "\e[2m",
|
140
|
+
invert: "\e[7m",
|
141
|
+
trigger: "\e[30;46m",
|
142
|
+
red: "\e[30;101m",
|
143
|
+
green: "\e[30;42m",
|
144
|
+
yellow: "\e[30;43m",
|
145
|
+
cyan: "\e[36m",
|
146
|
+
tag: "\e[35m",
|
147
|
+
params: "\e[36m",
|
148
|
+
siblings: "\e[35m"
|
149
|
+
}.select{ |key, _|
|
150
|
+
params.include? key
|
151
|
+
}.values.join
|
152
|
+
end
|
153
|
+
end
|
data/app/lib/lib.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
def log notice, warning: nil, error: nil
|
2
|
+
puts "┃\ \ \ \e[2m#{notice or warning or error}\e[0m"
|
3
|
+
end
|
4
|
+
|
5
|
+
# https://mrbrdo.wordpress.com/2013/02/27/ruby-with-statement/
|
6
|
+
module Kernel
|
7
|
+
def with(object, &block)
|
8
|
+
object.instance_eval &block
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Hash
|
13
|
+
def << hash
|
14
|
+
merge! hash
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Array
|
19
|
+
def include_one? array
|
20
|
+
(self & array).any?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# a hash which keys are initiated as arrays
|
25
|
+
# default values via `Hash.new []` are inadequate for being volatile
|
26
|
+
|
27
|
+
class ArrayHash < Hash
|
28
|
+
def [] key
|
29
|
+
store key, [] unless has_key? key
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
data/app/lib/options.rb
ADDED
data/app/lib/params.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
class Params
|
2
|
+
attr_reader :ordered, :named
|
3
|
+
|
4
|
+
def initialize ordered, named
|
5
|
+
@ordered, @named = ordered, named
|
6
|
+
end
|
7
|
+
|
8
|
+
def [] key
|
9
|
+
return ordered[key] if key.class == Integer
|
10
|
+
return named[key] if key.class == Symbol
|
11
|
+
end
|
12
|
+
|
13
|
+
def []= key, value
|
14
|
+
ordered[key] = value if key.class == Integer
|
15
|
+
named[key] = value if key.class == Symbol
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
[ ordered.collect{ |param|
|
20
|
+
"#{param}"
|
21
|
+
},
|
22
|
+
named.collect{ |k, v|
|
23
|
+
"\e[2m\e[1m#{k}:\e[21m\e[22m #{v[0..40].to_s.gsub("\n"," \\ ")}#{"\e[2m\e[1m…\e[21m\e[22m" if v.length > 40}"
|
24
|
+
}
|
25
|
+
].flatten(1).join("\e[2m\e[1m, \e[21m\e[22m")
|
26
|
+
end
|
27
|
+
|
28
|
+
def sendable
|
29
|
+
[*ordered, named.any? ? named : nil].compact
|
30
|
+
end
|
31
|
+
|
32
|
+
def empty?
|
33
|
+
true if ordered.none? and named.none?
|
34
|
+
end
|
35
|
+
|
36
|
+
def first
|
37
|
+
ordered[0]
|
38
|
+
end
|
39
|
+
|
40
|
+
def second
|
41
|
+
ordered[1]
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete *ids
|
45
|
+
copy = self.dup
|
46
|
+
[ids].flatten.each do |id|
|
47
|
+
copy.ordered.delete id if id.class == Integer
|
48
|
+
copy.named.delete id if id.class == Symbol
|
49
|
+
end
|
50
|
+
return copy
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
#
|
2
|
+
# quickeach
|
3
|
+
#
|
4
|
+
|
5
|
+
class QuickEach
|
6
|
+
def initialize enumerable
|
7
|
+
@enumerable = enumerable
|
8
|
+
end
|
9
|
+
def method_missing method, *args, &block
|
10
|
+
@enumerable.collect do |element|
|
11
|
+
element.send method, *args, &block
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Array
|
17
|
+
# copy method
|
18
|
+
define_method(
|
19
|
+
:_original_each,
|
20
|
+
instance_method(:each)
|
21
|
+
)
|
22
|
+
|
23
|
+
def each &block
|
24
|
+
unless block_given?
|
25
|
+
return QuickEach.new self
|
26
|
+
else
|
27
|
+
_original_each &block
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# quickselect
|
34
|
+
#
|
35
|
+
|
36
|
+
class QuickSelect
|
37
|
+
def initialize enumerable
|
38
|
+
@enumerable = enumerable
|
39
|
+
end
|
40
|
+
def method_missing method, *args, &block
|
41
|
+
@enumerable.select do |element|
|
42
|
+
element.send(method, *args, &block) == true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Array
|
48
|
+
# copy method
|
49
|
+
define_method(
|
50
|
+
:_original_select,
|
51
|
+
instance_method(:select)
|
52
|
+
)
|
53
|
+
|
54
|
+
def select &block
|
55
|
+
unless block_given?
|
56
|
+
return QuickSelect.new self
|
57
|
+
else
|
58
|
+
_original_select &block
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/app/node/file.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class Node::File
|
2
|
+
def initialize path:, filesystem:
|
3
|
+
@path = path
|
4
|
+
@filesystem = filesystem
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_writer :content, :mode
|
8
|
+
|
9
|
+
def content
|
10
|
+
@content ||= @filesystem.download @path
|
11
|
+
end
|
12
|
+
|
13
|
+
def mode
|
14
|
+
@mode ||= @filesystem.mode @path
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Node::Filesystem
|
2
|
+
def initialize node, overlays: false
|
3
|
+
@node = node
|
4
|
+
@underlying = overlays
|
5
|
+
@files = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def [] path
|
9
|
+
if @underlying
|
10
|
+
@files[path] || @underlying[path]
|
11
|
+
else
|
12
|
+
@files[path] ||= Node::File.new path: path, filesystem: self
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def download path
|
17
|
+
log "DOWNLOADING #{@node.name}: '#{path}'"
|
18
|
+
response = @node.remote.execute("cat '#{path}'")
|
19
|
+
response = "" if response.exitstatus != 0
|
20
|
+
response
|
21
|
+
end
|
22
|
+
end
|
data/app/node/job.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# contains parameters send to capabilities
|
2
|
+
# used to read configuration via "?"-suffix methods
|
3
|
+
|
4
|
+
class Node::Job
|
5
|
+
attr_reader :capability, :params, :node
|
6
|
+
|
7
|
+
def initialize node, capability, params
|
8
|
+
@node = node
|
9
|
+
@capability = capability
|
10
|
+
@params = params
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"#{@capability} #{@params}"
|
15
|
+
end
|
16
|
+
end
|
data/app/node/node.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
class Node
|
2
|
+
attr_reader :jobs, :definitions, :files, :name, :remote, :rbcm, :sandbox
|
3
|
+
attr_accessor :actions, :memberships, :triggered
|
4
|
+
|
5
|
+
def initialize rbcm, name
|
6
|
+
@rbcm = rbcm
|
7
|
+
@name = name
|
8
|
+
@definitions = []
|
9
|
+
@sandbox = Node::Sandbox.new self
|
10
|
+
@remote = Node::Remote.new self
|
11
|
+
@files = Node::Filesystem.new self, overlays: @remote.files
|
12
|
+
@actions = ActionList.new
|
13
|
+
@memberships = []
|
14
|
+
@jobs = []
|
15
|
+
@blocked_jobs = []
|
16
|
+
@triggered = [:file]
|
17
|
+
end
|
18
|
+
|
19
|
+
def << definition
|
20
|
+
@definitions << definition
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse
|
24
|
+
@sandbox.evaluate definitions.flatten.compact
|
25
|
+
end
|
26
|
+
|
27
|
+
def capabilities
|
28
|
+
jobs.each.capability.uniq
|
29
|
+
end
|
30
|
+
|
31
|
+
def additions
|
32
|
+
@rbcm.group_additions.select{ |group, additions|
|
33
|
+
memberships.include? group
|
34
|
+
}.values.flatten(1)
|
35
|
+
end
|
36
|
+
end
|
data/app/node/remote.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class Node::Remote
|
2
|
+
attr_reader :files
|
3
|
+
|
4
|
+
def initialize node
|
5
|
+
@host = node.name
|
6
|
+
@files = Node::Filesystem.new node
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute action
|
10
|
+
@session ||= Net::SSH.start @host, 'root'
|
11
|
+
@session.exec! action
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# @session = Net::SSH.start 'test.ckn.li', 'root'
|
16
|
+
# @session.exec!("ls").class
|
data/app/node/sandbox.rb
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
# runs a definition and catches jobs
|
2
|
+
# accepts definition-Proc and provides definition-Proc and job list
|
3
|
+
|
4
|
+
class Node::Sandbox
|
5
|
+
attr_reader :content, :jobs
|
6
|
+
|
7
|
+
def initialize node
|
8
|
+
@node = node
|
9
|
+
@name = node.name
|
10
|
+
@dependency_cache = []
|
11
|
+
@cache = {
|
12
|
+
chain: [@node.name], trigger: [], triggered_by: [], check: [],
|
13
|
+
source: [], tag: []
|
14
|
+
}
|
15
|
+
# define in instance, otherwise method-binding will be wrong (to class)
|
16
|
+
@@capabilities = @node.rbcm.project.capabilities.each.name
|
17
|
+
@node.rbcm.project.capabilities.each do |capability|
|
18
|
+
add_capability capability
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def evaluate definitions
|
23
|
+
[definitions].flatten.each do |definition|
|
24
|
+
__cache chain: definition.origin do
|
25
|
+
instance_eval &definition.content
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def tag name, &block
|
31
|
+
__cache tag: name, chain: "tag:#{name}" do
|
32
|
+
instance_eval &block
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def trigger name, &block
|
37
|
+
__cache trigger: name, chain: "trigger:#{name}" do
|
38
|
+
instance_eval &block
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def triggered_by name, &block
|
43
|
+
__cache triggered_by: name, chain: "triggered_by:#{name}" do
|
44
|
+
instance_eval &block
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def group name, &block
|
49
|
+
if block_given? # expand group
|
50
|
+
@node.rbcm.group_additions[name] << block
|
51
|
+
else # include group
|
52
|
+
raise "undefined group #{name}" unless @node.rbcm.groups[name]
|
53
|
+
@node.memberships << name
|
54
|
+
__cache chain: "group:#{name}" do
|
55
|
+
@node.rbcm.groups[name].each do |definition|
|
56
|
+
instance_eval &definition.content
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def dont *params
|
63
|
+
puts "dont #{params}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def needs *capabilities
|
67
|
+
@dependency_cache += [capabilities].flatten(1)
|
68
|
+
end
|
69
|
+
|
70
|
+
def check line, &block
|
71
|
+
__cache check: line do
|
72
|
+
instance_eval &block
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def run action, check: nil, tags: nil, trigger: nil, triggered_by: nil
|
77
|
+
@node.actions << Action::Command.new(
|
78
|
+
line: action,
|
79
|
+
check: check,
|
80
|
+
chain: @cache[:chain].dup.flatten(1),
|
81
|
+
dependencies: @dependency_cache.dup,
|
82
|
+
tags: [tags] + @cache[:tag].dup,
|
83
|
+
trigger: [@cache[:trigger].dup, trigger].flatten(1),
|
84
|
+
triggered_by: [triggered_by, @cache[:triggered_by].dup].flatten(1),
|
85
|
+
job: @node.jobs.last,
|
86
|
+
source: @cache[:source].dup.flatten, # information from other nodes
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
def file path, trigger: nil, **named
|
91
|
+
raise "RBCM: invalid file paramteres '#{named}'" if (
|
92
|
+
named.keys - [:exists, :includes_line, :after, :mode, :content,
|
93
|
+
:template, :context, :tags]
|
94
|
+
).any?
|
95
|
+
@node.actions << Action::File.new(
|
96
|
+
path: path,
|
97
|
+
params: Params.new([path], named),
|
98
|
+
chain: [@cache[:chain].dup].flatten(1),
|
99
|
+
tags: [named[:tags]] + @cache[:tag].dup,
|
100
|
+
trigger: [@cache[:trigger].dup, trigger].flatten(1),
|
101
|
+
triggered_by: @cache[:triggered_by].dup,
|
102
|
+
job: @node.jobs.last,
|
103
|
+
source: @cache[:source].dup.flatten, # information from other nodes
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
# handle getter method calls
|
108
|
+
def method_missing name, *named, **ordered, &block
|
109
|
+
#log "method #{name} missing on #{@name}"
|
110
|
+
capability_name = name[0..-2].to_sym
|
111
|
+
params = Params.new named, ordered
|
112
|
+
if not @@capabilities.include? capability_name
|
113
|
+
super
|
114
|
+
elsif name =~ /\!$/
|
115
|
+
return # never call cap! diectly
|
116
|
+
elsif name =~ /\?$/
|
117
|
+
__search capability_name, params, &block
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def __search capability_name, params, &block
|
122
|
+
if params[:nodes] == :all # scope
|
123
|
+
jobs = @node.rbcm.jobs
|
124
|
+
else
|
125
|
+
jobs = @node.jobs
|
126
|
+
end
|
127
|
+
jobs = jobs.select{|job| job.capability == capability_name}
|
128
|
+
if params.delete(:nodes).empty?
|
129
|
+
# return ordered prarams
|
130
|
+
r = jobs.collect{|job| job.params}
|
131
|
+
elsif params[0].class == Symbol
|
132
|
+
# return values of a named param
|
133
|
+
r = jobs.find_all{ |job|
|
134
|
+
job.params.named.include? params.first if job.params.named.any?
|
135
|
+
}.collect{ |job|
|
136
|
+
job.params.named
|
137
|
+
}.collect{ |named_params|
|
138
|
+
named_params[params.first]
|
139
|
+
}
|
140
|
+
elsif params.named.any?
|
141
|
+
if params[:with]
|
142
|
+
# return values of a named param
|
143
|
+
r = jobs.find_all{ |job|
|
144
|
+
job.params.named.keys.include? params[:with] and job.params.named.any?
|
145
|
+
}.collect{ |job|
|
146
|
+
params = job.params
|
147
|
+
params[:source] = job.node.name
|
148
|
+
params
|
149
|
+
}
|
150
|
+
end
|
151
|
+
end
|
152
|
+
return r.collect &block if block_given? # no-each-syntax
|
153
|
+
return r
|
154
|
+
end
|
155
|
+
|
156
|
+
def __cache trigger: nil, triggered_by: nil, params: nil, check: nil,
|
157
|
+
chain: [], source: nil, reset: nil, tag: nil
|
158
|
+
@cache[:source].append [] if chain
|
159
|
+
@cache[:source].last << source if source
|
160
|
+
@cache[:chain] << chain if chain
|
161
|
+
@cache[:tag] << tag if tag
|
162
|
+
@cache[:trigger] << trigger if trigger
|
163
|
+
@cache[:triggered_by] << triggered_by if triggered_by
|
164
|
+
@cache[:check] << check if check
|
165
|
+
yield if block_given?
|
166
|
+
@cache[:source].pop if chain
|
167
|
+
@cache[:chain].pop if chain
|
168
|
+
@cache[:tag].pop if tag
|
169
|
+
@cache[:trigger].pop if trigger
|
170
|
+
@cache[:triggered_by].pop if triggered_by
|
171
|
+
@cache[:check].pop if check
|
172
|
+
@cache[reset] = [] if reset
|
173
|
+
end
|
174
|
+
|
175
|
+
def add_capability capability
|
176
|
+
@@capabilities << capability.name unless capability.name[-1] == "!"
|
177
|
+
# define capability method
|
178
|
+
define_singleton_method :"__#{capability.name}", &capability.content.bind(self)
|
179
|
+
# define wrapper method
|
180
|
+
if capability.type == :regular
|
181
|
+
define_singleton_method capability.name do |*ordered, **named|
|
182
|
+
params = Params.new ordered, named
|
183
|
+
@node.jobs.append Node::Job.new @node, capability.name, params
|
184
|
+
@node.triggered.append capability.name
|
185
|
+
__cache trigger: params[:trigger],
|
186
|
+
triggered_by: params[:triggered_by],
|
187
|
+
chain: capability.name do
|
188
|
+
send "__#{__method__}", *params.delete(:trigger, :triggered_by).sendable
|
189
|
+
end
|
190
|
+
@dependency_cache = [:file]
|
191
|
+
end
|
192
|
+
else # capability.type == :final
|
193
|
+
define_singleton_method capability.name do
|
194
|
+
__cache chain: __method__ do
|
195
|
+
send "__#{__method__}"
|
196
|
+
end
|
197
|
+
@dependency_cache = [:file]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.capabilities
|
203
|
+
@@capabilities.uniq
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Node::Template
|
2
|
+
@@engines = [:erb, :mustache]
|
3
|
+
|
4
|
+
def initialize name:, capability: nil, context: {}
|
5
|
+
@name = name
|
6
|
+
@capability = capability
|
7
|
+
@context = context
|
8
|
+
end
|
9
|
+
|
10
|
+
def render
|
11
|
+
content = File.read path
|
12
|
+
layers.each do |layer|
|
13
|
+
if layer == :mustache
|
14
|
+
require "mustache"
|
15
|
+
content = Mustache.render(content, **@context)
|
16
|
+
elsif layer == :erb
|
17
|
+
# https://zaiste.net/rendering_erb_template_with_bindings_from_hash/
|
18
|
+
require "ostruct"; require "erb"
|
19
|
+
content = ERB.new(content).result(
|
20
|
+
OpenStruct.new(@context).instance_eval{binding}
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
return content
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def path
|
30
|
+
@path ||= Dir["#{@@project_path}/#{"capabilities/#{@capability.to_s.gsub("!","")}" if @capability}/#{@name}*"].first
|
31
|
+
end
|
32
|
+
|
33
|
+
def filename
|
34
|
+
File.basename(path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def layers
|
38
|
+
@layers = []
|
39
|
+
filename.split(".").reverse.each.to_sym.each do |layer|
|
40
|
+
break unless @@engines.include? layer
|
41
|
+
@layers << layer
|
42
|
+
end
|
43
|
+
return @layers
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.project_path= path
|
47
|
+
@@project_path = path
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# holds a capability in form of an unbound method, extracted from
|
2
|
+
# Project::Sandbox module
|
3
|
+
|
4
|
+
class Project::Capability
|
5
|
+
def initialize name:, content:
|
6
|
+
@name = name
|
7
|
+
@content = content
|
8
|
+
@type = type
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :name, :content
|
12
|
+
|
13
|
+
def type
|
14
|
+
@name[-1] == "!" ? :final : :regular
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# holds a definition on form of a proc to be executed in a nodes sandbox
|
2
|
+
|
3
|
+
class Project::Definition
|
4
|
+
def initialize type:, name:, content:
|
5
|
+
@type, @name, @content = type, name, content
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :type, :name, :content
|
9
|
+
|
10
|
+
def origin
|
11
|
+
"#{type}:#{name}"
|
12
|
+
end
|
13
|
+
end
|
data/app/project/file.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# extracts capabilities and definitions from project files
|
2
|
+
|
3
|
+
class Project::File
|
4
|
+
def initialize project_file_path
|
5
|
+
@path = project_file_path
|
6
|
+
@definitions = []
|
7
|
+
@capabilities = []
|
8
|
+
file = File.read project_file_path
|
9
|
+
method_names_cache = methods(false)
|
10
|
+
instance_eval file
|
11
|
+
sandbox = Project::Sandbox.dup
|
12
|
+
sandbox.module_eval(file)
|
13
|
+
sandbox.instance_methods.each do |name|
|
14
|
+
@capabilities.append Project::Capability.new(
|
15
|
+
name: name,
|
16
|
+
content: sandbox.instance_method(name)
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :capabilities, :definitions, :path
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def group name=nil
|
26
|
+
@definitions.append Project::Definition.new(
|
27
|
+
type: :group,
|
28
|
+
name: name,
|
29
|
+
content: Proc.new
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def node names=nil
|
34
|
+
[names].flatten(1).each do |name|
|
35
|
+
@definitions.append Project::Definition.new(
|
36
|
+
type: name.class == Regexp ? :pattern : :node,
|
37
|
+
name: name,
|
38
|
+
content: Proc.new
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Project
|
2
|
+
def initialize path
|
3
|
+
@path = path
|
4
|
+
@files = Dir["#{path}/**/*.rb"].collect{ |project_file_path|
|
5
|
+
Project::File.new project_file_path
|
6
|
+
}
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :path, :files
|
10
|
+
|
11
|
+
def capabilities
|
12
|
+
@files.each.capabilities.flatten(1).compact
|
13
|
+
end
|
14
|
+
|
15
|
+
def definitions type=nil
|
16
|
+
with @files.each.definitions.flatten(1) do
|
17
|
+
return select{|definition| definition.type == type} if type
|
18
|
+
return self
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/app/rbcm.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require "net/ssh"
|
2
|
+
require "net/scp"
|
3
|
+
require "fileutils"
|
4
|
+
require "shellwords"
|
5
|
+
require "diffy"
|
6
|
+
require "optparse"
|
7
|
+
require "parallel"
|
8
|
+
require "yaml"
|
9
|
+
|
10
|
+
require "pry"
|
11
|
+
|
12
|
+
APPDIR = File.expand_path File.dirname(__FILE__)
|
13
|
+
[ "action/action", "action/command", "action/file", "action/list",
|
14
|
+
"lib/array_hash", "lib/lib", "node/node", "node/file", "node/job",
|
15
|
+
"node/filesystem", "node/remote", "node/sandbox", "node/template",
|
16
|
+
"lib/options", "lib/quick_each",
|
17
|
+
"lib/params", "project/project", "project/definition", "project/file",
|
18
|
+
"project/capability", "project/sandbox", "cli"
|
19
|
+
].each{|requirement| require "#{APPDIR}/#{requirement}.rb"}
|
20
|
+
|
21
|
+
class RBCM
|
22
|
+
def initialize project_path
|
23
|
+
# initialize project
|
24
|
+
@project = Project.new project_path
|
25
|
+
# create nodes
|
26
|
+
@group_additions = ArrayHash.new
|
27
|
+
@nodes = {}
|
28
|
+
@project.definitions(:node).each do |node_definition|
|
29
|
+
@nodes[node_definition.name] ||= Node.new self, node_definition.name
|
30
|
+
@nodes[node_definition.name] << node_definition
|
31
|
+
# apply pattern definitions to node
|
32
|
+
@nodes[node_definition.name] << @project.definitions(:pattern).collect do |pattern_definition|
|
33
|
+
pattern_definition if node_definition.name =~ /#{pattern_definition.name}/
|
34
|
+
end
|
35
|
+
end
|
36
|
+
# create groups
|
37
|
+
@groups = ArrayHash.new
|
38
|
+
@project.definitions(:group).each do |group_definition|
|
39
|
+
@groups[group_definition.name] << group_definition
|
40
|
+
end
|
41
|
+
# else
|
42
|
+
# tell project path to template class
|
43
|
+
Node::Template.project_path = @project.path
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :nodes, :groups, :project, :actions
|
47
|
+
attr_accessor :group_additions
|
48
|
+
|
49
|
+
def actions
|
50
|
+
ActionList.new nodes.values.each.actions.flatten(1)
|
51
|
+
end
|
52
|
+
|
53
|
+
def jobs
|
54
|
+
nodes.values.each.jobs.flatten(1)
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse
|
58
|
+
log "parsing nodes"
|
59
|
+
nodes.values.each.parse
|
60
|
+
log "parsing additions"
|
61
|
+
nodes.values.each do |node|
|
62
|
+
node.sandbox.evaluate node.additions
|
63
|
+
end
|
64
|
+
log "parsing 'cap!'"
|
65
|
+
nodes.values.each do |node|
|
66
|
+
node.capabilities.each{|capability| node.sandbox.send "#{capability}!"}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/bin/rbcm
ADDED
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rbcm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Martin Wiegand
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-03-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: diffy
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mustache
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.0.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.0.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: net-ssh
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 4.2.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 4.2.0
|
55
|
+
description: manage your servers via simple config-files
|
56
|
+
email: martin@wiegand.tel
|
57
|
+
executables:
|
58
|
+
- rbcm
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- app/action/action.rb
|
63
|
+
- app/action/command.rb
|
64
|
+
- app/action/file.rb
|
65
|
+
- app/action/list.rb
|
66
|
+
- app/cli.rb
|
67
|
+
- app/lib/array_hash.rb
|
68
|
+
- app/lib/lib.rb
|
69
|
+
- app/lib/options.rb
|
70
|
+
- app/lib/params.rb
|
71
|
+
- app/lib/quick_each.rb
|
72
|
+
- app/node/file.rb
|
73
|
+
- app/node/filesystem.rb
|
74
|
+
- app/node/job.rb
|
75
|
+
- app/node/node.rb
|
76
|
+
- app/node/remote.rb
|
77
|
+
- app/node/sandbox.rb
|
78
|
+
- app/node/template.rb
|
79
|
+
- app/project/capability.rb
|
80
|
+
- app/project/definition.rb
|
81
|
+
- app/project/file.rb
|
82
|
+
- app/project/project.rb
|
83
|
+
- app/project/sandbox.rb
|
84
|
+
- app/rbcm.rb
|
85
|
+
- bin/rbcm
|
86
|
+
homepage: https://github.com/CroneKorkN/rbcm
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- app/
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.7.6
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Ruby Config Management
|
110
|
+
test_files: []
|