confctl 1.0.0

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 (130) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +11 -0
  3. data/.gitignore +8 -0
  4. data/.overcommit.yml +6 -0
  5. data/.rubocop.yml +67 -0
  6. data/.rubocop_todo.yml +5 -0
  7. data/.ruby-version +1 -0
  8. data/CHANGELOG.md +2 -0
  9. data/Gemfile +2 -0
  10. data/LICENSE.txt +674 -0
  11. data/README.md +522 -0
  12. data/Rakefile +40 -0
  13. data/bin/confctl +4 -0
  14. data/confctl.gemspec +33 -0
  15. data/example/.gitignore +2 -0
  16. data/example/README.md +38 -0
  17. data/example/cluster/cluster.nix +7 -0
  18. data/example/cluster/module-list.nix +3 -0
  19. data/example/cluster/nixos-machine/config.nix +15 -0
  20. data/example/cluster/nixos-machine/hardware.nix +4 -0
  21. data/example/cluster/nixos-machine/module.nix +8 -0
  22. data/example/cluster/vpsadminos-container/config.nix +22 -0
  23. data/example/cluster/vpsadminos-container/module.nix +8 -0
  24. data/example/cluster/vpsadminos-machine/config.nix +22 -0
  25. data/example/cluster/vpsadminos-machine/hardware.nix +4 -0
  26. data/example/cluster/vpsadminos-machine/module.nix +8 -0
  27. data/example/cluster/vpsfreecz-vps/config.nix +25 -0
  28. data/example/cluster/vpsfreecz-vps/module.nix +8 -0
  29. data/example/configs/confctl.nix +10 -0
  30. data/example/configs/swpins.nix +28 -0
  31. data/example/data/default.nix +5 -0
  32. data/example/data/ssh-keys.nix +7 -0
  33. data/example/environments/base.nix +13 -0
  34. data/example/modules/module-list.nix +13 -0
  35. data/example/shell.nix +11 -0
  36. data/example/swpins/channels/nixos-unstable.json +35 -0
  37. data/example/swpins/channels/vpsadminos-staging.json +35 -0
  38. data/lib/confctl/cli/app.rb +551 -0
  39. data/lib/confctl/cli/attr_filters.rb +51 -0
  40. data/lib/confctl/cli/cluster.rb +1248 -0
  41. data/lib/confctl/cli/command.rb +206 -0
  42. data/lib/confctl/cli/configuration.rb +296 -0
  43. data/lib/confctl/cli/gen_data.rb +97 -0
  44. data/lib/confctl/cli/generation.rb +335 -0
  45. data/lib/confctl/cli/log_view.rb +267 -0
  46. data/lib/confctl/cli/output_formatter.rb +288 -0
  47. data/lib/confctl/cli/swpins/base.rb +40 -0
  48. data/lib/confctl/cli/swpins/channel.rb +73 -0
  49. data/lib/confctl/cli/swpins/cluster.rb +80 -0
  50. data/lib/confctl/cli/swpins/core.rb +86 -0
  51. data/lib/confctl/cli/swpins/utils.rb +55 -0
  52. data/lib/confctl/cli/swpins.rb +5 -0
  53. data/lib/confctl/cli/tag_filters.rb +30 -0
  54. data/lib/confctl/cli.rb +5 -0
  55. data/lib/confctl/conf_cache.rb +105 -0
  56. data/lib/confctl/conf_dir.rb +88 -0
  57. data/lib/confctl/erb_template.rb +37 -0
  58. data/lib/confctl/exceptions.rb +3 -0
  59. data/lib/confctl/gcroot.rb +30 -0
  60. data/lib/confctl/generation/build.rb +145 -0
  61. data/lib/confctl/generation/build_list.rb +106 -0
  62. data/lib/confctl/generation/host.rb +35 -0
  63. data/lib/confctl/generation/host_list.rb +81 -0
  64. data/lib/confctl/generation/unified.rb +117 -0
  65. data/lib/confctl/generation/unified_list.rb +63 -0
  66. data/lib/confctl/git_repo_mirror.rb +79 -0
  67. data/lib/confctl/health_checks/base.rb +66 -0
  68. data/lib/confctl/health_checks/run_command.rb +179 -0
  69. data/lib/confctl/health_checks/systemd/properties.rb +84 -0
  70. data/lib/confctl/health_checks/systemd/property_check.rb +31 -0
  71. data/lib/confctl/health_checks/systemd/property_list.rb +20 -0
  72. data/lib/confctl/health_checks.rb +5 -0
  73. data/lib/confctl/hook.rb +35 -0
  74. data/lib/confctl/line_buffer.rb +53 -0
  75. data/lib/confctl/logger.rb +151 -0
  76. data/lib/confctl/machine.rb +107 -0
  77. data/lib/confctl/machine_control.rb +172 -0
  78. data/lib/confctl/machine_list.rb +108 -0
  79. data/lib/confctl/machine_status.rb +135 -0
  80. data/lib/confctl/module_options.rb +95 -0
  81. data/lib/confctl/nix.rb +382 -0
  82. data/lib/confctl/nix_build.rb +108 -0
  83. data/lib/confctl/nix_collect_garbage.rb +64 -0
  84. data/lib/confctl/nix_copy.rb +49 -0
  85. data/lib/confctl/nix_format.rb +124 -0
  86. data/lib/confctl/nix_literal_expression.rb +15 -0
  87. data/lib/confctl/parallel_executor.rb +43 -0
  88. data/lib/confctl/pattern.rb +9 -0
  89. data/lib/confctl/settings.rb +50 -0
  90. data/lib/confctl/std_line_buffer.rb +40 -0
  91. data/lib/confctl/swpins/change_set.rb +151 -0
  92. data/lib/confctl/swpins/channel.rb +62 -0
  93. data/lib/confctl/swpins/channel_list.rb +47 -0
  94. data/lib/confctl/swpins/cluster_name.rb +94 -0
  95. data/lib/confctl/swpins/cluster_name_list.rb +15 -0
  96. data/lib/confctl/swpins/core.rb +137 -0
  97. data/lib/confctl/swpins/deployed_info.rb +23 -0
  98. data/lib/confctl/swpins/spec.rb +20 -0
  99. data/lib/confctl/swpins/specs/base.rb +184 -0
  100. data/lib/confctl/swpins/specs/directory.rb +51 -0
  101. data/lib/confctl/swpins/specs/git.rb +135 -0
  102. data/lib/confctl/swpins/specs/git_rev.rb +24 -0
  103. data/lib/confctl/swpins.rb +17 -0
  104. data/lib/confctl/system_command.rb +10 -0
  105. data/lib/confctl/user_script.rb +13 -0
  106. data/lib/confctl/user_scripts.rb +41 -0
  107. data/lib/confctl/utils/file.rb +21 -0
  108. data/lib/confctl/version.rb +3 -0
  109. data/lib/confctl.rb +43 -0
  110. data/man/man8/confctl-options.nix.8 +1334 -0
  111. data/man/man8/confctl-options.nix.8.md +1340 -0
  112. data/man/man8/confctl.8 +660 -0
  113. data/man/man8/confctl.8.md +654 -0
  114. data/nix/evaluator.nix +160 -0
  115. data/nix/lib/default.nix +83 -0
  116. data/nix/lib/machine/default.nix +74 -0
  117. data/nix/lib/machine/info.nix +5 -0
  118. data/nix/lib/swpins/eval.nix +71 -0
  119. data/nix/lib/swpins/options.nix +94 -0
  120. data/nix/machines.nix +31 -0
  121. data/nix/modules/cluster/default.nix +459 -0
  122. data/nix/modules/confctl/cli.nix +21 -0
  123. data/nix/modules/confctl/generations.nix +84 -0
  124. data/nix/modules/confctl/nix.nix +28 -0
  125. data/nix/modules/confctl/swpins.nix +55 -0
  126. data/nix/modules/module-list.nix +19 -0
  127. data/shell.nix +42 -0
  128. data/template/confctl-options.nix/main.erb +45 -0
  129. data/template/confctl-options.nix/options.erb +15 -0
  130. metadata +353 -0
@@ -0,0 +1,53 @@
1
+ module ConfCtl
2
+ # Feed string data and get output as lines
3
+ class LineBuffer
4
+ # If instantiated with a block, the block is invoked for each read line
5
+ # @yieldparam line [String]
6
+ def initialize(&block)
7
+ @buffer = ''
8
+ @block = block
9
+ end
10
+
11
+ # Feed string
12
+ # @param str [String]
13
+ def <<(str)
14
+ buffer << str
15
+ return if block.nil?
16
+
17
+ loop do
18
+ out_line = read_line
19
+ break if out_line.nil?
20
+
21
+ block.call(out_line)
22
+ end
23
+ end
24
+
25
+ # Read one line if there is one
26
+ # @return [String, nil]
27
+ def read_line
28
+ nl = buffer.index("\n")
29
+ return if nl.nil?
30
+
31
+ line = buffer[0..nl]
32
+ @buffer = buffer[nl + 1..]
33
+ line
34
+ end
35
+
36
+ # Return the buffer's contents and flush it
37
+ #
38
+ # If block was given to {LineBuffer}, it will be invoked with the buffer
39
+ # contents.
40
+ #
41
+ # @return [String]
42
+ def flush
43
+ ret = buffer.clone
44
+ buffer.clear
45
+ block.call(ret) if block
46
+ ret
47
+ end
48
+
49
+ protected
50
+
51
+ attr_reader :buffer, :block
52
+ end
53
+ end
@@ -0,0 +1,151 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+ require 'pp'
4
+ require 'singleton'
5
+
6
+ module ConfCtl
7
+ class Logger
8
+ class << self
9
+ %i[
10
+ open
11
+ close
12
+ unlink
13
+ close_and_unlink
14
+ keep?
15
+ keep
16
+ io
17
+ path
18
+ relative_path
19
+ ].each do |v|
20
+ define_method(v) { |*args, **kwargs| instance.send(v, *args, **kwargs) }
21
+ end
22
+ end
23
+
24
+ include Singleton
25
+
26
+ def initialize
27
+ @mutex = Mutex.new
28
+ @readers = []
29
+ @keep = false
30
+ end
31
+
32
+ def open(name, output: nil)
33
+ if output
34
+ @io = output
35
+ else
36
+ dir = ConfDir.log_dir
37
+ FileUtils.mkdir_p(dir)
38
+
39
+ @io = File.new(File.join(dir, file_name(name)), 'w')
40
+ end
41
+ end
42
+
43
+ def open?
44
+ !@io.nil?
45
+ end
46
+
47
+ def close
48
+ raise 'log file not open' if @io.nil?
49
+
50
+ @io.close
51
+ end
52
+
53
+ def io
54
+ raise 'log file not open' if @io.nil?
55
+
56
+ @io
57
+ end
58
+
59
+ def path
60
+ raise 'log file not open' if @io.nil?
61
+
62
+ @io.path
63
+ end
64
+
65
+ def relative_path
66
+ return @relative_path if @relative_path
67
+
68
+ abs = Pathname.new(path)
69
+ dir = Pathname.new(ConfDir.path)
70
+ abs.relative_path_from(dir).to_s
71
+ end
72
+
73
+ def keep?
74
+ @keep
75
+ end
76
+
77
+ def keep
78
+ @keep = true
79
+ end
80
+
81
+ def unlink
82
+ raise 'log file not open' if @io.nil?
83
+
84
+ File.unlink(@io.path)
85
+ end
86
+
87
+ def close_and_unlink
88
+ close
89
+ unlink
90
+ end
91
+
92
+ def write(str)
93
+ sync do
94
+ @io << str
95
+ @io.flush
96
+ end
97
+
98
+ readers.each { |r| r << str }
99
+ end
100
+
101
+ def <<(str)
102
+ write(str)
103
+ end
104
+
105
+ def cli(cmd, gopts, opts, args)
106
+ sync do
107
+ PP.pp({
108
+ command: cmd,
109
+ global_options: prune_opts(gopts.clone),
110
+ command_options: prune_opts(opts.clone),
111
+ arguments: args
112
+ }, @io)
113
+ end
114
+ end
115
+
116
+ # @param obj [#<<]
117
+ def add_reader(obj)
118
+ @readers << obj
119
+ end
120
+
121
+ # @param obj [#<<]
122
+ def remove_reader(obj)
123
+ @readers.delete(obj)
124
+ end
125
+
126
+ protected
127
+
128
+ attr_reader :mutex, :readers
129
+
130
+ def sync(&)
131
+ if mutex.owned?
132
+ yield
133
+ else
134
+ mutex.synchronize(&)
135
+ end
136
+ end
137
+
138
+ def file_name(name)
139
+ n = [
140
+ Time.now.strftime('%Y-%m-%d--%H-%M-%S'),
141
+ 'confctl',
142
+ name
143
+ ].compact.join('-')
144
+ "#{n}.log"
145
+ end
146
+
147
+ def prune_opts(hash)
148
+ hash.delete_if { |k, _v| k.is_a?(::Symbol) }
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,107 @@
1
+ module ConfCtl
2
+ class Machine
3
+ attr_reader :name, :safe_name, :managed, :spin, :opts
4
+
5
+ # @param opts [Hash]
6
+ def initialize(opts)
7
+ @opts = opts
8
+ @name = opts['name']
9
+ @safe_name = opts['name'].gsub('/', ':')
10
+ @managed = opts['managed']
11
+ @spin = opts['spin']
12
+ end
13
+
14
+ def target_host
15
+ (opts['host'] && opts['host']['target']) || name
16
+ end
17
+
18
+ def localhost?
19
+ target_host == 'localhost'
20
+ end
21
+
22
+ def nix_paths
23
+ opts['nix']['nixPath'].to_h do |v|
24
+ eq = v.index('=')
25
+ raise "'#{v}' is not a valid nix path entry " if eq.nil?
26
+
27
+ [v[0..eq - 1], v[eq + 1..]]
28
+ end
29
+ end
30
+
31
+ def health_checks
32
+ return @health_checks if @health_checks
33
+
34
+ @health_checks = []
35
+
36
+ opts['healthChecks'].each do |type, checks|
37
+ case type
38
+ when 'systemd'
39
+ next if !checks['enable'] || spin != 'nixos'
40
+
41
+ if checks['systemProperties'].any?
42
+ @health_checks << HealthChecks::Systemd::Properties.new(
43
+ self,
44
+ property_checks: checks['systemProperties'].map do |v|
45
+ HealthChecks::Systemd::PropertyCheck.new(v)
46
+ end
47
+ )
48
+ end
49
+
50
+ checks['unitProperties'].each do |unit_name, prop_checks|
51
+ health_checks << HealthChecks::Systemd::Properties.new(
52
+ self,
53
+ pattern: unit_name,
54
+ property_checks: prop_checks.map do |v|
55
+ HealthChecks::Systemd::PropertyCheck.new(v)
56
+ end
57
+ )
58
+ end
59
+
60
+ when 'builderCommands', 'machineCommands'
61
+ checks.each do |cmd|
62
+ health_checks << HealthChecks::RunCommand.new(
63
+ self,
64
+ HealthChecks::RunCommand::Command.new(self, cmd),
65
+ remote: type == 'machineCommands'
66
+ )
67
+ end
68
+
69
+ else
70
+ raise "Unsupported health-check type #{type.inspect}"
71
+ end
72
+ end
73
+
74
+ @health_checks
75
+ end
76
+
77
+ def [](key)
78
+ if key.index('.')
79
+ get(opts, key.split('.'))
80
+ elsif key == 'checks'
81
+ health_checks.length
82
+ else
83
+ opts[key]
84
+ end
85
+ end
86
+
87
+ def to_s
88
+ name
89
+ end
90
+
91
+ protected
92
+
93
+ def get(hash, keys)
94
+ k = keys.shift
95
+
96
+ return unless hash.has_key?(k)
97
+
98
+ if keys.empty?
99
+ hash[k]
100
+ elsif hash[k].nil?
101
+ nil
102
+ else
103
+ get(hash[k], keys)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,172 @@
1
+ require 'etc'
2
+
3
+ module ConfCtl
4
+ class MachineControl
5
+ # @return [Machine]
6
+ attr_reader :machine
7
+
8
+ # @param machine [Machine]
9
+ def initialize(machine)
10
+ @machine = machine
11
+ @extra_ssh_opts = []
12
+ @cmd = SystemCommand.new
13
+ end
14
+
15
+ # Try to open SSH connection
16
+ # @raise [TTY::Command::ExitError]
17
+ def test_connection
18
+ with_ssh_opts(
19
+ '-o', 'ConnectTimeout=3',
20
+ '-o', 'ServerAliveInterval=3',
21
+ '-o', 'ServerAliveCountMax=1'
22
+ ) { uptime }
23
+ end
24
+
25
+ def interactive_shell
26
+ if machine.localhost?
27
+ Kernel.system(Etc.getpwuid(0).shell)
28
+ else
29
+ Kernel.system(*ssh_args)
30
+ end
31
+ end
32
+
33
+ # Reboot the machine
34
+ def reboot
35
+ execute!('reboot')
36
+ end
37
+
38
+ # Reboot the machine and wait for it to come back online
39
+ # @param timeout [Integer, nil]
40
+ # @yieldparam [:reboot, :went_down, :is_down, :is_up, :timeout] state
41
+ # @return [Integer] seconds it took to reboot the machine
42
+ def reboot_and_wait(timeout: nil)
43
+ initial_uptime = uptime
44
+ t = Time.now
45
+ went_down = false
46
+ reboot
47
+ yield :reboot, timeout if block_given?
48
+
49
+ loop do
50
+ state = nil
51
+
52
+ begin
53
+ current_uptime = with_ssh_opts(
54
+ '-o', 'ConnectTimeout=3',
55
+ '-o', 'ServerAliveInterval=3',
56
+ '-o', 'ServerAliveCountMax=1'
57
+ ) { uptime }
58
+
59
+ if current_uptime < initial_uptime
60
+ yield :is_up, nil
61
+ return Time.now - t
62
+ end
63
+ rescue TTY::Command::ExitError
64
+ if went_down
65
+ state = :is_down
66
+ else
67
+ state = :went_down
68
+ went_down = true
69
+ end
70
+ end
71
+
72
+ if timeout
73
+ timeleft = (t + timeout) - Time.now
74
+
75
+ raise 'timeout' if timeleft <= 0
76
+
77
+ yield state, timeleft if block_given?
78
+ elsif block_given?
79
+ yield state, nil
80
+ end
81
+
82
+ sleep(0.3)
83
+ end
84
+ end
85
+
86
+ # @return [Integer] uptime in seconds
87
+ def uptime
88
+ read_file('/proc/uptime').strip.split[0].to_f
89
+ end
90
+
91
+ # @return [Array<String>]
92
+ def timezone
93
+ out, = run_cmd('date', '+%Z;%z')
94
+ out.strip.split(';')
95
+ end
96
+
97
+ # @param path [String]
98
+ # @return [String]
99
+ def read_file(path)
100
+ out, = run_cmd('cat', path)
101
+ out
102
+ end
103
+
104
+ # @param path [String]
105
+ # @return [String]
106
+ def read_symlink(path)
107
+ out, = run_cmd('readlink', path)
108
+ out.strip
109
+ end
110
+
111
+ # Execute command, raises exception on error
112
+ # @yieldparam out [String]
113
+ # @yieldparam err [String]
114
+ # @raise [TTY::Command::ExitError]
115
+ # @return [TTY::Command::Result]
116
+ def execute(...)
117
+ run_cmd(...)
118
+ end
119
+
120
+ # Execute command, no exception raised on error
121
+ # @yieldparam out [String]
122
+ # @yieldparam err [String]
123
+ # @return [TTY::Command::Result]
124
+ def execute!(...)
125
+ run_cmd!(...)
126
+ end
127
+
128
+ # @param script [String]
129
+ # @return [TTY::Command::Result]
130
+ def bash_script(script)
131
+ run_cmd('bash', '--norc', input: script)
132
+ end
133
+
134
+ protected
135
+
136
+ attr_reader :cmd, :extra_ssh_opts
137
+
138
+ def run_cmd(...)
139
+ do_run_cmd(:run, ...)
140
+ end
141
+
142
+ def run_cmd!(...)
143
+ do_run_cmd(:run!, ...)
144
+ end
145
+
146
+ # rubocop:disable Style/ArgumentsForwarding
147
+
148
+ def do_run_cmd(method, *command, **kwargs, &)
149
+ args =
150
+ if machine.localhost?
151
+ command
152
+ else
153
+ ssh_args + command
154
+ end
155
+
156
+ cmd.method(method).call(*args, **kwargs, &)
157
+ end
158
+
159
+ # rubocop:enable Style/ArgumentsForwarding
160
+
161
+ def with_ssh_opts(*opts)
162
+ @extra_ssh_opts = opts
163
+ ret = yield
164
+ @extra_ssh_opts = []
165
+ ret
166
+ end
167
+
168
+ def ssh_args
169
+ ['ssh', '-l', 'root'] + extra_ssh_opts + [machine.target_host]
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,108 @@
1
+ require 'json'
2
+
3
+ module ConfCtl
4
+ class MachineList
5
+ # @param machine [Machine]
6
+ # @return [MachineList]
7
+ def self.from_machine(machine)
8
+ new(machines: { machine.name => machine })
9
+ end
10
+
11
+ # @param opts [Hash]
12
+ # @option opts [Boolean] :show_trace
13
+ # @option opts [MachineList] :machines
14
+ def initialize(opts = {})
15
+ @opts = opts
16
+ @machines = opts[:machines] || parse(extract)
17
+ end
18
+
19
+ # @yieldparam [String] host
20
+ # @yieldparam [Machine] machine
21
+ def each(&)
22
+ machines.each(&)
23
+ end
24
+
25
+ # @yieldparam [String] host
26
+ def each_key(&)
27
+ machines.each_key(&)
28
+ end
29
+
30
+ # @yieldparam [Machine] machine
31
+ def each_value(&)
32
+ machines.each_value(&)
33
+ end
34
+
35
+ # @yieldparam [String] host
36
+ # @yieldparam [Machine] machine
37
+ # @return [MachineList]
38
+ def select(&)
39
+ self.class.new(machines: machines.select(&))
40
+ end
41
+
42
+ # @yieldparam [String] host
43
+ # @yieldparam [Machine] machine
44
+ # @return [Array]
45
+ def map(&)
46
+ machines.map(&)
47
+ end
48
+
49
+ # @return [MachineList]
50
+ def managed
51
+ select { |_host, machine| machine.managed }
52
+ end
53
+
54
+ # @return [MachineList]
55
+ def unmanaged
56
+ select { |_host, machine| !machine.managed }
57
+ end
58
+
59
+ # @param host [String]
60
+ def [](host)
61
+ @machines[host]
62
+ end
63
+
64
+ # @return [Machine, nil]
65
+ def first
66
+ @machines.each_value.first
67
+ end
68
+
69
+ # @return [Integer]
70
+ def length
71
+ @machines.length
72
+ end
73
+
74
+ def empty?
75
+ @machines.empty?
76
+ end
77
+
78
+ def any?
79
+ !empty?
80
+ end
81
+
82
+ # @return [Array<HealthChecks::Base>]
83
+ def health_checks
84
+ checks = []
85
+
86
+ machines.each_value do |machine|
87
+ checks.concat(machine.health_checks)
88
+ end
89
+
90
+ checks
91
+ end
92
+
93
+ protected
94
+
95
+ attr_reader :opts, :machines
96
+
97
+ def extract
98
+ nix = Nix.new
99
+ nix.list_machines
100
+ end
101
+
102
+ def parse(data)
103
+ data.transform_values do |info|
104
+ Machine.new(info)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,135 @@
1
+ require 'tty-command'
2
+
3
+ module ConfCtl
4
+ class MachineStatus
5
+ class SwpinState
6
+ # @return [Swpins::Specs::Base]
7
+ attr_reader :target_spec
8
+
9
+ # @return [Hash, nil]
10
+ attr_reader :current_info
11
+
12
+ # @param target_spec [Swpins::Specs::Base]
13
+ # @param current_info [Hash, nil]
14
+ def initialize(target_spec, current_info)
15
+ @target_spec = target_spec
16
+ @current_info = current_info
17
+ @uptodate =
18
+ if current_info
19
+ target_spec.check_info(current_info)
20
+ else
21
+ false
22
+ end
23
+ end
24
+
25
+ def uptodate?
26
+ @uptodate
27
+ end
28
+
29
+ def outdated?
30
+ !uptodate?
31
+ end
32
+
33
+ # @return [String, nil]
34
+ def target_version
35
+ target_spec.version
36
+ end
37
+
38
+ # @return [String, nil]
39
+ def current_version
40
+ target_spec.version_info(current_info) || 'unknown'
41
+ end
42
+ end
43
+
44
+ # @return [Machine]
45
+ attr_reader :machine
46
+
47
+ # @return [Boolean]
48
+ attr_reader :status
49
+
50
+ # @return [Boolean]
51
+ attr_reader :online
52
+ alias online? online
53
+
54
+ # @return [Float]
55
+ attr_reader :uptime
56
+
57
+ # @return [String]
58
+ attr_accessor :target_toplevel
59
+
60
+ # @return [String]
61
+ attr_reader :current_toplevel
62
+
63
+ # @return [String]
64
+ attr_reader :timezone_name
65
+
66
+ # @return [String]
67
+ attr_reader :timezone_offset
68
+
69
+ # @return [Generation::HostList]
70
+ attr_reader :generations
71
+
72
+ # @return [Hash]
73
+ attr_accessor :target_swpin_specs
74
+
75
+ # @return [Hash]
76
+ attr_reader :swpins_info
77
+
78
+ # @return [Hash]
79
+ attr_reader :swpins_state
80
+
81
+ # @param machine [Machine]
82
+ def initialize(machine)
83
+ @machine = machine
84
+ @mc = MachineControl.new(machine)
85
+ end
86
+
87
+ # Connect to the machine and query its state
88
+ def query(toplevel: true, generations: true)
89
+ begin
90
+ @uptime = mc.uptime
91
+ rescue TTY::Command::ExitError
92
+ return
93
+ end
94
+
95
+ if toplevel
96
+ begin
97
+ @current_toplevel = mc.read_symlink('/run/current-system')
98
+ rescue TTY::Command::ExitError
99
+ return
100
+ end
101
+ end
102
+
103
+ if generations
104
+ begin
105
+ @generations = Generation::HostList.fetch(mc)
106
+ rescue TTY::Command::ExitError
107
+ return
108
+ end
109
+ end
110
+
111
+ begin
112
+ @swpins_info = Swpins::DeployedInfo.parse!(mc.read_file('/etc/confctl/swpins-info.json'))
113
+ rescue Error
114
+ nil
115
+ end
116
+ end
117
+
118
+ def evaluate
119
+ @swpins_state = {}
120
+
121
+ target_swpin_specs.each do |name, spec|
122
+ swpins_state[name] = SwpinState.new(spec, swpins_info && swpins_info[name])
123
+ end
124
+
125
+ outdated_swpins = swpins_state.detect { |_k, v| v.outdated? }
126
+ @online = uptime ? true : false
127
+ @status = online? && !outdated_swpins
128
+ @status = false if target_toplevel && target_toplevel != current_toplevel
129
+ end
130
+
131
+ protected
132
+
133
+ attr_reader :mc
134
+ end
135
+ end