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 +7 -0
- data/bootstrap_ruby +2 -0
- data/lib/hatecf.rb +2 -0
- data/lib/local.rb +132 -0
- data/lib/local_dsl.rb +200 -0
- data/remote/hatecf.rb +1 -0
- data/remote/remote.rb +498 -0
- data/remote/remote_dsl.rb +178 -0
- metadata +50 -0
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
data/lib/hatecf.rb
ADDED
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: []
|