rbcm 0.0.1
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 +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: []
|