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,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,3 @@
1
+ module ConfCtl
2
+ class Error < ::StandardError; end
3
+ 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