confctl 1.0.0

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