confctl 1.0.0 → 2.1.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +1 -1
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +1 -0
  5. data/CHANGELOG.md +30 -1
  6. data/README.md +4 -9
  7. data/confctl.gemspec +14 -14
  8. data/docs/carrier.md +150 -0
  9. data/lib/confctl/cli/app.rb +19 -0
  10. data/lib/confctl/cli/cluster.rb +214 -49
  11. data/lib/confctl/cli/configuration.rb +7 -2
  12. data/lib/confctl/cli/gen_data.rb +19 -1
  13. data/lib/confctl/cli/generation.rb +47 -16
  14. data/lib/confctl/generation/build.rb +42 -1
  15. data/lib/confctl/generation/build_list.rb +10 -0
  16. data/lib/confctl/generation/host.rb +9 -5
  17. data/lib/confctl/generation/host_list.rb +22 -7
  18. data/lib/confctl/generation/unified.rb +5 -0
  19. data/lib/confctl/generation/unified_list.rb +10 -0
  20. data/lib/confctl/git_repo_mirror.rb +2 -2
  21. data/lib/confctl/machine.rb +105 -11
  22. data/lib/confctl/machine_control.rb +10 -2
  23. data/lib/confctl/machine_list.rb +18 -1
  24. data/lib/confctl/machine_status.rb +51 -4
  25. data/lib/confctl/nix.rb +90 -22
  26. data/lib/confctl/nix_copy.rb +5 -5
  27. data/lib/confctl/null_logger.rb +7 -0
  28. data/lib/confctl/swpins/specs/git.rb +1 -1
  29. data/lib/confctl/swpins/specs/git_rev.rb +1 -1
  30. data/lib/confctl/system_command.rb +3 -2
  31. data/lib/confctl/version.rb +1 -1
  32. data/libexec/auto-rollback.rb +106 -0
  33. data/man/man8/confctl-options.nix.8 +165 -1
  34. data/man/man8/confctl-options.nix.8.md +165 -1
  35. data/man/man8/confctl.8 +109 -73
  36. data/man/man8/confctl.8.md +86 -55
  37. data/nix/evaluator.nix +26 -7
  38. data/nix/lib/default.nix +64 -17
  39. data/nix/lib/machine/default.nix +14 -11
  40. data/nix/lib/machine/info.nix +3 -3
  41. data/nix/modules/cluster/default.nix +162 -3
  42. data/nix/modules/confctl/carrier/base.nix +35 -0
  43. data/nix/modules/confctl/carrier/carrier-env.rb +81 -0
  44. data/nix/modules/confctl/carrier/netboot/build-netboot-server.rb +962 -0
  45. data/nix/modules/confctl/carrier/netboot/nixos.nix +185 -0
  46. data/nix/modules/confctl/kexec-netboot/default.nix +36 -0
  47. data/nix/modules/confctl/kexec-netboot/kexec-netboot.8.adoc +62 -0
  48. data/nix/modules/confctl/kexec-netboot/kexec-netboot.rb +455 -0
  49. data/nix/modules/system-list.nix +10 -0
  50. metadata +17 -7
  51. data/.ruby-version +0 -1
@@ -16,6 +16,9 @@ module ConfCtl
16
16
  # @return [String]
17
17
  attr_reader :toplevel
18
18
 
19
+ # @return [String]
20
+ attr_reader :auto_rollback
21
+
19
22
  # @return [Array<String>]
20
23
  attr_reader :swpin_names
21
24
 
@@ -29,22 +32,28 @@ module ConfCtl
29
32
  # @return [Boolean]
30
33
  attr_accessor :current
31
34
 
35
+ # @return [String, nil]
36
+ attr_reader :kernel_version
37
+
32
38
  # @param host [String]
33
39
  def initialize(host)
34
40
  @host = host
35
41
  end
36
42
 
37
43
  # @param toplevel [String]
44
+ # @param auto_rollback [String]
38
45
  # @param swpin_paths [Hash]
39
46
  # @param swpin_specs [Hash]
40
47
  # @param date [Time]
41
- def create(toplevel, swpin_paths, swpin_specs, date: nil)
48
+ def create(toplevel, auto_rollback, swpin_paths, swpin_specs, date: nil)
42
49
  @toplevel = toplevel
50
+ @auto_rollback = auto_rollback
43
51
  @swpin_names = swpin_paths.keys
44
52
  @swpin_paths = swpin_paths
45
53
  @swpin_specs = swpin_specs
46
54
  @date = date || Time.now
47
55
  @name = date.strftime('%Y-%m-%d--%H-%M-%S')
56
+ @kernel_version = extract_kernel_version
48
57
  end
49
58
 
50
59
  # @param name [String]
@@ -53,6 +62,7 @@ module ConfCtl
53
62
 
54
63
  cfg = JSON.parse(File.read(config_path))
55
64
  @toplevel = cfg['toplevel']
65
+ @auto_rollback = cfg['auto_rollback']
56
66
 
57
67
  @swpin_names = []
58
68
  @swpin_paths = {}
@@ -69,6 +79,7 @@ module ConfCtl
69
79
  end
70
80
 
71
81
  @date = Time.iso8601(cfg['date'])
82
+ @kernel_version = extract_kernel_version
72
83
  rescue StandardError => e
73
84
  raise Error, "invalid generation '#{name}': #{e.message}"
74
85
  end
@@ -76,6 +87,7 @@ module ConfCtl
76
87
  def save
77
88
  FileUtils.mkdir_p(dir)
78
89
  File.symlink(toplevel, toplevel_path)
90
+ File.symlink(auto_rollback, auto_rollback_path)
79
91
 
80
92
  swpin_paths.each do |name, path|
81
93
  File.symlink(path, swpin_path(name))
@@ -85,6 +97,7 @@ module ConfCtl
85
97
  f.puts(JSON.pretty_generate({
86
98
  date: date.iso8601,
87
99
  toplevel:,
100
+ auto_rollback:,
88
101
  swpins: swpin_paths.to_h do |name, path|
89
102
  [name, { path:, spec: swpin_specs[name].as_json }]
90
103
  end
@@ -97,6 +110,13 @@ module ConfCtl
97
110
  def destroy
98
111
  remove_gcroot
99
112
  File.unlink(toplevel_path)
113
+
114
+ begin
115
+ File.unlink(auto_rollback_path)
116
+ rescue Errno::ENOENT
117
+ # Older generations might not have auto_rollback
118
+ end
119
+
100
120
  swpin_paths.each_key { |name| File.unlink(swpin_path(name)) }
101
121
  File.unlink(config_path)
102
122
  Dir.rmdir(dir)
@@ -104,6 +124,7 @@ module ConfCtl
104
124
 
105
125
  def add_gcroot
106
126
  GCRoot.add(gcroot_name('toplevel'), toplevel_path)
127
+ GCRoot.add(gcroot_name('auto_rollback'), auto_rollback_path)
107
128
  swpin_paths.each_key do |name|
108
129
  GCRoot.add(gcroot_name("swpin.#{name}"), toplevel_path)
109
130
  end
@@ -111,6 +132,7 @@ module ConfCtl
111
132
 
112
133
  def remove_gcroot
113
134
  GCRoot.remove(gcroot_name('toplevel'))
135
+ GCRoot.remove(gcroot_name('auto_rollback'))
114
136
  swpin_paths.each_key do |name|
115
137
  GCRoot.remove(gcroot_name("swpin.#{name}"))
116
138
  end
@@ -130,6 +152,10 @@ module ConfCtl
130
152
  @toplevel_path ||= File.join(dir, 'toplevel')
131
153
  end
132
154
 
155
+ def auto_rollback_path
156
+ @auto_rollback_path ||= File.join(dir, 'auto_rollback')
157
+ end
158
+
133
159
  def swpin_path(name)
134
160
  File.join(dir, "#{name}.swpin")
135
161
  end
@@ -141,5 +167,20 @@ module ConfCtl
141
167
  def gcroot_name(file)
142
168
  "#{escaped_host}-generation-#{name}-#{file}"
143
169
  end
170
+
171
+ def extract_kernel_version
172
+ # `kernel` is for NixOS/vpsAdminOS and also carried NixOS machines (netboot)
173
+ # `bzImage` is for carried vpsAdminOS machines (netboot)
174
+ %w[kernel bzImage].each do |v|
175
+ link = File.readlink(File.join(toplevel, v))
176
+ next unless %r{\A/nix/store/[^-]+-linux-([^/]+)} =~ link
177
+
178
+ return ::Regexp.last_match(1)
179
+ rescue Errno::ENOENT, Errno::EINVAL
180
+ next
181
+ end
182
+
183
+ nil
184
+ end
144
185
  end
145
186
  end
@@ -55,6 +55,16 @@ module ConfCtl
55
55
  index[name]
56
56
  end
57
57
 
58
+ # @param offset [Integer] 0 = current/last, 1 = first (oldest), -1 = before last
59
+ # @return [Generation::Build]
60
+ def at_offset(offset)
61
+ if offset == 0
62
+ generations.last
63
+ else
64
+ generations[offset - 1]
65
+ end
66
+ end
67
+
58
68
  def each(&)
59
69
  generations.each(&)
60
70
  end
@@ -1,19 +1,22 @@
1
1
  module ConfCtl
2
2
  class Generation::Host
3
- attr_reader :host, :profile, :id, :toplevel, :date, :current
3
+ attr_reader :host, :profile, :id, :toplevel, :date, :kernel_version, :current
4
4
 
5
- # @param host [String]
5
+ # @param machine [Machine]
6
6
  # @param profile [String]
7
7
  # @param id [Integer]
8
8
  # @param toplevel [String]
9
9
  # @param date [Time]
10
+ # @param kernel_version [String, nil]
10
11
  # @param mc [MachineControl]
11
- def initialize(host, profile, id, toplevel, date, current: false, mc: nil)
12
- @host = host
12
+ def initialize(machine, profile, id, toplevel, date, kernel_version, current: false, mc: nil)
13
+ @host = machine.name
14
+ @machine = machine
13
15
  @profile = profile
14
16
  @id = id
15
17
  @toplevel = toplevel
16
18
  @date = date
19
+ @kernel_version = kernel_version
17
20
  @current = current
18
21
  @mc = mc
19
22
  end
@@ -25,7 +28,8 @@ module ConfCtl
25
28
  def destroy
26
29
  raise 'machine control not available' if mc.nil?
27
30
 
28
- mc.execute('nix-env', '-p', profile, '--delete-generations', id.to_s)
31
+ env_cmd = @machine.carried? ? 'carrier-env' : 'nix-env'
32
+ mc.execute(env_cmd, '-p', profile, '--delete-generations', id.to_s)
29
33
  end
30
34
 
31
35
  protected
@@ -1,24 +1,33 @@
1
1
  module ConfCtl
2
2
  class Generation::HostList
3
+ # @parma machine [Machine]
3
4
  # @param mc [MachineControl]
5
+ # @param profile [String]
4
6
  # @return [Generation::HostList]
5
- def self.fetch(mc, profile: '/nix/var/nix/profiles/system')
6
- out, = mc.bash_script(<<-END
7
+ def self.fetch(machine, mc, profile:)
8
+ out, = mc.bash_script(<<~END)
7
9
  realpath #{profile}
8
10
 
9
11
  for generation in `ls -d -1 #{profile}-*-link` ; do
10
- echo "$generation;$(readlink $generation);$(stat --format=%Y $generation)"
12
+ echo -n "$generation;"
13
+ echo -n "$(readlink $generation);"
14
+ echo -n "$(stat --format=%Y $generation);"
15
+
16
+ for kernel_file in kernel bzImage ; do
17
+ [ -h "$generation/$kernel_file" ] && echo -n $(readlink "$generation/$kernel_file")
18
+ done
19
+
20
+ echo
11
21
  done
12
22
  END
13
- )
14
23
 
15
- list = new(mc.machine.name)
24
+ list = new(machine.name)
16
25
  lines = out.strip.split("\n")
17
26
  current_path = lines.shift
18
27
  id_rx = /^#{Regexp.escape(profile)}-(\d+)-link$/
19
28
 
20
29
  lines.each do |line|
21
- link, path, created_at = line.split(';')
30
+ link, path, created_at, kernel = line.split(';')
22
31
 
23
32
  if id_rx =~ link
24
33
  id = ::Regexp.last_match(1).to_i
@@ -27,12 +36,18 @@ module ConfCtl
27
36
  next
28
37
  end
29
38
 
39
+ kernel_version =
40
+ if kernel && %r{\A/nix/store/[^-]+-linux-([^/]+)} =~ kernel
41
+ ::Regexp.last_match(1)
42
+ end
43
+
30
44
  list << Generation::Host.new(
31
- mc.machine.name,
45
+ machine,
32
46
  profile,
33
47
  id,
34
48
  path,
35
49
  Time.at(created_at.to_i),
50
+ kernel_version,
36
51
  current: path == current_path,
37
52
  mc:
38
53
  )
@@ -15,6 +15,9 @@ module ConfCtl
15
15
  # @return [Time]
16
16
  attr_reader :date
17
17
 
18
+ # @return [String, nil]
19
+ attr_reader :kernel_version
20
+
18
21
  # @return [Boolean]
19
22
  attr_reader :current
20
23
 
@@ -37,11 +40,13 @@ module ConfCtl
37
40
  @name = build_generation.name
38
41
  @toplevel = build_generation.toplevel
39
42
  @date = build_generation.date
43
+ @kernel_version = build_generation.kernel_version
40
44
  @current ||= build_generation.current
41
45
  elsif host_generation
42
46
  @name = host_generation.approx_name
43
47
  @toplevel = host_generation.toplevel
44
48
  @date = host_generation.date
49
+ @kernel_version = host_generation.kernel_version
45
50
  @current ||= host_generation.current
46
51
  else
47
52
  raise ArgumentError, 'set build or host'
@@ -46,6 +46,16 @@ module ConfCtl
46
46
  generations.each(&)
47
47
  end
48
48
 
49
+ # @param offset [Integer] 0 = current/last, 1 = first (oldest), -1 = before last
50
+ # @return [Generation::Unified]
51
+ def at_offset(offset)
52
+ if offset == 0
53
+ generations.last
54
+ else
55
+ generations[offset - 1]
56
+ end
57
+ end
58
+
49
59
  def delete_if(&)
50
60
  generations.delete_if(&)
51
61
  end
@@ -14,13 +14,13 @@ module ConfCtl
14
14
  @cmd = SystemCommand.new
15
15
  end
16
16
 
17
- def setup
17
+ def setup(ref: nil)
18
18
  File.stat(mirror_path)
19
19
  rescue Errno::ENOENT
20
20
  FileUtils.mkdir_p(mirror_path)
21
21
  git('clone', args: ['--mirror', url, mirror_path])
22
22
  else
23
- git_repo('fetch')
23
+ git_repo('fetch', opts: ['--no-show-forced-updates'], args: (ref ? ['origin', ref] : []))
24
24
  end
25
25
 
26
26
  def revision_parse(str)
@@ -1,26 +1,114 @@
1
1
  module ConfCtl
2
2
  class Machine
3
- attr_reader :name, :safe_name, :managed, :spin, :opts
3
+ CarriedMachine = Struct.new(
4
+ :carrier,
5
+ :name,
6
+ :alias,
7
+ :attribute,
8
+ keyword_init: true
9
+ )
10
+
11
+ # @return [String]
12
+ attr_reader :name
13
+
14
+ # @return [String]
15
+ attr_reader :safe_name
16
+
17
+ # @return [Boolean]
18
+ attr_reader :managed
19
+
20
+ # @return [String]
21
+ attr_reader :spin
22
+
23
+ # @return [String]
24
+ attr_reader :carrier_name
25
+
26
+ # @return [String]
27
+ attr_reader :cluster_name
28
+
29
+ # @return [String]
30
+ attr_reader :safe_cluster_name
31
+
32
+ # Alias for this machine on the carrier
33
+ # @return [String]
34
+ attr_reader :carried_alias
35
+
36
+ # Alias for this machine on the carrier
37
+ # @return [String]
38
+ attr_reader :safe_carried_alias
39
+
40
+ # @return [Hash] machine metadata
41
+ attr_reader :meta
4
42
 
5
43
  # @param opts [Hash]
6
- def initialize(opts)
7
- @opts = opts
44
+ # @param machine_list [MachineList]
45
+ def initialize(opts, machine_list:)
46
+ @meta = opts['metaConfig']
8
47
  @name = opts['name']
9
- @safe_name = opts['name'].gsub('/', ':')
10
- @managed = opts['managed']
11
- @spin = opts['spin']
48
+ @safe_name = name.gsub('/', ':')
49
+ @managed = meta['managed']
50
+ @spin = meta['spin']
51
+ @is_carrier = meta.fetch('carrier', {}).fetch('enable', false)
52
+ @carrier_name = opts['carrier']
53
+ @cluster_name = opts['clusterName']
54
+ @safe_cluster_name = cluster_name.gsub('/', ':')
55
+ @carried_alias = opts['alias'] || @cluster_name
56
+ @safe_carried_alias = @carried_alias.gsub('/', ':')
57
+ @machine_list = machine_list
58
+ end
59
+
60
+ # True if this machine carries other machines
61
+ def carrier?
62
+ @is_carrier
63
+ end
64
+
65
+ # @return [Array<CarriedMachine>]
66
+ def carried_machines
67
+ meta.fetch('carrier', {}).fetch('machines', []).map do |m|
68
+ CarriedMachine.new(
69
+ carrier: self,
70
+ name: m['machine'],
71
+ alias: m['alias'] || m['machine'],
72
+ attribute: m['attribute']
73
+ )
74
+ end
75
+ end
76
+
77
+ # True if this machine is on a carrier
78
+ def carried?
79
+ !@carrier_name.nil?
80
+ end
81
+
82
+ # @return [Machine] carrier
83
+ def carrier_machine
84
+ carrier = @machine_list[@carrier_name]
85
+
86
+ if carrier.nil?
87
+ raise "Carrier #{@carrier_name} not found in machine list"
88
+ end
89
+
90
+ carrier
12
91
  end
13
92
 
14
93
  def target_host
15
- (opts['host'] && opts['host']['target']) || name
94
+ meta.fetch('host', {}).fetch('target', name)
16
95
  end
17
96
 
18
97
  def localhost?
19
98
  target_host == 'localhost'
20
99
  end
21
100
 
101
+ # @return [String] path to nix-env managed profile
102
+ def profile
103
+ if carried?
104
+ "/nix/var/nix/profiles/confctl-#{safe_carried_alias}"
105
+ else
106
+ '/nix/var/nix/profiles/system'
107
+ end
108
+ end
109
+
22
110
  def nix_paths
23
- opts['nix']['nixPath'].to_h do |v|
111
+ meta['nix']['nixPath'].to_h do |v|
24
112
  eq = v.index('=')
25
113
  raise "'#{v}' is not a valid nix path entry " if eq.nil?
26
114
 
@@ -28,12 +116,16 @@ module ConfCtl
28
116
  end
29
117
  end
30
118
 
119
+ def auto_rollback?
120
+ meta.fetch('autoRollback', {}).fetch('enable', true)
121
+ end
122
+
31
123
  def health_checks
32
124
  return @health_checks if @health_checks
33
125
 
34
126
  @health_checks = []
35
127
 
36
- opts['healthChecks'].each do |type, checks|
128
+ meta['healthChecks'].each do |type, checks|
37
129
  case type
38
130
  when 'systemd'
39
131
  next if !checks['enable'] || spin != 'nixos'
@@ -76,11 +168,13 @@ module ConfCtl
76
168
 
77
169
  def [](key)
78
170
  if key.index('.')
79
- get(opts, key.split('.'))
171
+ get(meta, key.split('.'))
172
+ elsif key == 'name'
173
+ name
80
174
  elsif key == 'checks'
81
175
  health_checks.length
82
176
  else
83
- opts[key]
177
+ meta[key]
84
178
  end
85
179
  end
86
180
 
@@ -6,10 +6,11 @@ module ConfCtl
6
6
  attr_reader :machine
7
7
 
8
8
  # @param machine [Machine]
9
- def initialize(machine)
9
+ # @param logger [#<<]
10
+ def initialize(machine, logger: nil)
10
11
  @machine = machine
11
12
  @extra_ssh_opts = []
12
- @cmd = SystemCommand.new
13
+ @cmd = SystemCommand.new(logger:)
13
14
  end
14
15
 
15
16
  # Try to open SSH connection
@@ -108,6 +109,13 @@ module ConfCtl
108
109
  out.strip
109
110
  end
110
111
 
112
+ # @param path [String]
113
+ # @return [String]
114
+ def read_realpath(path)
115
+ out, = run_cmd('realpath', path)
116
+ out.strip
117
+ end
118
+
111
119
  # Execute command, raises exception on error
112
120
  # @yieldparam out [String]
113
121
  # @yieldparam err [String]
@@ -46,6 +46,12 @@ module ConfCtl
46
46
  machines.map(&)
47
47
  end
48
48
 
49
+ # @yieldparam [Machine] machine
50
+ # @return [Hash]
51
+ def transform_values(&)
52
+ machines.transform_values(&)
53
+ end
54
+
49
55
  # @return [MachineList]
50
56
  def managed
51
57
  select { |_host, machine| machine.managed }
@@ -56,6 +62,11 @@ module ConfCtl
56
62
  select { |_host, machine| !machine.managed }
57
63
  end
58
64
 
65
+ # @return [MachineList]
66
+ def runnable
67
+ select { |_host, machine| !machine.carried? && machine.target_host }
68
+ end
69
+
59
70
  # @param host [String]
60
71
  def [](host)
61
72
  @machines[host]
@@ -79,11 +90,17 @@ module ConfCtl
79
90
  !empty?
80
91
  end
81
92
 
93
+ def to_a
94
+ @machines.values
95
+ end
96
+
82
97
  # @return [Array<HealthChecks::Base>]
83
98
  def health_checks
84
99
  checks = []
85
100
 
86
101
  machines.each_value do |machine|
102
+ next if machine.carried?
103
+
87
104
  checks.concat(machine.health_checks)
88
105
  end
89
106
 
@@ -101,7 +118,7 @@ module ConfCtl
101
118
 
102
119
  def parse(data)
103
120
  data.transform_values do |info|
104
- Machine.new(info)
121
+ Machine.new(info, machine_list: self)
105
122
  end
106
123
  end
107
124
  end
@@ -1,3 +1,4 @@
1
+ require 'json'
1
2
  require 'tty-command'
2
3
 
3
4
  module ConfCtl
@@ -81,7 +82,7 @@ module ConfCtl
81
82
  # @param machine [Machine]
82
83
  def initialize(machine)
83
84
  @machine = machine
84
- @mc = MachineControl.new(machine)
85
+ @mc = MachineControl.new(machine.carried? ? machine.carrier_machine : machine)
85
86
  end
86
87
 
87
88
  # Connect to the machine and query its state
@@ -94,7 +95,7 @@ module ConfCtl
94
95
 
95
96
  if toplevel
96
97
  begin
97
- @current_toplevel = mc.read_symlink('/run/current-system')
98
+ @current_toplevel = query_toplevel
98
99
  rescue TTY::Command::ExitError
99
100
  return
100
101
  end
@@ -102,14 +103,14 @@ module ConfCtl
102
103
 
103
104
  if generations
104
105
  begin
105
- @generations = Generation::HostList.fetch(mc)
106
+ @generations = Generation::HostList.fetch(machine, mc, profile: machine.profile)
106
107
  rescue TTY::Command::ExitError
107
108
  return
108
109
  end
109
110
  end
110
111
 
111
112
  begin
112
- @swpins_info = Swpins::DeployedInfo.parse!(mc.read_file('/etc/confctl/swpins-info.json'))
113
+ @swpins_info = query_swpins
113
114
  rescue Error
114
115
  nil
115
116
  end
@@ -131,5 +132,51 @@ module ConfCtl
131
132
  protected
132
133
 
133
134
  attr_reader :mc
135
+
136
+ def query_toplevel
137
+ path =
138
+ if machine.carried?
139
+ machine.profile
140
+ else
141
+ '/run/current-system'
142
+ end
143
+
144
+ mc.read_realpath(path)
145
+ end
146
+
147
+ def query_swpins
148
+ json =
149
+ if machine.carried?
150
+ query_carried_swpins
151
+ else
152
+ mc.read_file('/etc/confctl/swpins-info.json')
153
+ end
154
+
155
+ case json
156
+ when String
157
+ Swpins::DeployedInfo.parse!(json)
158
+ when Hash
159
+ json
160
+ end
161
+ end
162
+
163
+ # @return [Hash, String]
164
+ def query_carried_swpins
165
+ begin
166
+ json = mc.read_file(File.join(machine.profile, 'machine.json'))
167
+ parsed = JSON.parse(json)
168
+ return parsed['swpins-info'] if parsed['swpins-info']
169
+ rescue TTY::Command::ExitError
170
+ # pass
171
+ end
172
+
173
+ begin
174
+ return mc.read_file(File.join(machine.profile, '/etc/confctl/swpins-info.json'))
175
+ rescue TTY::Command::ExitError
176
+ # pass
177
+ end
178
+
179
+ nil
180
+ end
134
181
  end
135
182
  end