hatecf 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: b9e89a8cfac1c0bf752d1c86a7b8637ca2191eecc55e53ff8f50ccf8b80acb5c
4
+ data.tar.gz: a3efe1d1aaeceb0c0566d88ad737d055e54181f5d7dece693a3129d4210e7525
5
+ SHA512:
6
+ metadata.gz: 1e2770dafb8f77bf2f505aa0899c33d923ea32d785331323dadc30e26620261f3406d3f73e8a0495995613b4eb4180234071c805816a1c20bcfbf81f4781a4d7
7
+ data.tar.gz: 1c2ba2caa0a5c2319aa4dda79644d93e2a2f75bd9bff6963a3f5c8bc130270a766a772d324ea8afbdff6153a78ab223cb59ec5ccb4c1f114ac2c4a0e5201edbe
data/bootstrap_ruby ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ dpkg -s ruby > /dev/null || (echo "machine has no ruby; installing..."; apt-get install --no-install-recommends -y ruby)
data/lib/hatecf.rb ADDED
@@ -0,0 +1,2 @@
1
+ #require "./local_dsl.rb"
2
+ require File.join(__dir__, "./local_dsl.rb")
data/lib/local.rb ADDED
@@ -0,0 +1,132 @@
1
+ module Local
2
+ extend self
3
+
4
+ DEFAULT_TARGET = { user: "root", port: 22 }.freeze
5
+ attr_accessor :target
6
+ @target = {}
7
+
8
+ attr_accessor :as_user_block
9
+ @as_user_block = false
10
+
11
+ def print_location
12
+ exclude = [
13
+ "local.rb",
14
+ "local_dsl.rb",
15
+ ]
16
+ caller.each do |line|
17
+ $stderr.puts line unless exclude.find{|x| line.include? x}
18
+ end
19
+ end
20
+
21
+ def die(message)
22
+ $stderr.puts "Script error: #{message}"
23
+ print_location
24
+ exit 1
25
+ end
26
+
27
+ def type_check(name, value, *types)
28
+ unless types.find{|x| value.is_a? x}
29
+ case name
30
+ when 1
31
+ arg_dsc = "first argument"
32
+ when 2
33
+ arg_dsc = "second argument"
34
+ when 3
35
+ arg_dsc = "third argument"
36
+ when Symbol
37
+ arg_dsc = %{argument "#{name}:"}
38
+ else
39
+ arg_dsc = "argument"
40
+ end
41
+ if types.length > 1
42
+ types_dsc = "types #{types.map(&:to_s).join(" or ")}"
43
+ else
44
+ types_dsc = "type #{types.first}"
45
+ end
46
+ hints = types.map do |t|
47
+ case t.to_s
48
+ when "LocalFile"
49
+ %{LocalFile is a file on this very computer and could be defined as local_file("~/hello")}
50
+ when "Regexp"
51
+ "Regexp is a regular expression wrapped in /slashes/"
52
+ else
53
+ nil
54
+ end
55
+ end.compact.map{|x|"\nHint: #{x}"}.join
56
+ die "#{arg_dsc} should be of #{types_dsc}, but #{value.class.to_s} here#{hints}"
57
+ end
58
+ end
59
+
60
+ def check_mode(mode)
61
+ mode = mode.to_i(8) if mode.is_a? String
62
+ die "please supply up to 4 digits before the leading zero, example: 00644" if mode && mode >= 010000
63
+ end
64
+
65
+ attr_accessor :local_files
66
+ @local_files = []
67
+
68
+ require 'pathname'
69
+ def copy_local_files_to(dir)
70
+ @local_files.each do |local_file|
71
+ # dublicating the whole directory stucture
72
+ acc = []
73
+ Pathname.new(local_file).each_filename do |chunk|
74
+ source_path = File.join(*(['/'] + acc << chunk))
75
+ dst = File.join([dir] + acc << chunk)
76
+ if File.directory? source_path
77
+ unless File.directory? dst
78
+ debug "mkdir #{dst}"
79
+ FileUtils.mkdir_p(dst)
80
+ end
81
+ else
82
+ debug "cp #{source_path} #{dst}"
83
+ FileUtils.cp(source_path, dst)
84
+ end
85
+ acc << chunk
86
+ end
87
+ end
88
+ end
89
+
90
+ require 'tmpdir'
91
+ def perform!
92
+ die "no target host specified!" if (@target[:host] || "").empty?
93
+ script_path = File.expand_path(ENV["_"])
94
+ script_dir = File.dirname(File.expand_path(ENV["_"]))
95
+ status = nil
96
+ Dir.mktmpdir "hatecf" do |tmp_dir|
97
+ debug "using temporary dir #{tmp_dir}"
98
+ FileUtils.cp(script_path, File.join(tmp_dir, "script.rb"))
99
+ File.chmod(00777, File.join(tmp_dir, "script.rb"))
100
+ FileUtils.cp(File.join(__dir__, "../remote/hatecf.rb" ), tmp_dir)
101
+ FileUtils.cp(File.join(__dir__, "../remote/remote_dsl.rb"), tmp_dir)
102
+ FileUtils.cp(File.join(__dir__, "../remote/remote.rb" ), tmp_dir)
103
+ FileUtils.cp(File.join(__dir__, "../bootstrap_ruby" ), tmp_dir)
104
+ File.chmod(00777, File.join(tmp_dir, "bootstrap_ruby"))
105
+ unless @local_files.empty?
106
+ local_files_dir = File.join(tmp_dir, "local_files")
107
+ FileUtils.mkdir local_files_dir
108
+ copy_local_files_to(local_files_dir)
109
+ end
110
+ cmd = "tar -czP #{tmp_dir} | ssh #{@target[:user] || DEFAULT_TARGET[:user]}@#{@target[:host]} \"tar -xzP -C / && (cd #{tmp_dir} && ./bootstrap_ruby && RUBYLIB=. ./script.rb #{@dry_run ? "--dry " : " "}--local-home #{ENV["HOME"]} --local-script-dir #{script_dir}); rm -r #{tmp_dir}\""
111
+ debug "executing: #{cmd}"
112
+ pid, status = Process.wait2(Process.spawn(cmd))
113
+ debug "exit status: #{status.exitstatus}"
114
+ end
115
+ exit status.exitstatus
116
+ end
117
+
118
+ require 'optparse'
119
+ OptionParser.new do |opts|
120
+ opts.on "--dry", "don't change anything, just test everything" do |x|
121
+ @dry_run = true
122
+ end
123
+ end.parse!
124
+
125
+ def debug(s)
126
+ end
127
+
128
+ def info(s)
129
+ $stdout.puts s
130
+ $stdout.flush
131
+ end
132
+ end
data/lib/local_dsl.rb ADDED
@@ -0,0 +1,200 @@
1
+ #require "./local.rb"
2
+ require File.join(__dir__, "./local.rb")
3
+
4
+ def target(host:, user: nil, port: nil)
5
+ Local.type_check(:host, host, String)
6
+ Local.type_check(:user, user, String) if user
7
+ Local.type_check(:port, port, Integer) if port
8
+ #Local.target = Local::DEFAULT_TARGET.dup
9
+ Local.target[:host] = host
10
+ Local.target[:user] = user if user
11
+ Local.target[:port] = port if port
12
+ end
13
+
14
+ class LocalFile
15
+ def initialize(path)
16
+ end
17
+ end
18
+
19
+ def local_file(path)
20
+ # File.expand_path("~/dir") -> "/home/user/dir"
21
+ # File.expand_path("~/dir", "/whatever") -> "/home/user/dir" anyway
22
+ # File.expand_path("./dir", "/specific") -> "/specific/dir"
23
+ # File.join('/specific', '/absolute') -> "/specific/absolute"
24
+ path = File.expand_path(path, File.dirname(ENV["_"]))
25
+ Local.die "#{path} doesn't exists" unless File.exist? path
26
+ Local.local_files = Local.local_files | [path]
27
+ return LocalFile.new(path)
28
+ end
29
+
30
+ class LocalEntity
31
+ def initialize(payload)
32
+ @payload = payload
33
+ end
34
+ end
35
+
36
+ def local(x)
37
+ LocalEntity.new(x)
38
+ end
39
+
40
+ class TaskResult
41
+ def afterwards
42
+ yield
43
+ end
44
+ end
45
+
46
+ class ConfigBlockHandler
47
+ attr_accessor :path
48
+
49
+ def replace_or_add_line(a, b)
50
+ Local.type_check(1, a, String, Regexp)
51
+ Local.type_check(2, b, String)
52
+ TaskResult.new
53
+ end
54
+
55
+ def add_line(s)
56
+ Local.type_check(nil, s, String)
57
+ TaskResult.new
58
+ end
59
+
60
+ def add_block(text)
61
+ Local.type_check(nil, text, String)
62
+ TaskResult.new
63
+ end
64
+ end
65
+
66
+ def edit_config(path)
67
+ Local.type_check(nil, path, String)
68
+ h = ConfigBlockHandler.new
69
+ h.path = path
70
+ yield h
71
+ TaskResult.new
72
+ end
73
+
74
+ def service_reload(name)
75
+ Local.type_check(nil, name, String, Symbol)
76
+ end
77
+
78
+ def service_restart(name)
79
+ Local.type_check(nil, name, String, Symbol)
80
+ end
81
+
82
+ def create_user(name, create_home:, shell:)
83
+ Local.type_check(nil, name, String, Symbol)
84
+ Local.type_check(:create_home, create_home, TrueClass, FalseClass)
85
+ Local.type_check(:shell, shell, String)
86
+ TaskResult.new
87
+ end
88
+
89
+ def authorize_ssh_key(user:, key:)
90
+ Local.type_check(:user, user, String, Symbol)
91
+ Local.type_check(:key, key, String, LocalFile)
92
+ TaskResult.new
93
+ end
94
+
95
+ def apt_update
96
+ end
97
+
98
+ def apt_install(names)
99
+ Local.type_check(nil, names, String, Symbol, Array)
100
+ TaskResult.new
101
+ end
102
+
103
+ def apt_remove(names)
104
+ Local.type_check(nil, names, String, Symbol, Array)
105
+ TaskResult.new
106
+ end
107
+
108
+ def command(cmd, expect_status: 0)
109
+ Local.type_check(nil, cmd, String, Symbol, Array)
110
+ Local.type_check(:expect_status, expect_status, Integer, Array, Range)
111
+ TaskResult.new
112
+ end
113
+
114
+ def create_config(path, text)
115
+ Local.type_check(1, path, String)
116
+ Local.type_check(2, text, String)
117
+ TaskResult.new
118
+ end
119
+
120
+ def mkdir_p(x)
121
+ Local.type_check(nil, x, String, Symbol, Array)
122
+ TaskResult.new
123
+ end
124
+
125
+ def cp(src, dst, mode: nil)
126
+ Local.type_check(1, src, String, LocalFile)
127
+ Local.type_check(1, dst, String)
128
+ if mode
129
+ Local.type_check(:mode, mode, String, Integer)
130
+ Local.check_mode mode
131
+ end
132
+ TaskResult.new
133
+ end
134
+
135
+ def chmod(mode, path)
136
+ Local.type_check(2, path, String)
137
+ if mode
138
+ Local.type_check(:mode, mode, String, Integer)
139
+ Local.check_mode mode
140
+ end
141
+ TaskResult.new
142
+ end
143
+
144
+ def ln_s(src, dst)
145
+ Local.type_check(1, src, String)
146
+ Local.type_check(2, dst, String)
147
+ TaskResult.new
148
+ end
149
+
150
+ def block
151
+ yield
152
+ end
153
+
154
+ def as(user, group: nil)
155
+ Local.die 'nested "as" blocks are not supported' if Local.as_user_block
156
+ Local.type_check(nil, user, String, Symbol)
157
+ Local.type_check(nil, group, String, Symbol) if group
158
+ Local.as_user_block = true
159
+ begin
160
+ yield
161
+ ensure
162
+ Local.as_user_block = false
163
+ end
164
+ end
165
+
166
+ def remote_file(path)
167
+ Local.type_check(nil, path, String)
168
+ UndefinedOnLocal.new
169
+ end
170
+
171
+ class UndefinedOnLocal
172
+ def to_s
173
+ "[undefined on local]"
174
+ end
175
+ def method_missing(*args)
176
+ self
177
+ end
178
+ end
179
+
180
+ class RemoteFile
181
+ attr_reader :path
182
+ def initialize(path)
183
+ @path = path
184
+ end
185
+ def read
186
+ UndefinedOnLocal.new
187
+ end
188
+ end
189
+
190
+ def perform!
191
+ Local.perform!
192
+ end
193
+
194
+ # Monkey-patching
195
+
196
+ class Array
197
+ def afterwards
198
+ yield
199
+ end
200
+ end
data/remote/hatecf.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(__dir__, "./remote_dsl.rb")
data/remote/remote.rb ADDED
@@ -0,0 +1,498 @@
1
+ module Remote
2
+ extend self
3
+
4
+ # $HOME variable should be imported from local machine
5
+ # in order to access ~/ paths
6
+ # and directory of scrip should be imported
7
+ # in order to access relative ./ paths
8
+ attr_accessor :local_home, :local_script_dir
9
+
10
+ class LocalFile
11
+ attr_reader :local_path
12
+ def initialize(local_path)
13
+ @local_path = local_path
14
+ end
15
+
16
+ def remote_path
17
+ raise "local home wasn't set!" if Remote.local_home.nil?
18
+ # Temporary swapping $HOME for File.expand_path
19
+ t_home = ENV["HOME"]
20
+ begin
21
+ $stdout.flush
22
+ ENV["HOME"] = Remote.local_home
23
+ # File.expand_path("~/dir") -> "/home/user/dir"
24
+ # File.expand_path("~/dir", "/whatever") -> "/home/user/dir" anyway
25
+ # File.expand_path("./dir", "/specific") -> "/specific/dir"
26
+ # File.join('/specific', '/absolute') -> "/specific/absolute"
27
+ r = File.join(__dir__, "local_files", File.expand_path(local_path, Remote.local_script_dir))
28
+ ensure
29
+ ENV["HOME"] = t_home
30
+ end
31
+ return r
32
+ end
33
+ end
34
+
35
+ attr_accessor :block_contexts
36
+ @block_contexts = []
37
+
38
+ class TaskResult
39
+ attr_reader :changed
40
+ def initialize(changed)
41
+ @changed = changed
42
+ # propagating change to the block above
43
+ Remote.block_contexts[-1] = Remote.block_contexts[-1] || changed unless Remote.block_contexts[-1].nil?
44
+ end
45
+ def afterwards(&block)
46
+ if @changed
47
+ yield
48
+ end
49
+ end
50
+ end
51
+
52
+ class ConfigBlockHandler
53
+ attr_reader :changed
54
+ def initialize(path, handler)
55
+ @path = path
56
+ @handler = handler
57
+ @changed = false
58
+ end
59
+
60
+ def replace_or_add_line(a, b)
61
+ task_result = Remote.task do
62
+ b = b.strip
63
+ content = File.read(@handler)
64
+ case a # force to one line
65
+ when String
66
+ a_regexp = /^#{Regexp.quote(a)}$/
67
+ when Regexp
68
+ a_regexp = /^#{a}$/
69
+ end
70
+ if content.match /^#{Regexp.quote(b)}$/
71
+ Remote.ok "#{@path} config includes #{b.inspect}"
72
+ else
73
+ a_match = content.match(a_regexp)
74
+ if a_match
75
+ Remote.destructive "replacing in config #{@path} line #{a_match[0].inspect} with #{b.inspect}" do
76
+ @handler.truncate(0)
77
+ @handler.write(content.gsub(a_regexp, b))
78
+ end
79
+ else # appending
80
+ Remote.destructive "appending config #{@path} with #{b.inspect}" do
81
+ @handler.seek(-1, IO::SEEK_END) if @handler.size > 0
82
+ @handler.write("\n")
83
+ @handler.write(b)
84
+ @handler.write("\n")
85
+ end
86
+ end
87
+ end
88
+ @handler.seek(0, IO::SEEK_SET) # rewind handler back
89
+ end
90
+ @changed = @changed || task_result.changed # propagate to block
91
+ end
92
+
93
+ def add_line(line)
94
+ task_result = Remote.task do
95
+ line = line.strip
96
+ content = File.read(@handler)
97
+ if content.include? line
98
+ Remote.ok "#{@path} config includes #{line}"
99
+ else
100
+ Remote.destructive "appending config #{@path} with #{b.inspect}" do
101
+ @handler.seek(-1, IO::SEEK_END) if @handler.size > 0
102
+ @handler.write("\n")
103
+ @handler.write(b)
104
+ @handler.write("\n")
105
+ end
106
+ end
107
+ @handler.seek(0, IO::SEEK_SET) # rewind handler back
108
+ end
109
+ @changed = @changed || task_result.changed # propagate to block
110
+
111
+ end
112
+
113
+ def add_block(text)
114
+ task_result = Remote.task do
115
+ content = File.read(@handler)
116
+ if content.include? text.strip
117
+ Remote.ok "config #{@path} includes the block of text"
118
+ else
119
+ Remote.destructive "appending config #{@path} with the block of text" do
120
+ @handler.seek(-1, IO::SEEK_END) if @handler.size > 0
121
+ @handler.write("\n")
122
+ @handler.write(text)
123
+ end
124
+ end
125
+ @handler.seek(0, IO::SEEK_SET) # rewind handler back
126
+ end
127
+ @changed = @changed || task_result.changed # propagate to block
128
+ end
129
+ end
130
+
131
+ def service_reload(name)
132
+ destructive "reloading service #{name}" do
133
+ cmd = ["systemctl", "reload", name]
134
+ spawn(cmd, expect_status: 0)
135
+ end
136
+ end
137
+
138
+ def service_restart(name)
139
+ destructive "restarting service #{name}" do
140
+ cmd = ["systemctl", "restart", name]
141
+ spawn(cmd, expect_status: 0)
142
+ end
143
+ end
144
+
145
+ require 'etc'
146
+ def user_exists?(name)
147
+ begin
148
+ Etc.getpwnam(name)
149
+ true
150
+ rescue
151
+ false
152
+ end
153
+ end
154
+
155
+ def get_user_home_dir(name)
156
+ Etc.getpwnam(name).dir
157
+ end
158
+
159
+ def get_uid_and_gid_of_user(name)
160
+ r = Etc.getpwnam(name).dir
161
+ [r.uid, r.gid]
162
+ end
163
+
164
+ def create_user(name, create_home, shell)
165
+ if user_exists?(name)
166
+ ok "#{name} user exists"
167
+ else
168
+ destructive "creating user #{name}" do
169
+ cmd = ["useradd", name]
170
+ cmd << "--create-home" if create_home
171
+ cmd << "--shell" << shell if shell
172
+ spawn cmd, expect_status: 0
173
+ end
174
+ end
175
+ end
176
+
177
+ def authorize_ssh_key(user, key)
178
+ case key
179
+ when LocalFile
180
+ key_text = File.read(key.remote_path).strip
181
+ when String
182
+ key_text = key
183
+ else
184
+ raise "type #{key.class} is not supported for a SSH public key"
185
+ end
186
+ require 'fileutils'
187
+ home_dir = get_user_home_dir(user)
188
+ ssh_dir = File.join(home_dir, ".ssh")
189
+ authorized_keys_path = File.join(ssh_dir, "authorized_keys")
190
+ if File.exist? authorized_keys_path
191
+ file_mode = @dry_run ? File::RDONLY : File::RDWR | File::APPEND
192
+ File.open(authorized_keys_path, file_mode, 0600) do |f|
193
+ f.flock(File::LOCK_EX)
194
+ content = File.read(f)
195
+ if content.include? key_text
196
+ ok "#{authorized_keys_path} has the key"
197
+ else
198
+ destructive "appending key to #{authorized_keys_path}" do
199
+ f.seek(-1, IO::SEEK_END) if f.size > 0
200
+ f.write(key_text)
201
+ f.write("\n")
202
+ end
203
+ end
204
+ end
205
+ else
206
+ unless File.exist? ssh_dir
207
+ destructive "creating #{ssh_dir}" do
208
+ FileUtils.mkdir ssh_dir, mode: 0700
209
+ File.chown(*(get_uid_and_gid_of_user(user) << ssh_dir))
210
+ end
211
+ end
212
+ destructive "creating #{authorized_keys_path} with the key" do
213
+ File.open(authorized_keys_path, File::CREAT | File::WRONLY, 0600) do |f|
214
+ f.flock(File::LOCK_EX)
215
+ f.write(key_text)
216
+ f.write("\n")
217
+ end
218
+ File.chown(*(get_uid_and_gid_of_user(user) << authorized_keys_path))
219
+ end
220
+ end
221
+ end
222
+
223
+ attr_accessor :apt_updated
224
+ def dpkg_installed?(names)
225
+ # | virtual | virtual | removed
226
+ # | package | package | but
227
+ # | present | absent | dependency
228
+ # method | | | left
229
+ # ----------------------------------------
230
+ # dpkg -s | exit 1 | exit 1 | exit 1
231
+ # ----------------------------------------
232
+ # dpkg-qeury | exit 0 | exit 1 | exit 0
233
+ # -W | | |
234
+ case names
235
+ when Array
236
+ cmd = ["dpkg-query", "-W"] + names
237
+ when String
238
+ cmd = ["dpkg-query", "-W", names]
239
+ end
240
+ spawn(cmd, expect_status: [0,1]) == 0
241
+ end
242
+
243
+ def apt_update
244
+ task do
245
+ destructive "apt-get update" do
246
+ spawn ["apt-get", "update", "-q"], expect_status: 0
247
+ end
248
+ end
249
+ end
250
+
251
+ def apt_install(names)
252
+ names = names.to_s if names.is_a? Symbol
253
+ names = names.split(/\s+/).map(&:strip).select{|x|not x.empty?} if names.is_a? String
254
+ if dpkg_installed?(names)
255
+ ok "installed: #{names.join(', ')}"
256
+ else
257
+ destructive "apt-get install #{names.is_a?(Array) ? names.join(' ') : names}" do
258
+ unless apt_updated
259
+ cmd = ["apt-get", "update", "-q"]
260
+ spawn(cmd, expect_status: 0)
261
+ apt_updated = true
262
+ end
263
+
264
+ cmd = ["apt-get", "install", "--no-install-recommends", "-y"]
265
+ cmd += names
266
+ spawn(cmd, expect_status: 0)
267
+ end
268
+ end
269
+ end
270
+
271
+ def apt_remove(names)
272
+ names = names.to_s if names.is_a? Symbol
273
+ names = names.split(/\s+/).map(&:strip).select{|x|not x.empty?} if names.is_a? String
274
+ installed = names.each.select do |x|
275
+ dpkg_installed? x
276
+ end
277
+ if installed.empty?
278
+ ok "removed: #{names.join(", ")}"
279
+ else
280
+ installed.each do |name|
281
+ destructive "apt-get remove #{name}" do
282
+ cmd = ["apt-get", "remove", "-y"]
283
+ cmd += names
284
+ spawn cmd, expect_status: 0
285
+ end
286
+ end
287
+ # We need to run autoremove because dpkg treat
288
+ # removed packages with installed dependencies
289
+ # as still installed. See the note in "dpkg_installed?"
290
+ # method
291
+ destructive "apt-get autoremove" do
292
+ spawn ["apt-get", "autoremove", "-y"], expect_status: 0
293
+ end
294
+ end
295
+ end
296
+
297
+ def create_config(path, text)
298
+ path = File.expand_path path
299
+ unless File.exist?(path) && File.read(path).rstrip == text
300
+ destructive "writing config #{path}" do
301
+ File.open(path, File::CREAT | File::TRUNC | File::WRONLY) do |f|
302
+ f.flock(File::LOCK_EX)
303
+ f.write(text)
304
+ f.write("\n")
305
+ end
306
+ end
307
+ else
308
+ ok "#{path} already there"
309
+ end
310
+ end
311
+
312
+ class RemoteFile
313
+ attr_reader :path
314
+ def initialize(path)
315
+ @path = path
316
+ end
317
+ def read
318
+ File.read(@path)
319
+ end
320
+ end
321
+
322
+ def mkdir_p(x)
323
+ x = File.expand_path x
324
+ if File.directory? x
325
+ ok "#{x} directory exists"
326
+ else
327
+ destructive "mkdir -p #{x}" do
328
+ FileUtils.mkdir_p x
329
+ end
330
+ end
331
+ end
332
+
333
+ # TODO: the dst is a dir case
334
+ def cp(src, dst, mode)
335
+ src = src.remote_path if src.is_a? LocalFile
336
+ dst = File.expand_path dst
337
+ if (not File.exist?(dst)) || (not FileUtils.cmp(src, dst))
338
+ destructive "copying to #{dst.inspect}" do
339
+ FileUtils.cp src, dst
340
+ end
341
+ else
342
+ ok "#{dst} already there"
343
+ end
344
+ if mode
345
+ mode = mode.to_i(8) if mode.is_a? String
346
+ if File.stat(dst).mode % 010000 == mode
347
+ ok "#{dst} already in #{sprintf "%04o", mode}"
348
+ else
349
+ destructive "chmod #{sprintf "%04o", mode} #{dst}" do
350
+ File.chmod(mode, dst)
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ def chmod(mode, path)
357
+ mode = mode.to_i(8) if mode.is_a? String
358
+ path = File.expand_path path
359
+ if File.stat(path).mode % 010000 == mode
360
+ ok "#{path} already in #{sprintf "%04o", mode}"
361
+ else
362
+ destructive "chmod #{sprintf "%04o", mode} #{path}" do
363
+ File.chmod(mode, path)
364
+ end
365
+ end
366
+ end
367
+
368
+ def ln_s(src, dst)
369
+ src = File.expand_path src
370
+ dst = File.expand_path dst
371
+ if File.lstat(dst).symlink? && File.readlink(dst) == src
372
+ ok "#{dst} leads to #{src}"
373
+ else
374
+ destructive "ln -s #{src} #{dst}" do
375
+ FileUtils.ln_s src dst
376
+ end
377
+ end
378
+ end
379
+
380
+ #attr_accessor :impressionating_user
381
+
382
+ def spawn(cmd, expect_status: nil)
383
+ expect_status = [expect_status] unless expect_status.nil? || expect_status.respond_to?(:include?)
384
+ stdout_r, stdout_w = IO.pipe
385
+ debug "executing #{cmd.inspect}"
386
+ pid, status = Process.wait2(Process.spawn(*cmd, in: File::NULL, out: stdout_w, err: stdout_w))
387
+ if expect_status && (not expect_status.include?(status.exitstatus))
388
+ stdout_w.close
389
+ text = stdout_r.read
390
+ raise CommandError.new("A process #{cmd.inspect} returned unexpected exit status #{status.exitstatus}", text)
391
+ end
392
+ stdout_w.close
393
+ stdout_r.close
394
+ return status.exitstatus
395
+ end
396
+
397
+ class CommandError < StandardError
398
+ attr_reader :output
399
+ def initialize(msg, output)
400
+ @output = output
401
+ super(msg)
402
+ end
403
+ end
404
+
405
+ def ok(s)
406
+ info "[ ] #{s}"
407
+ end
408
+
409
+ attr_reader :dry_run
410
+ @dry_run = false
411
+ def destructive(desc = nil)
412
+ if desc
413
+ if @dry_run
414
+ info "[-] NOT #{desc} [dry run]"
415
+ else
416
+ info "[x] #{desc}"
417
+ end
418
+ end
419
+ @task_changed_something = true
420
+ yield unless @dry_run
421
+ end
422
+
423
+ attr_reader :ok_counter, :changed_counter
424
+ @ok_counter = @changed_counter = 0
425
+ def task
426
+ @task_changed_something = false
427
+ begin
428
+ yield
429
+ rescue => e
430
+ handle_error e
431
+ end
432
+ if @task_changed_something
433
+ @changed_counter += 1
434
+ else
435
+ @ok_counter += 1
436
+ end
437
+ TaskResult.new @task_changed_something
438
+ end
439
+
440
+ def handle_error(exc)
441
+ $stderr.puts
442
+ #exclude = [
443
+ # "hatecf.rb",
444
+ # "remote.rb",
445
+ #]
446
+ #line = caller.select do |line|
447
+ # not exclude.find{|x| line.include? x}
448
+ # #$stderr.puts line unless exclude.find{|x| line.include? x}
449
+ #end.first
450
+
451
+ exc.backtrace.each do |line|
452
+ $stderr.puts line
453
+ end
454
+ $stderr.puts
455
+ $stderr.puts exc.message
456
+ $stderr.puts
457
+ if exc.respond_to? :output
458
+ $stderr.puts exc.output
459
+ $stderr.puts
460
+ end
461
+ exit 1
462
+ end
463
+
464
+ def debug(s)
465
+ end
466
+
467
+ def info(s)
468
+ $stdout.puts s
469
+ $stdout.flush
470
+ end
471
+
472
+ require 'json'
473
+ def save_state
474
+ JSON.dump({
475
+ ok_counter: @ok_counter,
476
+ changed_counter: @changed_counter,
477
+ })
478
+ end
479
+
480
+ def load_state(json)
481
+ h = JSON.parse(json, symbolize_names: true)
482
+ @ok_counter = h[:ok_counter]
483
+ @changed_counter = h[:changed_counter]
484
+ end
485
+
486
+ require 'optparse'
487
+ OptionParser.new do |opts|
488
+ opts.on "--local-home HOME" do |x|
489
+ @local_home = x
490
+ end
491
+ opts.on "--local-script-dir DIR" do |x|
492
+ @local_script_dir = x
493
+ end
494
+ opts.on "--dry" do |x|
495
+ @dry_run = true
496
+ end
497
+ end.parse!
498
+ end
@@ -0,0 +1,178 @@
1
+ require './remote.rb'
2
+
3
+ def target(*args)
4
+ # skip on remote
5
+ end
6
+
7
+ def local_file(path)
8
+ Remote::LocalFile.new(path)
9
+ end
10
+
11
+ def edit_config(path)
12
+ path = File.expand_path path
13
+ File.open(path, File::RDWR) do |f|
14
+ f.flock(File::LOCK_EX)
15
+ h = Remote::ConfigBlockHandler.new(path, f)
16
+ yield h
17
+ return Remote::TaskResult.new(h.changed)
18
+ end
19
+ end
20
+
21
+ def service_reload(name)
22
+ Remote.task do
23
+ Remote.service_reload(name)
24
+ end
25
+ end
26
+
27
+ def service_restart(name)
28
+ Remote.task do
29
+ Remote.service_restart(name)
30
+ end
31
+ end
32
+
33
+ def create_user(name, create_home: false, shell: nil)
34
+ Remote.task do
35
+ Remote.create_user(name, create_home, shell)
36
+ end
37
+ end
38
+
39
+ def authorize_ssh_key(user:, key:)
40
+ Remote.task do
41
+ Remote.authorize_ssh_key(user, key)
42
+ end
43
+ end
44
+
45
+ def apt_update
46
+ Remote.apt_update
47
+ end
48
+
49
+ def apt_install(names)
50
+ Remote.task do
51
+ Remote.apt_install names
52
+ end
53
+ end
54
+
55
+ def apt_remove(names)
56
+ Remote.task do
57
+ Remote.apt_remove(names)
58
+ end
59
+ end
60
+
61
+ def command(cmd, expect_status: 0)
62
+ Remote.task do
63
+ Remote.destructive "executing #{cmd.inspect}" do
64
+ Remote.spawn(cmd, expect_status: expect_status)
65
+ end
66
+ end
67
+ end
68
+
69
+ def create_config(path, text)
70
+ Remote.task do
71
+ Remote.create_config(path, text)
72
+ end
73
+ end
74
+
75
+ def mkdir_p(x)
76
+ Remote.task do
77
+ Remote.mkdir_p(x)
78
+ end
79
+ end
80
+
81
+ def cp(src, dst, mode: nil)
82
+ Remote.task do
83
+ Remote.cp(src, dst, mode)
84
+ end
85
+ end
86
+
87
+ def chmod(mode, path)
88
+ Remote.task do
89
+ Remote.chmod(mode, path)
90
+ end
91
+ end
92
+
93
+ def ln_s(src, dst)
94
+ Remote.task do
95
+ Remote.ln_s(src, dst)
96
+ end
97
+ end
98
+
99
+ def block
100
+ Remote.block_contexts << false
101
+ begin
102
+ yield
103
+ ensure
104
+ changed = Remote.block_contexts.pop
105
+ end
106
+ Remote::TaskResult.new(changed)
107
+ end
108
+
109
+ def as(user, group: nil)
110
+ require 'etc'
111
+ u = Etc.getpwnam(user)
112
+ if group
113
+ g = Etc.getgrnam(group)
114
+ else
115
+ begin
116
+ g = Etc.getgrnam(user)
117
+ rescue
118
+ end
119
+ end
120
+ rd, wr = IO.pipe # for state
121
+ child_pid = fork
122
+ if child_pid # parent
123
+ wr.close
124
+ pid, status = Process.wait2(child_pid)
125
+ if status.exitstatus > 0
126
+ exit status.exitstatus
127
+ else
128
+ Remote.load_state(rd.read)
129
+ rd.close
130
+ end
131
+ else # child
132
+ rd.close
133
+ wr.close_on_exec = true
134
+ #ENV["USER"] = user
135
+ #ENV["LOGNAME"] = user
136
+ ENV["HOME"] = u.dir # required at least for File.expand_path
137
+ Process.gid = Process.egid = g.gid if g
138
+ Process.uid = Process.euid = u.uid
139
+ yield
140
+ wr.write(Remote.save_state)
141
+ wr.flush
142
+ wr.close
143
+ $stdout.flush
144
+ $stderr.flush
145
+ exit 0
146
+ end
147
+ end
148
+
149
+ def remote_file(path)
150
+ Remote::RemoteFile.new(path)
151
+ end
152
+
153
+ def perform!
154
+ Remote.info "ok: #{Remote.ok_counter}, changed: #{Remote.changed_counter}"
155
+ end
156
+
157
+ # Monkey-patching
158
+
159
+ module ArrayExtension
160
+ @task_result = nil
161
+ def afterwards(&block)
162
+ @task_result.afterwards(&block)
163
+ end
164
+ def each
165
+ Remote.block_contexts << false
166
+ begin
167
+ r = super
168
+ ensure
169
+ changed = Remote.block_contexts.pop
170
+ end
171
+ @task_result = Remote::TaskResult.new(changed)
172
+ return r
173
+ end
174
+ end
175
+
176
+ class Array
177
+ prepend ArrayExtension
178
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hatecf
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Markov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-02-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Configuration management engine like Ansible but without YAML and 30
14
+ times faster
15
+ email: apshertonets@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - bootstrap_ruby
21
+ - lib/hatecf.rb
22
+ - lib/local.rb
23
+ - lib/local_dsl.rb
24
+ - remote/hatecf.rb
25
+ - remote/remote.rb
26
+ - remote/remote_dsl.rb
27
+ homepage: https://github.com/apsheronets/hatecf
28
+ licenses:
29
+ - LGPL-3.0
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubygems_version: 3.3.15
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: Configuration management powered by hate
50
+ test_files: []