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,124 @@
1
+ require 'pathname'
2
+
3
+ module ConfCtl
4
+ # Format Ruby values in Nix
5
+ class NixFormat
6
+ # @param value [any]
7
+ # @param sort [Boolean] sort hash keys
8
+ # @return [String]
9
+ def self.to_nix(value, sort: true)
10
+ new(sort:).to_nix(value)
11
+ end
12
+
13
+ # @param sort [Boolean] sort hash keys
14
+ def initialize(sort: true)
15
+ @sort = sort
16
+ @output = ''
17
+ @level = 0
18
+ end
19
+
20
+ # @param value [any]
21
+ # @return [String]
22
+ def to_nix(value)
23
+ format_value(value).strip
24
+ end
25
+
26
+ def pad(str, indent: true)
27
+ if indent
28
+ "#{' ' * @level}#{str}"
29
+ else
30
+ str
31
+ end
32
+ end
33
+
34
+ def indent
35
+ @level += 2
36
+ yield
37
+ @level -= 2
38
+ end
39
+
40
+ protected
41
+
42
+ def format_value(value, indent: true, semicolon: false, nl: false)
43
+ if value.respond_to?(:to_nix)
44
+ return value.to_nix(nix_format: self, indent:, semicolon:, nl:)
45
+ end
46
+
47
+ case value
48
+ when Hash
49
+ format_hash(value, indent:, semicolon:, nl:)
50
+
51
+ when Array
52
+ format_array(value, indent:, semicolon:, nl:)
53
+
54
+ else
55
+ simple =
56
+ case value
57
+ when String
58
+ value.dump
59
+
60
+ when Symbol
61
+ value.to_s.dump
62
+
63
+ when Numeric, true, false
64
+ value.to_s
65
+
66
+ when Pathname
67
+ str = value.to_s
68
+
69
+ if str.start_with?('/')
70
+ str
71
+ else
72
+ "./#{str}"
73
+ end
74
+
75
+ when nil
76
+ 'null'
77
+
78
+ else
79
+ raise "Unable to format #{value.inspect} (#{value.class}) to nix: " \
80
+ 'implement #to_nix()?'
81
+ end
82
+
83
+ pad("#{simple}#{semicolon ? ';' : ''}#{nl ? "\n" : ''}", indent:)
84
+ end
85
+ end
86
+
87
+ def format_hash(hash, indent: true, semicolon: false, nl: true)
88
+ ret = pad("{\n", indent:)
89
+
90
+ quote_keys = !hash.each_key.all? { |k| /^[a-zA-Z_][a-zA-Z_\-']*$/ =~ k }
91
+
92
+ sorted =
93
+ if @sort
94
+ hash.sort do |a, b|
95
+ a[0] <=> b[0]
96
+ end
97
+ else
98
+ hash
99
+ end
100
+
101
+ indent do
102
+ sorted.each do |k, v|
103
+ ret << (quote_keys ? pad("\"#{k}\"") : pad(k.to_s))
104
+ ret << " = #{format_value(v, indent: false, semicolon: true, nl: false)}\n"
105
+ end
106
+ end
107
+
108
+ ret << pad("}#{semicolon ? ';' : ''}#{nl ? "\n" : ''}")
109
+ ret
110
+ end
111
+
112
+ def format_array(array, indent: true, semicolon: false, nl: true)
113
+ ret = pad("[\n", indent:)
114
+
115
+ indent do
116
+ array.each do |v|
117
+ ret << "#{pad(format_value(v, indent: false, nl: false))}\n"
118
+ end
119
+ end
120
+
121
+ ret << pad("]#{semicolon ? ';' : ''}#{nl ? "\n" : ''}")
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,15 @@
1
+ module ConfCtl
2
+ class NixLiteralExpression
3
+ def initialize(value)
4
+ @value = value
5
+ end
6
+
7
+ def to_s
8
+ @value
9
+ end
10
+
11
+ def to_nix(**)
12
+ @value
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ module ConfCtl
2
+ class ParallelExecutor
3
+ attr_reader :thread_count
4
+
5
+ def initialize(threads)
6
+ @thread_count = threads
7
+ @threads = []
8
+ @queue = Queue.new
9
+ @retvals = []
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def add(&block)
14
+ queue << block
15
+ end
16
+
17
+ def run
18
+ thread_count.times do
19
+ threads << Thread.new { worker }
20
+ end
21
+
22
+ threads.each(&:join)
23
+ retvals
24
+ end
25
+
26
+ protected
27
+
28
+ attr_reader :threads, :queue, :mutex, :retvals
29
+
30
+ def worker
31
+ loop do
32
+ begin
33
+ block = queue.pop(true)
34
+ rescue ThreadError
35
+ return
36
+ end
37
+
38
+ v = block.call
39
+ mutex.synchronize { retvals << v }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ module ConfCtl
2
+ module Pattern
3
+ # @param pattern [String]
4
+ # @param name [String]
5
+ def self.match?(pattern, name)
6
+ File.fnmatch?(pattern, name, File::FNM_EXTGLOB)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,50 @@
1
+ require 'singleton'
2
+
3
+ module ConfCtl
4
+ class Settings
5
+ include Singleton
6
+
7
+ def initialize
8
+ @settings = nil
9
+ end
10
+
11
+ def list_columns
12
+ read_settings { |s| s['list']['columns'] }
13
+ end
14
+
15
+ def max_jobs
16
+ read_settings { |s| s['nix']['maxJobs'] }
17
+ end
18
+
19
+ def nix_paths
20
+ read_settings { |s| s['nix']['nixPath'] }
21
+ end
22
+
23
+ def core_swpin_channels
24
+ read_settings { |s| s['swpins']['core']['channels'] }
25
+ end
26
+
27
+ def core_swpin_pins
28
+ read_settings { |s| s['swpins']['core']['pins'] }
29
+ end
30
+
31
+ def build_generations
32
+ read_settings { |s| s['buildGenerations'] }
33
+ end
34
+
35
+ def host_generations
36
+ read_settings { |s| s['hostGenerations'] }
37
+ end
38
+
39
+ protected
40
+
41
+ def read_settings
42
+ if @settings.nil?
43
+ nix = Nix.stateless
44
+ @settings = nix.confctl_settings
45
+ end
46
+
47
+ yield(@settings)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,40 @@
1
+ module ConfCtl
2
+ # Pair of line buffers for standard output/error
3
+ class StdLineBuffer
4
+ # @yieldparam out [String, nil]
5
+ # @yieldparam err [String, nil]
6
+ def initialize(&block)
7
+ @out_buffer = LineBuffer.new
8
+ @err_buffer = LineBuffer.new
9
+ @block = block
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ # Get a block which can be called to feed data to the buffer
14
+ # @return [Proc]
15
+ def feed_block
16
+ proc do |stdout, stderr|
17
+ @mutex.synchronize do
18
+ out_buffer << stdout if stdout
19
+ err_buffer << stderr if stderr
20
+
21
+ loop do
22
+ out_line = out_buffer.read_line
23
+ err_line = err_buffer.read_line
24
+ break if out_line.nil? && err_line.nil?
25
+
26
+ block.call(out_line, err_line)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def flush
33
+ block.call(out_buffer.flush, err_buffer.flush)
34
+ end
35
+
36
+ protected
37
+
38
+ attr_reader :out_buffer, :err_buffer, :block
39
+ end
40
+ end
@@ -0,0 +1,151 @@
1
+ module ConfCtl
2
+ class Swpins::ChangeSet
3
+ class SpecOwner
4
+ attr_reader :name, :path
5
+ end
6
+
7
+ SpecSet = Struct.new(:spec, :original_version, :original_info, keyword_init: true) do
8
+ def name
9
+ spec.name
10
+ end
11
+
12
+ def new_version
13
+ spec.version
14
+ end
15
+
16
+ def new_info
17
+ spec.info
18
+ end
19
+
20
+ def changed?
21
+ original_version != new_version
22
+ end
23
+
24
+ def string_changelog(type)
25
+ if original_info
26
+ spec.string_changelog_info(type, original_info, color: false)
27
+ else
28
+ "Initial version\n"
29
+ end
30
+ end
31
+ end
32
+
33
+ def initialize
34
+ @owners = {}
35
+ end
36
+
37
+ # @param owner [SpecOwner]
38
+ # @param spec [Swpins::Spec]
39
+ def add(owner, spec)
40
+ @owners[owner] ||= []
41
+ @owners[owner] << SpecSet.new(
42
+ spec:,
43
+ original_version: spec.valid? ? spec.version : nil,
44
+ original_info: spec.valid? ? spec.info : nil
45
+ )
46
+ nil
47
+ end
48
+
49
+ # @param type [:upgrade, :downgrade]
50
+ # @param changelog [Boolean]
51
+ def commit(type: :upgrade, changelog: false)
52
+ return if @owners.empty?
53
+
54
+ ret = Kernel.system(
55
+ 'git',
56
+ 'commit',
57
+ '-e',
58
+ '-m', build_message(type, changelog),
59
+ *changed_files
60
+ )
61
+
62
+ return if ret
63
+
64
+ raise 'git commit exited with non-zero status code'
65
+ end
66
+
67
+ protected
68
+
69
+ def build_message(type, changelog)
70
+ [
71
+ build_title(type, changelog),
72
+ build_description(type, changelog)
73
+ ].join("\n\n")
74
+ end
75
+
76
+ def build_title(type, changelog)
77
+ msg = 'swpins: '
78
+
79
+ if same_changes?
80
+ spec_sets = @owners.first[1]
81
+ msg << "#{@owners.each_key.map(&:name).sort.join(', ')}: update "
82
+
83
+ msg << spec_sets.map do |spec_set|
84
+ "#{spec_set.name} to #{spec_set.new_version}"
85
+ end.join(', ')
86
+ else
87
+ all_spec_names = []
88
+
89
+ @owners.each_value do |spec_sets|
90
+ spec_sets.each do |spec_set|
91
+ all_spec_names << spec_set.name unless all_spec_names.include?(spec_set.name)
92
+ end
93
+ end
94
+
95
+ msg << "#{@owners.each_key.map(&:name).join(', ')}: update #{all_spec_names.sort.join(', ')}"
96
+ end
97
+
98
+ msg
99
+ end
100
+
101
+ def build_description(type, changelog)
102
+ msg = ''
103
+
104
+ @owners.each do |owner, spec_sets|
105
+ spec_sets.each do |spec_set|
106
+ if spec_set.changed?
107
+ msg << "#{owner.name} #{spec_set.name}: "
108
+
109
+ msg << if spec_set.original_version
110
+ "#{spec_set.original_version} -> #{spec_set.new_version}\n"
111
+ else
112
+ "set to #{spec_set.new_version}\n"
113
+ end
114
+
115
+ if changelog
116
+ msg << spec_set.string_changelog(type)
117
+ msg << "\n"
118
+ end
119
+ else
120
+ msg << " #{spec_set.name}: unchanged\n"
121
+ end
122
+ end
123
+
124
+ msg << "\n" if changelog
125
+ end
126
+
127
+ msg
128
+ end
129
+
130
+ def changed_files
131
+ @owners.each_key.map(&:path)
132
+ end
133
+
134
+ def same_changes?
135
+ return true if @owners.length == 1
136
+
137
+ expected_spec_sets = @owners.first[1]
138
+
139
+ @owners.each_value do |spec_sets|
140
+ return false if expected_spec_sets.length != spec_sets.length
141
+
142
+ spec_sets.each do |spec_set|
143
+ expected = expected_spec_sets.detect { |v| v.name == spec_set.name }
144
+ return false if expected.nil? || expected.new_version != spec_set.new_version
145
+ end
146
+ end
147
+
148
+ true
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,62 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+
4
+ module ConfCtl
5
+ class Swpins::Channel
6
+ # @return [String]
7
+ attr_reader :path
8
+
9
+ # @return [String]
10
+ attr_reader :name
11
+
12
+ # @return [Hash<String, Swpins::Specs::Base>]
13
+ attr_reader :specs
14
+
15
+ # @param name [String]
16
+ # @param nix_specs [Hash]
17
+ def initialize(name, nix_specs)
18
+ @name = name
19
+ @path = File.join(ConfCtl::Swpins.channel_dir, "#{name}.json")
20
+ @nix_specs = nix_specs
21
+ end
22
+
23
+ def parse
24
+ @json_specs = if File.exist?(path)
25
+ JSON.parse(File.read(path))
26
+ else
27
+ {}
28
+ end
29
+
30
+ @specs = nix_specs.to_h do |name, nix_opts|
31
+ [
32
+ name,
33
+ Swpins::Spec.for(nix_opts['type'].to_sym).new(
34
+ name,
35
+ nix_opts[nix_opts['type']],
36
+ json_specs[name]
37
+ )
38
+ ]
39
+ end
40
+ end
41
+
42
+ def valid?
43
+ specs.values.all?(&:valid?)
44
+ end
45
+
46
+ def save
47
+ tmp = "#{path}.new"
48
+
49
+ FileUtils.mkdir_p(ConfCtl::Swpins.channel_dir)
50
+
51
+ File.open(tmp, 'w') do |f|
52
+ f.puts(JSON.pretty_generate(specs))
53
+ end
54
+
55
+ File.rename(tmp, path)
56
+ end
57
+
58
+ protected
59
+
60
+ attr_reader :nix_specs, :json_specs
61
+ end
62
+ end
@@ -0,0 +1,47 @@
1
+ module ConfCtl
2
+ class Swpins::ChannelList < Array
3
+ # @return [Swpins::ChannelList]
4
+ def self.get
5
+ @get ||= new
6
+ end
7
+
8
+ # @param pattern [String]
9
+ def self.pattern(pattern)
10
+ get.pattern(pattern)
11
+ end
12
+
13
+ def self.refresh
14
+ get.refresh
15
+ end
16
+
17
+ # @param pattern [String]
18
+ def initialize(pattern: '*')
19
+ super()
20
+ parse(pattern:)
21
+ end
22
+
23
+ # @param pattern [String]
24
+ # @return [Array<Swpins::Channel>]
25
+ def pattern(pattern)
26
+ select { |c| Pattern.match?(pattern, c.name) }
27
+ end
28
+
29
+ def refresh
30
+ clear
31
+ parse
32
+ end
33
+
34
+ protected
35
+
36
+ def parse(pattern: '*')
37
+ nix = Nix.new
38
+ nix.list_swpins_channels.each do |name, nix_specs|
39
+ next unless Pattern.match?(pattern, name)
40
+
41
+ c = Swpins::Channel.new(name, nix_specs)
42
+ c.parse
43
+ self << c
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,94 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+
4
+ module ConfCtl
5
+ class Swpins::ClusterName
6
+ # @return [Machine]
7
+ attr_reader :machine
8
+
9
+ # @return [String]
10
+ attr_reader :name
11
+
12
+ # @return [String]
13
+ attr_reader :path
14
+
15
+ # @return [Hash<String, Swpins::Specs::Base>]
16
+ attr_reader :specs
17
+
18
+ # @return [Array<Swpins::Channel>]
19
+ attr_reader :channels
20
+
21
+ # @param machine [Machine]
22
+ # @param channels [Swpins::ChannelList]
23
+ def initialize(machine, channels)
24
+ @machine = machine
25
+ @name = machine.name
26
+ @path = File.join(ConfCtl::Swpins.cluster_dir, "#{name.gsub('/', ':')}.json")
27
+ @channels = channels.select do |c|
28
+ machine['swpins']['channels'].include?(c.name)
29
+ end
30
+ @nix_specs = machine['swpins']['pins']
31
+ end
32
+
33
+ def parse
34
+ @specs = {}
35
+
36
+ # Add specs from channels
37
+ channels.each do |chan|
38
+ chan.specs.each do |name, chan_spec|
39
+ s = chan_spec.clone
40
+ s.channel = chan.name
41
+ specs[name] = s
42
+ end
43
+ end
44
+
45
+ # Add machine-specific specs
46
+ @json_specs = if File.exist?(path)
47
+ JSON.parse(File.read(path))
48
+ else
49
+ {}
50
+ end
51
+
52
+ nix_specs.each do |name, nix_opts|
53
+ specs[name] = Swpins::Spec.for(nix_opts['type'].to_sym).new(
54
+ name,
55
+ nix_opts[nix_opts['type']],
56
+ json_specs[name]
57
+ )
58
+ end
59
+ end
60
+
61
+ def valid?
62
+ specs.values.all?(&:valid?)
63
+ end
64
+
65
+ def save
66
+ custom = {}
67
+ specs.each do |name, s|
68
+ custom[name] = s unless s.from_channel?
69
+ end
70
+
71
+ if custom.empty?
72
+ begin
73
+ File.unlink(path)
74
+ rescue Errno::ENOENT
75
+ # ignore
76
+ end
77
+ else
78
+ tmp = "#{path}.new"
79
+
80
+ FileUtils.mkdir_p(ConfCtl::Swpins.cluster_dir)
81
+
82
+ File.open(tmp, 'w') do |f|
83
+ f.puts(JSON.pretty_generate(custom))
84
+ end
85
+
86
+ File.rename(tmp, path)
87
+ end
88
+ end
89
+
90
+ protected
91
+
92
+ attr_reader :nix_specs, :json_specs
93
+ end
94
+ end
@@ -0,0 +1,15 @@
1
+ module ConfCtl
2
+ class Swpins::ClusterNameList < Array
3
+ # @param channels [Swpins::ChannelList]
4
+ # @param pattern [String]
5
+ # @param machines [MachineList]
6
+ def initialize(channels: nil, pattern: '*', machines: nil)
7
+ super()
8
+ channels ||= ConfCtl::Swpins::ChannelList.get
9
+
10
+ (machines || MachineList.new).each_value do |machine|
11
+ self << Swpins::ClusterName.new(machine, channels) if Pattern.match?(pattern, machine.name)
12
+ end
13
+ end
14
+ end
15
+ end