rbcm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ # a hash which keys are initiated as arrays
2
+ # default values via `Hash.new []` are inadequate for being volatile
3
+
4
+ class ArrayHash < Hash
5
+ def [] key
6
+ store key, [] unless has_key? key
7
+ super
8
+ end
9
+ 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
@@ -0,0 +1,6 @@
1
+ # https://docs.ruby-lang.org/en/2.1.0/OptionParser.html
2
+
3
+ class Options
4
+ def initialize params
5
+ end
6
+ end
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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ module Project::Sandbox
2
+ def self.method_missing *params, &block
3
+ # do nothing
4
+ end
5
+ 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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rbcm'
4
+
5
+ CLI.new ARGV
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: []