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,206 @@
1
+ require 'gli'
2
+ require 'rainbow'
3
+
4
+ module ConfCtl::Cli
5
+ class Command
6
+ def self.run(gli_cmd, klass, method)
7
+ proc do |global_opts, opts, args|
8
+ log = ConfCtl::Logger.instance
9
+ log.open(gli_cmd.name_for_help.join('-'))
10
+ log.cli(
11
+ gli_cmd.name_for_help,
12
+ global_opts,
13
+ opts,
14
+ args
15
+ )
16
+
17
+ cmd = klass.new(global_opts, opts, args)
18
+ cmd.run_method(method)
19
+
20
+ if log.keep?
21
+ log.close
22
+ else
23
+ log.close_and_unlink
24
+ end
25
+ end
26
+ end
27
+
28
+ attr_reader :gopts, :opts, :args
29
+
30
+ def initialize(global_opts, opts, args)
31
+ @gopts = global_opts
32
+ @opts = opts
33
+ @args = args
34
+ @use_color = determine_color
35
+ @use_pager = determine_pager
36
+ end
37
+
38
+ # @param v [Array] list of required arguments
39
+ # @param optional [Array] list of optional arguments
40
+ # @param strict [Boolean] do not allow more arguments than specified
41
+ def require_args!(*required, optional: [], strict: true)
42
+ if args.count < required.count
43
+ arg = required[args.count]
44
+ raise GLI::BadCommandLine, "missing argument <#{arg}>"
45
+
46
+ elsif strict && args.count > (required.count + optional.count)
47
+ unknown = args[(required.count + optional.count)..]
48
+
49
+ msg = ''
50
+
51
+ msg << if unknown.count > 1
52
+ 'unknown arguments: '
53
+ else
54
+ 'unknown argument: '
55
+ end
56
+
57
+ msg << unknown.join(' ')
58
+
59
+ msg << ' (note that options must come before arguments)' if unknown.detect { |v| v.start_with?('-') }
60
+
61
+ raise GLI::BadCommandLine, msg
62
+ end
63
+ end
64
+
65
+ def use_color?
66
+ @use_color
67
+ end
68
+
69
+ def use_pager?
70
+ @use_pager
71
+ end
72
+
73
+ def ask_confirmation(always: false)
74
+ return true if !always && opts[:yes]
75
+
76
+ yield if block_given?
77
+
78
+ loop do
79
+ $stdout.write("\nContinue? [y/N]: ")
80
+ $stdout.flush
81
+
82
+ case $stdin.readline.strip.downcase
83
+ when 'y'
84
+ puts
85
+ return true
86
+ when 'n'
87
+ puts
88
+ return false
89
+ end
90
+ end
91
+ end
92
+
93
+ def ask_confirmation!(...)
94
+ raise 'Aborted' unless ask_confirmation(...)
95
+ end
96
+
97
+ # @param options [Hash<String, String>] key => description
98
+ # @param default [String] default option key
99
+ # @return [String] selection option key
100
+ def ask_action(options:, default:)
101
+ yield if block_given?
102
+
103
+ loop do
104
+ $stdout.puts("\nOptions:\n")
105
+
106
+ options.each do |key, desc|
107
+ $stdout.puts(" [#{key}] #{desc}")
108
+ end
109
+
110
+ keys = options.keys.map do |k|
111
+ if k == default
112
+ k.upcase
113
+ else
114
+ k
115
+ end
116
+ end.join('/')
117
+
118
+ $stdout.write("\nAction: [#{keys}]: ")
119
+ $stdout.flush
120
+
121
+ answer = $stdin.readline.strip.downcase
122
+
123
+ if options.has_key?(answer)
124
+ puts
125
+ return answer
126
+ end
127
+ end
128
+ end
129
+
130
+ def run_method(method)
131
+ self.method(method).call
132
+ end
133
+
134
+ protected
135
+
136
+ def run_command(klass, method)
137
+ c = klass.new(gopts, opts, args)
138
+ c.run_method(method)
139
+ end
140
+
141
+ def determine_color
142
+ case gopts[:color]
143
+ when 'always'
144
+ Rainbow.enabled = true
145
+ true
146
+ when 'never'
147
+ Rainbow.enabled = false
148
+ false
149
+ when 'auto'
150
+ Rainbow.enabled
151
+ end
152
+ end
153
+
154
+ def determine_pager
155
+ ENV.fetch('PAGER', nil) && ENV['PAGER'].strip != ''
156
+ end
157
+
158
+ def select_machines(pattern)
159
+ machines = ConfCtl::MachineList.new(show_trace: opts['show-trace'])
160
+
161
+ attr_filters = AttrFilters.new(opts[:attr])
162
+ tag_filters = TagFilters.new(opts[:tag])
163
+
164
+ machines.select do |host, d|
165
+ (pattern.nil? || ConfCtl::Pattern.match?(pattern, host)) \
166
+ && attr_filters.pass?(d) \
167
+ && tag_filters.pass?(d)
168
+ end
169
+ end
170
+
171
+ def select_machines_with_managed(pattern)
172
+ selected = select_machines(pattern)
173
+
174
+ case opts[:managed]
175
+ when 'n', 'no'
176
+ selected.unmanaged
177
+ when 'a', 'all'
178
+ selected
179
+ else
180
+ selected.managed
181
+ end
182
+ end
183
+
184
+ def list_machines(machines, prepend_cols: [])
185
+ cols =
186
+ if opts[:output]
187
+ opts[:output].split(',')
188
+ else
189
+ ConfCtl::Settings.instance.list_columns
190
+ end
191
+
192
+ cols = prepend_cols + cols if prepend_cols
193
+
194
+ rows = machines.map do |_host, machine|
195
+ cols.to_h { |c| [c, machine[c]] }
196
+ end
197
+
198
+ OutputFormatter.print(
199
+ rows,
200
+ cols,
201
+ header: !opts['hide-header'],
202
+ layout: :columns
203
+ )
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,296 @@
1
+ require 'fileutils'
2
+ require 'securerandom'
3
+
4
+ module ConfCtl::Cli
5
+ class Configuration < Command
6
+ DIR_MODE = 0o755
7
+ FILE_MODE = 0o644
8
+
9
+ def init
10
+ dir = File.realpath(Dir.pwd)
11
+
12
+ Dir.entries(dir).each do |v|
13
+ raise 'init must be called in an empty directory' unless %w[. .. shell.nix .confctl .gems
14
+ .gitignore].include?(v)
15
+ end
16
+
17
+ mkdir('cluster')
18
+
19
+ mkfile('cluster/module-list.nix') do |f|
20
+ f.puts(<<~END
21
+ (import ./cluster.nix) ++ [
22
+ # Place for custom modules
23
+ ]
24
+ END
25
+ )
26
+ end
27
+
28
+ mkfile('cluster/cluster.nix') do |f|
29
+ f.puts(<<~END
30
+ # This file is generated by confctl, changes will be lost
31
+ []
32
+ END
33
+ )
34
+ end
35
+
36
+ mkdir('configs')
37
+ mkfile('configs/confctl.nix') do |f|
38
+ f.puts(<<~END
39
+ { config, ... }:
40
+ {
41
+ confctl = {
42
+ # listColumns = {
43
+ # "name"
44
+ # "spin"
45
+ # "host.fqdn"
46
+ # };
47
+ };
48
+ }
49
+ END
50
+ )
51
+ end
52
+
53
+ mkfile('configs/swpins.nix') do |f|
54
+ f.puts(<<~END
55
+ { config, ... }:
56
+ let
57
+ nixpkgsBranch = branch: {
58
+ type = "git-rev";
59
+
60
+ git-rev = {
61
+ url = "https://github.com/NixOS/nixpkgs";
62
+ update.ref = "refs/heads/${branch}";
63
+ };
64
+ };
65
+
66
+ vpsadminosBranch = branch: {
67
+ type = "git-rev";
68
+
69
+ git-rev = {
70
+ url = "https://github.com/vpsfreecz/vpsadminos";
71
+ update.ref = "refs/heads/${branch}";
72
+ };
73
+ };
74
+ in {
75
+ confctl.swpins.channels = {
76
+ nixos-unstable = { nixpkgs = nixpkgsBranch "nixos-unstable"; };
77
+
78
+ # nixos-stable = { nixpkgs = nixpkgsBranch "nixos-20.09"; };
79
+
80
+ # vpsadminos-staging = { vpsadminos = vpsadminosBranch "staging"; };
81
+ };
82
+ }
83
+ END
84
+ )
85
+ end
86
+
87
+ mkdir('data')
88
+
89
+ mkfile('data/default.nix') do |f|
90
+ f.puts(<<~END
91
+ { lib }:
92
+ {
93
+ # Place to load custom data sets
94
+ sshKeys = import ./ssh-keys.nix;
95
+ }
96
+ END
97
+ )
98
+ end
99
+
100
+ mkfile('data/ssh-keys.nix') do |f|
101
+ f.puts(<<~END
102
+ rec {
103
+ admins = [
104
+ # someone
105
+ ];
106
+
107
+ someone = "...ssh public key...";
108
+ }
109
+ END
110
+ )
111
+ end
112
+
113
+ mkdir('environments')
114
+
115
+ mkfile('environments/base.nix') do |f|
116
+ f.puts(<<~END
117
+ { config, pkgs, confData, ... }:
118
+ {
119
+ time.timeZone = "Europe/Amsterdam";
120
+
121
+ services.openssh.enable = true;
122
+
123
+ environment.systemPackages = with pkgs; [
124
+ vim
125
+ screen
126
+ ];
127
+
128
+ users.users.root.openssh.authorizedKeys.keys = with confData.sshKeys; admins;
129
+ }
130
+ END
131
+ )
132
+ end
133
+
134
+ mkdir('modules')
135
+ mkfile('modules/module-list.nix') do |f|
136
+ f.puts(<<~END
137
+ rec {
138
+ shared = [
139
+ # Modules not dependent on spin
140
+ ];
141
+
142
+ nixos = shared ++ [
143
+ # Modules only for NixOS
144
+ ];
145
+
146
+ vpsadminos = shared ++ [
147
+ # Modules only for vpsAdminOS
148
+ ];
149
+ }
150
+ END
151
+ )
152
+ end
153
+
154
+ mkdir('swpins')
155
+ mkdir('swpins/channels')
156
+ end
157
+
158
+ def add
159
+ require_args!('name')
160
+
161
+ name = args[0]
162
+ dir = File.join('cluster', name)
163
+ depth = name.count('/')
164
+
165
+ raise "#{dir} already exists" if Dir.exist?(dir)
166
+
167
+ mkdir_p(dir)
168
+
169
+ mkfile(File.join(dir, 'module.nix')) do |f|
170
+ f.puts(<<~END
171
+ { config, ... }:
172
+ {
173
+ cluster."#{name}" = {
174
+ spin = "nixos";
175
+ swpins.channels = [ "nixos-unstable" ];
176
+ host = { name = "machine"; domain = "example.com"; };
177
+ };
178
+ }
179
+ END
180
+ )
181
+ end
182
+
183
+ mkfile(File.join(dir, 'config.nix')) do |f|
184
+ f.puts(<<~END
185
+ { config, pkgs, lib, ... }:
186
+ {
187
+ imports = [
188
+ #{'../' * (depth + 2)}environments/base.nix
189
+ ];
190
+
191
+ # ... standard NixOS configuration ...
192
+
193
+ networking.hostName = "#{name.gsub('/', '-')}";
194
+ }
195
+ END
196
+ )
197
+ end
198
+
199
+ rediscover
200
+ end
201
+
202
+ def rename
203
+ require_args!('old-name', 'new-name')
204
+
205
+ src = args[0]
206
+ dst = args[1]
207
+
208
+ src_path = File.join('cluster', src)
209
+ dst_path = File.join('cluster', dst)
210
+
211
+ if !Dir.exist?(src_path)
212
+ raise "'#{src}' not found"
213
+ elsif Dir.exist?(dst_path)
214
+ raise "'#{dst}' already exists"
215
+ end
216
+
217
+ dst_dir = File.dirname(dst_path)
218
+ mkdir_p(dst_dir)
219
+ mv(src_path, dst_path)
220
+
221
+ src_swpins = File.join('swpins/cluster', "#{src.gsub('/', ':')}.json")
222
+ dst_swpins = File.join('swpins/cluster', "#{dst.gsub('/', ':')}.json")
223
+ mv(src_swpins, dst_swpins) if File.exist?(src_swpins)
224
+
225
+ rediscover
226
+ end
227
+
228
+ def rediscover
229
+ hosts = discover_dir('cluster').sort
230
+
231
+ replace_file('cluster/cluster.nix') do |f|
232
+ f.puts('# This file is generated by confctl, changes will be lost')
233
+ f.puts('[')
234
+
235
+ hosts.each do |host|
236
+ f.puts(" ./#{host}/module.nix")
237
+ end
238
+
239
+ f.puts(']')
240
+ end
241
+ end
242
+
243
+ protected
244
+
245
+ def discover_dir(dir_path, rel_path = nil)
246
+ ret = []
247
+
248
+ Dir.entries(dir_path).each do |v|
249
+ entry_abs_path = File.join(dir_path, v)
250
+ next if %w[. ..].include?(v) || !File.directory?(entry_abs_path)
251
+
252
+ entry_rel_path = File.join(*[rel_path, v].compact)
253
+
254
+ if File.exist?(File.join(entry_abs_path, 'module.nix')) \
255
+ && File.exist?(File.join(entry_abs_path, 'config.nix'))
256
+ ret << entry_rel_path
257
+ end
258
+
259
+ ret.concat(discover_dir(entry_abs_path, entry_rel_path))
260
+ end
261
+
262
+ ret
263
+ end
264
+
265
+ def mkdir(name)
266
+ puts "mkdir #{name}"
267
+ Dir.mkdir(name, DIR_MODE)
268
+ end
269
+
270
+ def mkdir_p(name)
271
+ puts "mkdir #{name}"
272
+ FileUtils.mkdir_p(name, mode: DIR_MODE)
273
+ end
274
+
275
+ def mkfile(name)
276
+ puts "mkfile #{name}"
277
+ f = File.open(name, 'w', FILE_MODE)
278
+ yield(f)
279
+ f.close
280
+ end
281
+
282
+ def replace_file(name, &)
283
+ puts "replace #{name}"
284
+ replacement = "#{name}.new-#{SecureRandom.hex(3)}"
285
+
286
+ File.open(replacement, 'w', FILE_MODE, &)
287
+
288
+ File.rename(replacement, name)
289
+ end
290
+
291
+ def mv(old_name, new_name)
292
+ puts "mv #{old_name} #{new_name}"
293
+ File.rename(old_name, new_name)
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,97 @@
1
+ require 'highline/import'
2
+ require 'vpsfree/client'
3
+ require 'confctl/conf_dir'
4
+
5
+ module ConfCtl::Cli
6
+ class GenData < Command
7
+ def vpsadmin_all
8
+ vpsadmin_containers
9
+ vpsadmin_network
10
+ end
11
+
12
+ def vpsadmin_containers
13
+ api = vpsadmin_client
14
+
15
+ machines = ConfCtl::MachineList.new
16
+ data = {}
17
+
18
+ machines.each_value do |m|
19
+ next if m['container'].nil?
20
+
21
+ ct = api.vps.show(
22
+ m['container.id'],
23
+ meta: { includes: 'node__location__environment' }
24
+ )
25
+
26
+ ct_fqdn = [
27
+ m['host.name'],
28
+ m['host.location'],
29
+ m['host.domain']
30
+ ].compact.join('.')
31
+
32
+ data[ct_fqdn] = {
33
+ node: {
34
+ id: ct.node.id,
35
+ name: ct.node.name,
36
+ location: ct.node.location.domain,
37
+ domain: ct.node.location.environment.domain,
38
+ fqdn: "#{ct.node.domain_name}.#{ct.node.location.environment.domain}"
39
+ }
40
+ }
41
+ end
42
+
43
+ update_file('vpsadmin/containers.nix') do |f|
44
+ f.puts(ConfCtl::NixFormat.to_nix(data))
45
+ end
46
+ end
47
+
48
+ def vpsadmin_network
49
+ vpsadmin_network_containers
50
+ end
51
+
52
+ def vpsadmin_network_containers
53
+ api = vpsadmin_client
54
+ networks = api.network.list
55
+ data = {}
56
+
57
+ [4, 6].each do |ip_v|
58
+ data["ipv#{ip_v}"] = networks.select { |net| net.ip_version == ip_v }.map do |net|
59
+ { address: net.address, prefix: net.prefix }
60
+ end
61
+ end
62
+
63
+ update_file('vpsadmin/networks/containers.nix') do |f|
64
+ f.puts(ConfCtl::NixFormat.to_nix(data))
65
+ end
66
+ end
67
+
68
+ protected
69
+
70
+ def vpsadmin_client
71
+ return @vpsadmin_client if @vpsadmin_client
72
+
73
+ @vpsadmin_client = VpsFree::Client.new
74
+
75
+ user = ask('User name: ') { |q| q.default = nil }.to_s
76
+ password = ask('Password: ') do |q|
77
+ q.default = nil
78
+ q.echo = false
79
+ end.to_s
80
+
81
+ @vpsadmin_client.authenticate(:basic, user:, password:)
82
+ @vpsadmin_client
83
+ end
84
+
85
+ def update_file(relpath, &)
86
+ abs = File.join(data_dir, relpath)
87
+ tmp = "#{abs}.new"
88
+
89
+ File.open(tmp, 'w', &)
90
+ File.rename(tmp, abs)
91
+ end
92
+
93
+ def data_dir
94
+ @data_dir ||= File.join(ConfCtl::ConfDir.path, 'data')
95
+ end
96
+ end
97
+ end