kanrisuru 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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