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.
- checksums.yaml +7 -0
- data/.editorconfig +11 -0
- data/.gitignore +8 -0
- data/.overcommit.yml +6 -0
- data/.rubocop.yml +67 -0
- data/.rubocop_todo.yml +5 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +674 -0
- data/README.md +522 -0
- data/Rakefile +40 -0
- data/bin/confctl +4 -0
- data/confctl.gemspec +33 -0
- data/example/.gitignore +2 -0
- data/example/README.md +38 -0
- data/example/cluster/cluster.nix +7 -0
- data/example/cluster/module-list.nix +3 -0
- data/example/cluster/nixos-machine/config.nix +15 -0
- data/example/cluster/nixos-machine/hardware.nix +4 -0
- data/example/cluster/nixos-machine/module.nix +8 -0
- data/example/cluster/vpsadminos-container/config.nix +22 -0
- data/example/cluster/vpsadminos-container/module.nix +8 -0
- data/example/cluster/vpsadminos-machine/config.nix +22 -0
- data/example/cluster/vpsadminos-machine/hardware.nix +4 -0
- data/example/cluster/vpsadminos-machine/module.nix +8 -0
- data/example/cluster/vpsfreecz-vps/config.nix +25 -0
- data/example/cluster/vpsfreecz-vps/module.nix +8 -0
- data/example/configs/confctl.nix +10 -0
- data/example/configs/swpins.nix +28 -0
- data/example/data/default.nix +5 -0
- data/example/data/ssh-keys.nix +7 -0
- data/example/environments/base.nix +13 -0
- data/example/modules/module-list.nix +13 -0
- data/example/shell.nix +11 -0
- data/example/swpins/channels/nixos-unstable.json +35 -0
- data/example/swpins/channels/vpsadminos-staging.json +35 -0
- data/lib/confctl/cli/app.rb +551 -0
- data/lib/confctl/cli/attr_filters.rb +51 -0
- data/lib/confctl/cli/cluster.rb +1248 -0
- data/lib/confctl/cli/command.rb +206 -0
- data/lib/confctl/cli/configuration.rb +296 -0
- data/lib/confctl/cli/gen_data.rb +97 -0
- data/lib/confctl/cli/generation.rb +335 -0
- data/lib/confctl/cli/log_view.rb +267 -0
- data/lib/confctl/cli/output_formatter.rb +288 -0
- data/lib/confctl/cli/swpins/base.rb +40 -0
- data/lib/confctl/cli/swpins/channel.rb +73 -0
- data/lib/confctl/cli/swpins/cluster.rb +80 -0
- data/lib/confctl/cli/swpins/core.rb +86 -0
- data/lib/confctl/cli/swpins/utils.rb +55 -0
- data/lib/confctl/cli/swpins.rb +5 -0
- data/lib/confctl/cli/tag_filters.rb +30 -0
- data/lib/confctl/cli.rb +5 -0
- data/lib/confctl/conf_cache.rb +105 -0
- data/lib/confctl/conf_dir.rb +88 -0
- data/lib/confctl/erb_template.rb +37 -0
- data/lib/confctl/exceptions.rb +3 -0
- data/lib/confctl/gcroot.rb +30 -0
- data/lib/confctl/generation/build.rb +145 -0
- data/lib/confctl/generation/build_list.rb +106 -0
- data/lib/confctl/generation/host.rb +35 -0
- data/lib/confctl/generation/host_list.rb +81 -0
- data/lib/confctl/generation/unified.rb +117 -0
- data/lib/confctl/generation/unified_list.rb +63 -0
- data/lib/confctl/git_repo_mirror.rb +79 -0
- data/lib/confctl/health_checks/base.rb +66 -0
- data/lib/confctl/health_checks/run_command.rb +179 -0
- data/lib/confctl/health_checks/systemd/properties.rb +84 -0
- data/lib/confctl/health_checks/systemd/property_check.rb +31 -0
- data/lib/confctl/health_checks/systemd/property_list.rb +20 -0
- data/lib/confctl/health_checks.rb +5 -0
- data/lib/confctl/hook.rb +35 -0
- data/lib/confctl/line_buffer.rb +53 -0
- data/lib/confctl/logger.rb +151 -0
- data/lib/confctl/machine.rb +107 -0
- data/lib/confctl/machine_control.rb +172 -0
- data/lib/confctl/machine_list.rb +108 -0
- data/lib/confctl/machine_status.rb +135 -0
- data/lib/confctl/module_options.rb +95 -0
- data/lib/confctl/nix.rb +382 -0
- data/lib/confctl/nix_build.rb +108 -0
- data/lib/confctl/nix_collect_garbage.rb +64 -0
- data/lib/confctl/nix_copy.rb +49 -0
- data/lib/confctl/nix_format.rb +124 -0
- data/lib/confctl/nix_literal_expression.rb +15 -0
- data/lib/confctl/parallel_executor.rb +43 -0
- data/lib/confctl/pattern.rb +9 -0
- data/lib/confctl/settings.rb +50 -0
- data/lib/confctl/std_line_buffer.rb +40 -0
- data/lib/confctl/swpins/change_set.rb +151 -0
- data/lib/confctl/swpins/channel.rb +62 -0
- data/lib/confctl/swpins/channel_list.rb +47 -0
- data/lib/confctl/swpins/cluster_name.rb +94 -0
- data/lib/confctl/swpins/cluster_name_list.rb +15 -0
- data/lib/confctl/swpins/core.rb +137 -0
- data/lib/confctl/swpins/deployed_info.rb +23 -0
- data/lib/confctl/swpins/spec.rb +20 -0
- data/lib/confctl/swpins/specs/base.rb +184 -0
- data/lib/confctl/swpins/specs/directory.rb +51 -0
- data/lib/confctl/swpins/specs/git.rb +135 -0
- data/lib/confctl/swpins/specs/git_rev.rb +24 -0
- data/lib/confctl/swpins.rb +17 -0
- data/lib/confctl/system_command.rb +10 -0
- data/lib/confctl/user_script.rb +13 -0
- data/lib/confctl/user_scripts.rb +41 -0
- data/lib/confctl/utils/file.rb +21 -0
- data/lib/confctl/version.rb +3 -0
- data/lib/confctl.rb +43 -0
- data/man/man8/confctl-options.nix.8 +1334 -0
- data/man/man8/confctl-options.nix.8.md +1340 -0
- data/man/man8/confctl.8 +660 -0
- data/man/man8/confctl.8.md +654 -0
- data/nix/evaluator.nix +160 -0
- data/nix/lib/default.nix +83 -0
- data/nix/lib/machine/default.nix +74 -0
- data/nix/lib/machine/info.nix +5 -0
- data/nix/lib/swpins/eval.nix +71 -0
- data/nix/lib/swpins/options.nix +94 -0
- data/nix/machines.nix +31 -0
- data/nix/modules/cluster/default.nix +459 -0
- data/nix/modules/confctl/cli.nix +21 -0
- data/nix/modules/confctl/generations.nix +84 -0
- data/nix/modules/confctl/nix.nix +28 -0
- data/nix/modules/confctl/swpins.nix +55 -0
- data/nix/modules/module-list.nix +19 -0
- data/shell.nix +42 -0
- data/template/confctl-options.nix/main.erb +45 -0
- data/template/confctl-options.nix/options.erb +15 -0
- metadata +353 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
module ConfCtl
|
|
5
|
+
# Cache the list of configuration files to detect changes
|
|
6
|
+
#
|
|
7
|
+
# The assumption is that if there hasn't been any changes in configuration
|
|
8
|
+
# directory, we can reuse previously built artefacts, such as the list of
|
|
9
|
+
# machines, etc. This is much faster than invoking nix-build to query
|
|
10
|
+
# the machines.
|
|
11
|
+
class ConfCache
|
|
12
|
+
# @param conf_dir [ConfDir]
|
|
13
|
+
def initialize(conf_dir)
|
|
14
|
+
@conf_dir = conf_dir
|
|
15
|
+
@cmd = SystemCommand.new
|
|
16
|
+
@cache_dir = File.join(conf_dir.cache_dir, 'build')
|
|
17
|
+
@cache_file = File.join(@cache_dir, 'git-files.json')
|
|
18
|
+
@files = {}
|
|
19
|
+
@loaded = false
|
|
20
|
+
@uptodate = nil
|
|
21
|
+
@checked_at = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Load file list from cache file
|
|
25
|
+
def load_cache
|
|
26
|
+
begin
|
|
27
|
+
data = File.read(@cache_file)
|
|
28
|
+
rescue Errno::ENOENT
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@files = JSON.parse(data)['files']
|
|
33
|
+
@loaded = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Check if cached file list differs from files on disk
|
|
37
|
+
# @param force [Boolean] force a new check
|
|
38
|
+
def uptodate?(force: false)
|
|
39
|
+
return @uptodate if !@uptodate.nil? && !force
|
|
40
|
+
|
|
41
|
+
@uptodate = check_uptodate
|
|
42
|
+
@uptodate
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Update cache file with the current state of the configuration directory
|
|
46
|
+
def update
|
|
47
|
+
@files.clear
|
|
48
|
+
|
|
49
|
+
list_files.each do |file|
|
|
50
|
+
begin
|
|
51
|
+
st = File.lstat(file)
|
|
52
|
+
rescue Errno::ENOENT
|
|
53
|
+
next
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@files[file] = {
|
|
57
|
+
'mtime' => st.mtime.to_i,
|
|
58
|
+
'size' => st.size
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
tmp = "#{@cache_file}.new"
|
|
63
|
+
|
|
64
|
+
FileUtils.mkpath(@cache_dir)
|
|
65
|
+
File.write(tmp, { 'files' => @files }.to_json)
|
|
66
|
+
File.rename(tmp, @cache_file)
|
|
67
|
+
|
|
68
|
+
@uptodate = true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [Time, nil]
|
|
72
|
+
def mtime
|
|
73
|
+
File.lstat(@cache_file).mtime
|
|
74
|
+
rescue Errno::ENOENT
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
protected
|
|
79
|
+
|
|
80
|
+
def check_uptodate
|
|
81
|
+
load_cache unless @loaded
|
|
82
|
+
return false if @files.empty?
|
|
83
|
+
|
|
84
|
+
list_files.each do |file_path|
|
|
85
|
+
file = @files[file_path]
|
|
86
|
+
return false if file.nil?
|
|
87
|
+
|
|
88
|
+
begin
|
|
89
|
+
st = File.lstat(file_path)
|
|
90
|
+
rescue Errno::ENOENT
|
|
91
|
+
return false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return false if file['mtime'] != st.mtime.to_i || file['size'] != st.size
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def list_files
|
|
101
|
+
out, = @cmd.run('git', '-C', @path, 'ls-files', '-z')
|
|
102
|
+
out.strip.split("\0")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
require 'singleton'
|
|
3
|
+
|
|
4
|
+
module ConfCtl
|
|
5
|
+
class ConfDir
|
|
6
|
+
include Singleton
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
%i[
|
|
10
|
+
path
|
|
11
|
+
hash
|
|
12
|
+
short_hash
|
|
13
|
+
cache_dir
|
|
14
|
+
generation_dir
|
|
15
|
+
log_dir
|
|
16
|
+
user_script_dir
|
|
17
|
+
changed?
|
|
18
|
+
unchanged?
|
|
19
|
+
state_mtime
|
|
20
|
+
update_state
|
|
21
|
+
].each do |v|
|
|
22
|
+
define_method(v) do |*args, **kwargs, &block|
|
|
23
|
+
instance.send(v, *args, **kwargs, &block)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
@cache = ConfCache.new(self)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Path to the directory containing cluster configuration
|
|
33
|
+
# @return [String]
|
|
34
|
+
def path
|
|
35
|
+
@path ||= File.realpath(Dir.pwd)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Unique hash identifying the configuration based on its filesystem path
|
|
39
|
+
# @return [String]
|
|
40
|
+
def hash
|
|
41
|
+
@hash ||= Digest::SHA256.hexdigest(path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Shorter prefix of {hash}
|
|
45
|
+
# @return [String]
|
|
46
|
+
def short_hash
|
|
47
|
+
@short_hash ||= hash[0..7]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Path to configuration-specific cache directory
|
|
51
|
+
# @return [String]
|
|
52
|
+
def cache_dir
|
|
53
|
+
@cache_dir ||= File.join(path, '.confctl')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Path to directory with build generations
|
|
57
|
+
# @return [String]
|
|
58
|
+
def generation_dir
|
|
59
|
+
@generation_dir ||= File.join(cache_dir, 'generations')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Path to configuration-specific log directory
|
|
63
|
+
# @return [String]
|
|
64
|
+
def log_dir
|
|
65
|
+
@log_dir ||= File.join(cache_dir, 'logs')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def user_script_dir
|
|
69
|
+
@user_script_dir ||= File.join(path, 'scripts')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def changed?
|
|
73
|
+
!unchanged?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def unchanged?
|
|
77
|
+
@cache.uptodate?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def state_mtime
|
|
81
|
+
@cache.mtime
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def update_state
|
|
85
|
+
@cache.update
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'erb'
|
|
2
|
+
|
|
3
|
+
module ConfCtl
|
|
4
|
+
class ErbTemplate
|
|
5
|
+
def self.render(name, vars)
|
|
6
|
+
t = new(name, vars)
|
|
7
|
+
t.render
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.render_to(name, vars, path)
|
|
11
|
+
File.write("#{path}.new", render(name, vars))
|
|
12
|
+
File.rename("#{path}.new", path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(name, vars)
|
|
16
|
+
@_tpl = ERB.new(
|
|
17
|
+
File.read(
|
|
18
|
+
File.join(ConfCtl.root, 'template', "#{name}.erb")
|
|
19
|
+
), trim_mode: '-'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
vars.each do |k, v|
|
|
23
|
+
if v.is_a?(Proc)
|
|
24
|
+
define_singleton_method(k, &v)
|
|
25
|
+
elsif v.is_a?(Method)
|
|
26
|
+
define_singleton_method(k) { |*args| v.call(*args) }
|
|
27
|
+
else
|
|
28
|
+
define_singleton_method(k) { v }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render
|
|
34
|
+
@_tpl.result(binding)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'confctl/utils/file'
|
|
2
|
+
require 'etc'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module ConfCtl
|
|
6
|
+
module GCRoot
|
|
7
|
+
extend Utils::File
|
|
8
|
+
|
|
9
|
+
def self.dir
|
|
10
|
+
File.join(
|
|
11
|
+
'/nix/var/nix/gcroots/per-user',
|
|
12
|
+
Etc.getlogin,
|
|
13
|
+
"confctl-#{ConfDir.short_hash}"
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.exist?(name)
|
|
18
|
+
File.symlink?(File.join(dir, name))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.add(name, path)
|
|
22
|
+
FileUtils.mkdir_p(dir)
|
|
23
|
+
File.symlink(path, File.join(dir, name))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.remove(name)
|
|
27
|
+
unlink_if_exists(File.join(dir, name))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module ConfCtl
|
|
6
|
+
class Generation::Build
|
|
7
|
+
# @return [String]
|
|
8
|
+
attr_reader :host
|
|
9
|
+
|
|
10
|
+
# @return [String]
|
|
11
|
+
attr_reader :name
|
|
12
|
+
|
|
13
|
+
# @return [Time]
|
|
14
|
+
attr_reader :date
|
|
15
|
+
|
|
16
|
+
# @return [String]
|
|
17
|
+
attr_reader :toplevel
|
|
18
|
+
|
|
19
|
+
# @return [Array<String>]
|
|
20
|
+
attr_reader :swpin_names
|
|
21
|
+
|
|
22
|
+
# @return [Hash]
|
|
23
|
+
attr_reader :swpin_paths
|
|
24
|
+
|
|
25
|
+
# @return [Hash]
|
|
26
|
+
attr_reader :swpin_specs
|
|
27
|
+
|
|
28
|
+
# @param current [Boolean]
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
attr_accessor :current
|
|
31
|
+
|
|
32
|
+
# @param host [String]
|
|
33
|
+
def initialize(host)
|
|
34
|
+
@host = host
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param toplevel [String]
|
|
38
|
+
# @param swpin_paths [Hash]
|
|
39
|
+
# @param swpin_specs [Hash]
|
|
40
|
+
# @param date [Time]
|
|
41
|
+
def create(toplevel, swpin_paths, swpin_specs, date: nil)
|
|
42
|
+
@toplevel = toplevel
|
|
43
|
+
@swpin_names = swpin_paths.keys
|
|
44
|
+
@swpin_paths = swpin_paths
|
|
45
|
+
@swpin_specs = swpin_specs
|
|
46
|
+
@date = date || Time.now
|
|
47
|
+
@name = date.strftime('%Y-%m-%d--%H-%M-%S')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param name [String]
|
|
51
|
+
def load(name)
|
|
52
|
+
@name = name
|
|
53
|
+
|
|
54
|
+
cfg = JSON.parse(File.read(config_path))
|
|
55
|
+
@toplevel = cfg['toplevel']
|
|
56
|
+
|
|
57
|
+
@swpin_names = []
|
|
58
|
+
@swpin_paths = {}
|
|
59
|
+
@swpin_specs = {}
|
|
60
|
+
|
|
61
|
+
cfg['swpins'].each do |swpin_name, swpin|
|
|
62
|
+
@swpin_names << swpin_name
|
|
63
|
+
@swpin_paths[swpin_name] = swpin['path']
|
|
64
|
+
@swpin_specs[swpin_name] = Swpins::Spec.for(swpin['spec']['type'].to_sym).new(
|
|
65
|
+
swpin_name,
|
|
66
|
+
swpin['spec']['nix_options'],
|
|
67
|
+
swpin['spec']
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@date = Time.iso8601(cfg['date'])
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
raise Error, "invalid generation '#{name}': #{e.message}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def save
|
|
77
|
+
FileUtils.mkdir_p(dir)
|
|
78
|
+
File.symlink(toplevel, toplevel_path)
|
|
79
|
+
|
|
80
|
+
swpin_paths.each do |name, path|
|
|
81
|
+
File.symlink(path, swpin_path(name))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
File.open(config_path, 'w') do |f|
|
|
85
|
+
f.puts(JSON.pretty_generate({
|
|
86
|
+
date: date.iso8601,
|
|
87
|
+
toplevel:,
|
|
88
|
+
swpins: swpin_paths.to_h do |name, path|
|
|
89
|
+
[name, { path:, spec: swpin_specs[name].as_json }]
|
|
90
|
+
end
|
|
91
|
+
}))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
add_gcroot
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def destroy
|
|
98
|
+
remove_gcroot
|
|
99
|
+
File.unlink(toplevel_path)
|
|
100
|
+
swpin_paths.each_key { |name| File.unlink(swpin_path(name)) }
|
|
101
|
+
File.unlink(config_path)
|
|
102
|
+
Dir.rmdir(dir)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def add_gcroot
|
|
106
|
+
GCRoot.add(gcroot_name('toplevel'), toplevel_path)
|
|
107
|
+
swpin_paths.each_key do |name|
|
|
108
|
+
GCRoot.add(gcroot_name("swpin.#{name}"), toplevel_path)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def remove_gcroot
|
|
113
|
+
GCRoot.remove(gcroot_name('toplevel'))
|
|
114
|
+
swpin_paths.each_key do |name|
|
|
115
|
+
GCRoot.remove(gcroot_name("swpin.#{name}"))
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def dir
|
|
120
|
+
@dir ||= File.join(ConfDir.generation_dir, escaped_host, name)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
protected
|
|
124
|
+
|
|
125
|
+
def config_path
|
|
126
|
+
@config_path ||= File.join(dir, 'generation.json')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def toplevel_path
|
|
130
|
+
@toplevel_path ||= File.join(dir, 'toplevel')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def swpin_path(name)
|
|
134
|
+
File.join(dir, "#{name}.swpin")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def escaped_host
|
|
138
|
+
@escaped_host ||= ConfCtl.safe_host_name(host)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def gcroot_name(file)
|
|
142
|
+
"#{escaped_host}-generation-#{name}-#{file}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require 'confctl/utils/file'
|
|
2
|
+
|
|
3
|
+
module ConfCtl
|
|
4
|
+
class Generation::BuildList
|
|
5
|
+
include Utils::File
|
|
6
|
+
|
|
7
|
+
# @return [String]
|
|
8
|
+
attr_reader :host
|
|
9
|
+
|
|
10
|
+
# @return [Generation::Build, nil]
|
|
11
|
+
attr_reader :current
|
|
12
|
+
|
|
13
|
+
# @return [String]
|
|
14
|
+
def initialize(host)
|
|
15
|
+
@host = host
|
|
16
|
+
@generations = []
|
|
17
|
+
@index = {}
|
|
18
|
+
|
|
19
|
+
return unless Dir.exist?(dir)
|
|
20
|
+
|
|
21
|
+
Dir.entries(dir).each do |v|
|
|
22
|
+
abs_path = File.join(dir, v)
|
|
23
|
+
next if %w[. ..].include?(v) || !Dir.exist?(abs_path) || File.symlink?(abs_path)
|
|
24
|
+
|
|
25
|
+
gen = Generation::Build.new(host)
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
gen.load(v)
|
|
29
|
+
rescue Error => e
|
|
30
|
+
warn "Ignoring invalid generation #{gen.dir}"
|
|
31
|
+
next
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
generations << gen
|
|
35
|
+
index[gen.name] = gen
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
generations.sort! do |a, b|
|
|
39
|
+
a.date <=> b.date
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
current_gen =
|
|
43
|
+
if File.exist?(current_symlink)
|
|
44
|
+
name = File.basename(File.readlink(current_symlink))
|
|
45
|
+
index[name] || generations.last
|
|
46
|
+
else
|
|
47
|
+
generations.last
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
change_current(current_gen) if current_gen
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param name [String]
|
|
54
|
+
def [](name)
|
|
55
|
+
index[name]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def each(&)
|
|
59
|
+
generations.each(&)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Array<Generation::Build>]
|
|
63
|
+
def to_a
|
|
64
|
+
generations.clone
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [Integer]
|
|
68
|
+
def count
|
|
69
|
+
generations.length
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @param gen [Generation::Build]
|
|
73
|
+
def current=(gen)
|
|
74
|
+
change_current(gen)
|
|
75
|
+
generations << gen unless generations.include?(gen)
|
|
76
|
+
replace_symlink(current_symlink, gen.name)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @param toplevel [String]
|
|
80
|
+
# @param swpin_paths [Hash]
|
|
81
|
+
# @return [Generation::Build, nil]
|
|
82
|
+
def find(toplevel, swpin_paths)
|
|
83
|
+
generations.detect do |gen|
|
|
84
|
+
gen.toplevel == toplevel && gen.swpin_paths == swpin_paths
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
protected
|
|
89
|
+
|
|
90
|
+
attr_reader :generations, :index
|
|
91
|
+
|
|
92
|
+
def dir
|
|
93
|
+
@dir ||= File.join(ConfDir.generation_dir, ConfCtl.safe_host_name(host))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def current_symlink
|
|
97
|
+
@current_symlink ||= File.join(dir, 'current')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def change_current(gen)
|
|
101
|
+
@current = gen
|
|
102
|
+
generations.each { |g| g.current = false }
|
|
103
|
+
gen.current = true
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module ConfCtl
|
|
2
|
+
class Generation::Host
|
|
3
|
+
attr_reader :host, :profile, :id, :toplevel, :date, :current
|
|
4
|
+
|
|
5
|
+
# @param host [String]
|
|
6
|
+
# @param profile [String]
|
|
7
|
+
# @param id [Integer]
|
|
8
|
+
# @param toplevel [String]
|
|
9
|
+
# @param date [Time]
|
|
10
|
+
# @param mc [MachineControl]
|
|
11
|
+
def initialize(host, profile, id, toplevel, date, current: false, mc: nil)
|
|
12
|
+
@host = host
|
|
13
|
+
@profile = profile
|
|
14
|
+
@id = id
|
|
15
|
+
@toplevel = toplevel
|
|
16
|
+
@date = date
|
|
17
|
+
@current = current
|
|
18
|
+
@mc = mc
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def approx_name
|
|
22
|
+
@approx_name ||= date.strftime('%Y-%m-%d--%H-%M-%S')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def destroy
|
|
26
|
+
raise 'machine control not available' if mc.nil?
|
|
27
|
+
|
|
28
|
+
mc.execute('nix-env', '-p', profile, '--delete-generations', id.to_s)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
protected
|
|
32
|
+
|
|
33
|
+
attr_reader :mc
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module ConfCtl
|
|
2
|
+
class Generation::HostList
|
|
3
|
+
# @param mc [MachineControl]
|
|
4
|
+
# @return [Generation::HostList]
|
|
5
|
+
def self.fetch(mc, profile: '/nix/var/nix/profiles/system')
|
|
6
|
+
out, = mc.bash_script(<<-END
|
|
7
|
+
realpath #{profile}
|
|
8
|
+
|
|
9
|
+
for generation in `ls -d -1 #{profile}-*-link` ; do
|
|
10
|
+
echo "$generation;$(readlink $generation);$(stat --format=%Y $generation)"
|
|
11
|
+
done
|
|
12
|
+
END
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
list = new(mc.machine.name)
|
|
16
|
+
lines = out.strip.split("\n")
|
|
17
|
+
current_path = lines.shift
|
|
18
|
+
id_rx = /^#{Regexp.escape(profile)}-(\d+)-link$/
|
|
19
|
+
|
|
20
|
+
lines.each do |line|
|
|
21
|
+
link, path, created_at = line.split(';')
|
|
22
|
+
|
|
23
|
+
if id_rx =~ link
|
|
24
|
+
id = ::Regexp.last_match(1).to_i
|
|
25
|
+
else
|
|
26
|
+
warn "Invalid profile generation link '#{link}'"
|
|
27
|
+
next
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
list << Generation::Host.new(
|
|
31
|
+
mc.machine.name,
|
|
32
|
+
profile,
|
|
33
|
+
id,
|
|
34
|
+
path,
|
|
35
|
+
Time.at(created_at.to_i),
|
|
36
|
+
current: path == current_path,
|
|
37
|
+
mc:
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
list.sort
|
|
42
|
+
list
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [String]
|
|
46
|
+
attr_reader :host
|
|
47
|
+
|
|
48
|
+
# @param host [String]
|
|
49
|
+
def initialize(host)
|
|
50
|
+
@host = host
|
|
51
|
+
@generations = []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param generation [Generation::Host]
|
|
55
|
+
def <<(generation)
|
|
56
|
+
generations << generation
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def sort
|
|
60
|
+
generations.sort! { |a, b| a.id <=> b.id }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def each(&)
|
|
64
|
+
generations.each(&)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [Integer]
|
|
68
|
+
def count
|
|
69
|
+
generations.length
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [Generation::Host]
|
|
73
|
+
def current
|
|
74
|
+
generations.detect(&:current)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
protected
|
|
78
|
+
|
|
79
|
+
attr_reader :generations
|
|
80
|
+
end
|
|
81
|
+
end
|