kanrisuru 0.1.0 → 0.2.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +17 -0
  3. data/.rubocop.yml +47 -0
  4. data/.rubocop_todo.yml +0 -0
  5. data/CHANGELOG.md +0 -0
  6. data/Gemfile +2 -5
  7. data/LICENSE.txt +1 -1
  8. data/README.md +143 -7
  9. data/Rakefile +5 -3
  10. data/bin/console +4 -3
  11. data/kanrisuru.gemspec +21 -12
  12. data/lib/kanrisuru.rb +41 -2
  13. data/lib/kanrisuru/command.rb +99 -0
  14. data/lib/kanrisuru/core.rb +53 -0
  15. data/lib/kanrisuru/core/archive.rb +154 -0
  16. data/lib/kanrisuru/core/disk.rb +302 -0
  17. data/lib/kanrisuru/core/file.rb +332 -0
  18. data/lib/kanrisuru/core/find.rb +108 -0
  19. data/lib/kanrisuru/core/group.rb +97 -0
  20. data/lib/kanrisuru/core/ip.rb +1032 -0
  21. data/lib/kanrisuru/core/mount.rb +138 -0
  22. data/lib/kanrisuru/core/path.rb +140 -0
  23. data/lib/kanrisuru/core/socket.rb +168 -0
  24. data/lib/kanrisuru/core/stat.rb +104 -0
  25. data/lib/kanrisuru/core/stream.rb +121 -0
  26. data/lib/kanrisuru/core/system.rb +348 -0
  27. data/lib/kanrisuru/core/transfer.rb +203 -0
  28. data/lib/kanrisuru/core/user.rb +198 -0
  29. data/lib/kanrisuru/logger.rb +8 -0
  30. data/lib/kanrisuru/mode.rb +277 -0
  31. data/lib/kanrisuru/os_package.rb +235 -0
  32. data/lib/kanrisuru/remote.rb +10 -0
  33. data/lib/kanrisuru/remote/cluster.rb +95 -0
  34. data/lib/kanrisuru/remote/cpu.rb +68 -0
  35. data/lib/kanrisuru/remote/env.rb +33 -0
  36. data/lib/kanrisuru/remote/file.rb +354 -0
  37. data/lib/kanrisuru/remote/fstab.rb +412 -0
  38. data/lib/kanrisuru/remote/host.rb +191 -0
  39. data/lib/kanrisuru/remote/memory.rb +19 -0
  40. data/lib/kanrisuru/remote/os.rb +87 -0
  41. data/lib/kanrisuru/result.rb +78 -0
  42. data/lib/kanrisuru/template.rb +32 -0
  43. data/lib/kanrisuru/util.rb +40 -0
  44. data/lib/kanrisuru/util/bits.rb +203 -0
  45. data/lib/kanrisuru/util/fs_mount_opts.rb +655 -0
  46. data/lib/kanrisuru/util/os_family.rb +213 -0
  47. data/lib/kanrisuru/util/signal.rb +161 -0
  48. data/lib/kanrisuru/version.rb +3 -1
  49. data/spec/functional/core/archive_spec.rb +228 -0
  50. data/spec/functional/core/disk_spec.rb +80 -0
  51. data/spec/functional/core/file_spec.rb +341 -0
  52. data/spec/functional/core/find_spec.rb +52 -0
  53. data/spec/functional/core/group_spec.rb +65 -0
  54. data/spec/functional/core/ip_spec.rb +71 -0
  55. data/spec/functional/core/path_spec.rb +93 -0
  56. data/spec/functional/core/socket_spec.rb +31 -0
  57. data/spec/functional/core/stat_spec.rb +98 -0
  58. data/spec/functional/core/stream_spec.rb +99 -0
  59. data/spec/functional/core/system_spec.rb +96 -0
  60. data/spec/functional/core/transfer_spec.rb +108 -0
  61. data/spec/functional/core/user_spec.rb +76 -0
  62. data/spec/functional/os_package_spec.rb +75 -0
  63. data/spec/functional/remote/cluster_spec.rb +45 -0
  64. data/spec/functional/remote/cpu_spec.rb +41 -0
  65. data/spec/functional/remote/env_spec.rb +36 -0
  66. data/spec/functional/remote/fstab_spec.rb +76 -0
  67. data/spec/functional/remote/host_spec.rb +68 -0
  68. data/spec/functional/remote/memory_spec.rb +29 -0
  69. data/spec/functional/remote/os_spec.rb +63 -0
  70. data/spec/functional/remote/remote_file_spec.rb +180 -0
  71. data/spec/helper/test_hosts.rb +68 -0
  72. data/spec/hosts.json +92 -0
  73. data/spec/spec_helper.rb +11 -3
  74. data/spec/unit/fstab_spec.rb +22 -0
  75. data/spec/unit/kanrisuru_spec.rb +9 -0
  76. data/spec/unit/mode_spec.rb +183 -0
  77. data/spec/unit/template_spec.rb +13 -0
  78. data/spec/unit/util_spec.rb +177 -0
  79. data/spec/zz_reboot_spec.rb +46 -0
  80. metadata +136 -13
  81. data/spec/kanrisuru_spec.rb +0 -9
@@ -0,0 +1,412 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kanrisuru
4
+ module Remote
5
+ class Fstab
6
+ include Enumerable
7
+
8
+ def initialize(host, path = '/etc/fstab')
9
+ @host = host
10
+ @path = path
11
+ @file = nil
12
+ @backup = nil
13
+
14
+ init_from_os
15
+ end
16
+
17
+ def [](device)
18
+ get_entry(device)
19
+ end
20
+
21
+ def get_entry(device)
22
+ result = @entries[device]
23
+
24
+ return result[:entry] if result
25
+
26
+ result = nil
27
+
28
+ ## Lookup by uuid or label
29
+ @entries.each do |_, entry|
30
+ result = entry[:entry] if !@result && (entry[:entry].uuid == device || entry[:entry].label == device)
31
+ end
32
+
33
+ result
34
+ end
35
+
36
+ def <<(entry)
37
+ append(entry)
38
+ end
39
+
40
+ def append(entry)
41
+ if entry.instance_of?(Kanrisuru::Remote::Fstab::Entry)
42
+ return if @entries.key?(entry.device)
43
+ elsif entry.instance_of?(String)
44
+ entry = Kanrisuru::Remote::Fstab::Entry.new(host: @host, line: entry)
45
+ return if @entries.key?(entry.device)
46
+ else
47
+ raise ArgumentError, 'Invalid entry type'
48
+ end
49
+
50
+ @entries[entry.device] = {
51
+ entry: entry,
52
+ new: true
53
+ }
54
+
55
+ nil
56
+ end
57
+
58
+ def find_device(device); end
59
+
60
+ def each(&block)
61
+ @entries.each do |_, entry|
62
+ block.call(entry[:entry])
63
+ end
64
+ end
65
+
66
+ ## Only append new entries to file
67
+ def append_file!
68
+ @file.append do |f|
69
+ @entries.each do |_, entry|
70
+ f << entry.to_s if entry[:new]
71
+ end
72
+ end
73
+
74
+ reload!
75
+ end
76
+
77
+ ## Rewrites entire fstab file with new and old entries
78
+ def write_file!
79
+ @file.write do |f|
80
+ @entries.each do |_, entry|
81
+ f << entry.to_s
82
+ end
83
+ end
84
+
85
+ reload!
86
+ end
87
+
88
+ def reload!
89
+ init_from_os
90
+ end
91
+
92
+ def inspect
93
+ format('#<Kanrisuru::Remote::Fstab:0x%<object_id>s @path=%<path>s @entries=%<entries>s>',
94
+ object_id: object_id, path: @path, entries: @entries)
95
+ end
96
+
97
+ private
98
+
99
+ def init_from_os
100
+ @entries = {}
101
+
102
+ raise 'Not implemented' unless @host.os && @host.os.kernel == 'Linux'
103
+
104
+ initialize_linux
105
+ end
106
+
107
+ def initialize_linux
108
+ if @file
109
+ @file.reload!
110
+ else
111
+ @file = @host.file(@path)
112
+ end
113
+
114
+ raise ArgumentError, 'Invalid file' if !@file.exists? || !@file.file?
115
+
116
+ @file.each do |line|
117
+ next if line.strip.chomp.empty?
118
+ next if line =~ /\s*#/
119
+
120
+ entry = Fstab::Entry.new(host: @host, line: line)
121
+ @entries[entry.device] = {
122
+ entry: entry,
123
+ new: false
124
+ }
125
+ end
126
+ end
127
+
128
+ class Entry
129
+ attr_reader :device, :uuid, :invalid, :label, :type, :opts, :freq, :passno
130
+ attr_accessor :mount_point
131
+
132
+ def initialize(opts = {})
133
+ @host = opts[:host]
134
+ @line = opts[:line]
135
+
136
+ @default = nil
137
+
138
+ @device = opts[:device] || nil
139
+ @opts = opts[:opts] || nil
140
+ @label = opts[:label] || nil
141
+ @uuid = opts[:uuid] || nil
142
+ @mount_point = opts[:mount_point] || nil
143
+ @type = opts[:type] || nil
144
+ @freq = opts[:freq] || nil
145
+ @passno = opts[:passno] || nil
146
+
147
+ @changed = false
148
+
149
+ @ucount = 0
150
+ @special = false
151
+ @invalid = false
152
+
153
+ if Kanrisuru::Util.present?(@line) && @line.instance_of?(String)
154
+ parse_line!
155
+ elsif Kanrisuru::Util.present?(@opts) && @opts.instance_of?(String) || @opts.instance_of?(Hash)
156
+ @opts = Kanrisuru::Remote::Fstab::Options.new(@type, @opts)
157
+ end
158
+ end
159
+
160
+ def inspect
161
+ str = '#<Kanrisuru::Remote::Fstab::Entry:0x%<object_id>s ' \
162
+ '@line=%<line>s @device=%<device>s @label=%<label>s' \
163
+ '@uuid=%<uuid>s @freq=%<freq>s @pasno=%<passno>s' \
164
+ '@opts=%<opts>s}>'
165
+
166
+ format(
167
+ str,
168
+ object_id: object_id,
169
+ line: @line,
170
+ device: @device,
171
+ label: @label,
172
+ uuid: @uuid,
173
+ freq: @freq,
174
+ passno: @passno,
175
+ opts: @opts.inspect
176
+ )
177
+ end
178
+
179
+ def valid?
180
+ !@invalid
181
+ end
182
+
183
+ def to_s(override = nil)
184
+ mode = override || @default
185
+
186
+ case mode
187
+ when 'uuid'
188
+ "UUID=#{@uuid} #{@mount_point} #{@type} #{@opts} #{@freq} #{@passno}"
189
+ when 'label'
190
+ "LABEL=#{@label} #{@mount_point} #{@type} #{@opts} #{@freq} #{@passno}"
191
+ else
192
+ "#{@device} #{@mount_point} #{@type} #{@opts} #{@freq} #{@passno}"
193
+ end
194
+ end
195
+
196
+ private
197
+
198
+ def parse_line!
199
+ fsline, mp, @type, opts, freq, passno = @line.split
200
+
201
+ @mount_point = mp
202
+ @freq = freq || '0'
203
+ @passno = passno || '0'
204
+
205
+ @opts = Fstab::Options.new(@type, opts)
206
+
207
+ case @line
208
+ when /^\s*LABEL=/
209
+ @default = 'label'
210
+ parse_label(fsline)
211
+ when /^\s*UUID=/
212
+ @default = 'uuid'
213
+ parse_uuid(fsline)
214
+ when %r{^\s*/dev}
215
+ @default = 'dev'
216
+ parse_dev(fsline)
217
+ else
218
+ # TODO: somewhat risky to assume that everything else
219
+ # can be considered a special device, but validating this
220
+ # is really tricky.
221
+ @special = true
222
+ @device = fsline
223
+ end
224
+
225
+ # Fstab entries not matching real devices have device unknown
226
+ @invalid = (@line.split.count != 6) # invalid entry if < 6 columns
227
+
228
+ if (@uuid.nil? && @label.nil? && !@special) ||
229
+ @device =~ /^unknown_/ ||
230
+ (!@host.inode?(@device) && !@special)
231
+ @invalid = true
232
+ @ucount += 1
233
+ end
234
+
235
+ @invalid = true unless @freq =~ /0|1|2/ && @passno =~ /0|1|2/
236
+ end
237
+
238
+ def parse_label(fsline)
239
+ @label = fsline.split('=').last.strip.chomp
240
+ path = @host.realpath("/dev/disk/by-label/#{@label}").path
241
+
242
+ @device = begin
243
+ "/dev/#{path.split('/').last}"
244
+ rescue StandardError
245
+ "unknown_#{@ucount}"
246
+ end
247
+
248
+ result = @host.blkid(device: @device)
249
+ @uuid = result.success? ? result.uuid : nil
250
+ end
251
+
252
+ def parse_uuid(fsline)
253
+ @uuid = fsline.split('=').last.strip.chomp
254
+ path = @host.realpath("/dev/disk/by-uuid/#{uuid}").path
255
+
256
+ @device = begin
257
+ "/dev/#{path.split('/').last}"
258
+ rescue StandardError
259
+ "unknown_#{@ucount}"
260
+ end
261
+
262
+ result = @host.blkid(device: @device)
263
+ @label = result.success? ? result.label : nil
264
+ end
265
+
266
+ def parse_dev(fsline)
267
+ @device = fsline
268
+ result = @host.blkid(device: @device)
269
+
270
+ @label = result.success? ? result.label : nil
271
+ @uuid = result.success? ? result.uuid : nil
272
+ end
273
+ end
274
+
275
+ class Options
276
+ def initialize(type, opts)
277
+ @type = type
278
+ @valid = false
279
+
280
+ if opts.instance_of?(String)
281
+ @opts = parse_opts(opts)
282
+ elsif opts.instance_of?(Hash)
283
+ @opts = opts.transform_keys(&:to_s)
284
+ else
285
+ raise ArgumentError, 'Invalid option type'
286
+ end
287
+
288
+ validate_opts!
289
+ end
290
+
291
+ def inspect
292
+ format('<Kanrisuru::Remote::Fstab::Options:0x%<object_id>s @opts=%<opts>s @type=%<type>s>',
293
+ object_id: object_id, opts: @opts, type: @type)
294
+ end
295
+
296
+ def [](option)
297
+ @opts[option]
298
+ end
299
+
300
+ def []=(option, value)
301
+ option = option.to_s
302
+
303
+ unless Kanrisuru::Remote::Fstab::Options.option_exists?(option, @type)
304
+ raise ArgumentError,
305
+ "Invalid option: #{option} for #{@type} file system."
306
+ end
307
+
308
+ unless Kanrisuru::Remote::Fstab::Options.valid_option?(option, value, @type)
309
+ raise ArgumentError,
310
+ "Invalid option value: #{value} for #{option} on #{@type} file system."
311
+ end
312
+
313
+ @opts[option] = value
314
+ end
315
+
316
+ def to_s
317
+ string = ''
318
+ opts_length = @opts.length
319
+
320
+ @opts.each_with_index do |(key, value), index|
321
+ append_comma = true
322
+
323
+ if value == true
324
+ string += key.to_s
325
+ elsif value.instance_of?(String) || value.instance_of?(Integer) || value.instance_of?(Float)
326
+ string += "#{key}=#{value}"
327
+ else
328
+ append_comma = false
329
+ end
330
+
331
+ string += ',' if append_comma && index < opts_length - 1
332
+ end
333
+
334
+ string
335
+ end
336
+
337
+ def to_h
338
+ @opts
339
+ end
340
+
341
+ def self.option_exists?(value, type = nil)
342
+ value = value.to_sym
343
+ type = type ? type.to_sym : nil
344
+
345
+ common = Kanrisuru::Util::FsMountOpts[:common]
346
+ fs_opts = Kanrisuru::Util::FsMountOpts[type]
347
+
348
+ common.key?(value) ||
349
+ fs_opts&.key?(value)
350
+ end
351
+
352
+ def self.valid_option?(value, field, type = nil)
353
+ value = value.to_sym
354
+ type = type ? type.to_sym : nil
355
+
356
+ common = Kanrisuru::Util::FsMountOpts[:common]
357
+ fs_opts = Kanrisuru::Util::FsMountOpts[type]
358
+
359
+ if common.key?(value)
360
+ case common[value]
361
+ when 'boolean'
362
+ [true, false].include?(field)
363
+ when 'value'
364
+ field.instance_of?(String) || field.instance_of?(Float) || field.instance_of?(Integer)
365
+ else
366
+ false
367
+ end
368
+ elsif fs_opts&.key?(value)
369
+ case fs_opts[value]
370
+ when 'boolean'
371
+ [true, false].include?(field)
372
+ when 'value'
373
+ field.instance_of?(String) || field.instance_of?(Float) || field.instance_of?(Integer)
374
+ else
375
+ false
376
+ end
377
+ else
378
+ raise ArgumentError, 'Invalid option'
379
+ end
380
+ end
381
+
382
+ private
383
+
384
+ def validate_opts!
385
+ @opts.each do |key, value|
386
+ unless Kanrisuru::Remote::Fstab::Options.valid_option?(key, value, @type)
387
+ raise ArgumentError, "Invalid option: #{key} for #{@type}"
388
+ end
389
+ end
390
+
391
+ @valid = true
392
+ end
393
+
394
+ def parse_opts(string)
395
+ opts = {}
396
+
397
+ options = string.split(',')
398
+ options.each do |option|
399
+ if option.include?('=')
400
+ opt, value = option.split('=')
401
+ opts[opt] = value
402
+ else
403
+ opts[option] = true
404
+ end
405
+ end
406
+
407
+ opts
408
+ end
409
+ end
410
+ end
411
+ end
412
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/ssh'
4
+ require 'net/scp'
5
+ require 'net/ping'
6
+
7
+ module Kanrisuru
8
+ module Remote
9
+ class Host
10
+ extend OsPackage::Include
11
+
12
+ attr_reader :host, :username, :password, :port, :keys
13
+
14
+ def initialize(opts = {})
15
+ @host = opts[:host]
16
+ @username = opts[:username]
17
+ @login_user = @username
18
+
19
+ @port = opts[:port]
20
+ @password = opts[:password] if opts[:password]
21
+ @keys = opts[:keys] if opts[:keys]
22
+ @shell = opts[:shell] || '/bin/bash'
23
+
24
+ @current_dir = ''
25
+ end
26
+
27
+ def remote_user
28
+ @remote_user ||= @username
29
+ end
30
+
31
+ def hostname
32
+ @hostname ||= init_hostname
33
+ end
34
+
35
+ def os
36
+ @os ||= init_os
37
+ end
38
+
39
+ def env
40
+ @env ||= init_env
41
+ end
42
+
43
+ def template(path, args = {})
44
+ Kanrisuru::Template.new(path, args)
45
+ end
46
+
47
+ def fstab(file = '/etc/fstab')
48
+ @fstab ||= init_fstab(file)
49
+ end
50
+
51
+ def chdir(path = '~')
52
+ cd(path)
53
+ end
54
+
55
+ def cd(path = '~')
56
+ @current_dir = pwd.path if Kanrisuru::Util.blank?(@current_dir)
57
+
58
+ @current_dir =
59
+ if path == '~'
60
+ realpath('~').path
61
+ elsif path[0] == '.' || path[0] != '/'
62
+ ## Use strip to preserve symlink directories
63
+ realpath("#{@current_dir}/#{path}", strip: true).path
64
+ else
65
+ ## Use strip to preserve symlink directories
66
+ realpath(path, strip: true).path
67
+ end
68
+ end
69
+
70
+ def cpu
71
+ @cpu ||= init_cpu
72
+ end
73
+
74
+ def memory
75
+ @memory ||= init_memory
76
+ end
77
+
78
+ def su(user)
79
+ @remote_user = user
80
+ end
81
+
82
+ def file(path)
83
+ Kanrisuru::Remote::File.new(path, self)
84
+ end
85
+
86
+ def ssh
87
+ @ssh ||= Net::SSH.start(@host, @username, keys: @keys, password: @password)
88
+ end
89
+
90
+ def ping?
91
+ check = Net::Ping::External.new(@host)
92
+ check.ping?
93
+ end
94
+
95
+ def execute_shell(command)
96
+ command = Kanrisuru::Command.new(command) if command.instance_of?(String)
97
+
98
+ command.remote_user = remote_user
99
+ command.remote_shell = @shell
100
+ command.remote_path = @current_dir
101
+ command.remote_env = env.to_s
102
+
103
+ execute_with_retries(command)
104
+ end
105
+
106
+ def execute(command)
107
+ command = Kanrisuru::Command.new(command) if command.instance_of?(String)
108
+ execute_with_retries(command)
109
+ end
110
+
111
+ def disconnect
112
+ ssh.close
113
+ end
114
+
115
+ private
116
+
117
+ def execute_with_retries(command)
118
+ raise 'Invalid command type' unless command.instance_of?(Kanrisuru::Command)
119
+
120
+ retry_attempts = 3
121
+
122
+ Kanrisuru.logger.debug { "kanrisuru:~$ #{command.prepared_command}" }
123
+
124
+ begin
125
+ channel = ssh.open_channel do |ch|
126
+ ch.exec(command.prepared_command) do |_, success|
127
+ raise "could not execute command: #{command.prepared_command}" unless success
128
+
129
+ ch.on_request('exit-status') do |_, data|
130
+ command.handle_status(data.read_long)
131
+ end
132
+
133
+ ch.on_request('exit-signal') do |_, data|
134
+ command.handle_signal(data.read_long)
135
+ end
136
+
137
+ ch.on_data do |_, data|
138
+ command.handle_data(data)
139
+ end
140
+
141
+ ch.on_extended_data do |_, _type, data|
142
+ command.handle_data(data)
143
+ end
144
+ end
145
+ end
146
+
147
+ channel.wait
148
+
149
+ Kanrisuru.logger.debug { command.to_a }
150
+
151
+ command
152
+ rescue Net::SSH::ConnectionTimeout, Net::SSH::Timeout => e
153
+ if retry_attempts > 1
154
+ retry_attempts -= 1
155
+ retry
156
+ else
157
+ disconnect
158
+ raise e.class
159
+ end
160
+ end
161
+ end
162
+
163
+ def init_hostname
164
+ command = Kanrisuru::Command.new('hostname')
165
+ execute(command)
166
+
167
+ command.success? ? command.to_s : nil
168
+ end
169
+
170
+ def init_env
171
+ Env.new
172
+ end
173
+
174
+ def init_fstab(file)
175
+ Fstab.new(self, file)
176
+ end
177
+
178
+ def init_memory
179
+ Memory.new(self)
180
+ end
181
+
182
+ def init_os
183
+ Os.new(self)
184
+ end
185
+
186
+ def init_cpu
187
+ Cpu.new(self)
188
+ end
189
+ end
190
+ end
191
+ end