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 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: []