vagrant-ssh-config-manager 0.8.2

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.
@@ -0,0 +1,251 @@
1
+ require 'fileutils'
2
+ require 'tempfile'
3
+ require 'log4r'
4
+
5
+ module VagrantPlugins
6
+ module SshConfigManager
7
+ class IncludeManager
8
+ PLUGIN_MARKER_START = "# BEGIN vagrant-ssh-config-manager"
9
+ PLUGIN_MARKER_END = "# END vagrant-ssh-config-manager"
10
+
11
+ # Initialize IncludeManager with configuration
12
+ def initialize(config)
13
+ @config = config
14
+ @logger = Log4r::Logger.new("vagrant::plugins::sshconfigmanager::includemanager")
15
+ end
16
+
17
+ # Get the SSH config file path (always ~/.ssh/config)
18
+ def ssh_config_file
19
+ @ssh_config_file ||= File.expand_path("~/.ssh/config")
20
+ end
21
+
22
+ # Check if Include directive exists in main SSH config
23
+ def include_directive_exists?
24
+ return false unless File.exist?(ssh_config_file)
25
+
26
+ content = File.read(ssh_config_file)
27
+ include_pattern = /^Include\s+#{Regexp.escape(@config.ssh_config_dir)}/
28
+ content.match?(include_pattern)
29
+ end
30
+
31
+ # Add Include directive to main SSH config file
32
+ def add_include_directive
33
+ return false unless @config.manage_includes
34
+ return true if include_directive_exists?
35
+
36
+ begin
37
+ # Create backup before modifying
38
+ create_backup
39
+
40
+ # Add Include directive at the beginning of file
41
+ add_include_to_config
42
+
43
+ @logger.info("Added Include directive for #{@config.ssh_config_dir} to ~/.ssh/config")
44
+ true
45
+ rescue => e
46
+ @logger.error("Failed to add Include directive: #{e.message}")
47
+ restore_backup
48
+ false
49
+ end
50
+ end
51
+
52
+ # Remove Include directive from main SSH config file
53
+ def remove_include_directive
54
+ return false unless @config.manage_includes
55
+ return true unless include_directive_exists?
56
+
57
+ begin
58
+ # Create backup before modifying
59
+ create_backup
60
+
61
+ # Remove Include directive
62
+ remove_include_from_config
63
+
64
+ @logger.info("Removed Include directive for #{@config.ssh_config_dir} from ~/.ssh/config")
65
+ true
66
+ rescue => e
67
+ @logger.error("Failed to remove Include directive: #{e.message}")
68
+ restore_backup
69
+ false
70
+ end
71
+ end
72
+
73
+ # Check if Include directive should be removed (no config files exist)
74
+ def should_remove_include_directive?
75
+ return false unless @config.cleanup_empty_dir
76
+ return false unless Dir.exist?(@config.ssh_config_dir)
77
+
78
+ # Check if directory is empty (excluding . and ..)
79
+ entries = Dir.entries(@config.ssh_config_dir) - %w[. ..]
80
+ config_files = entries.select { |file| file.end_with?('.conf') }
81
+ config_files.empty?
82
+ end
83
+
84
+ # Manage Include directive based on current state
85
+ def manage_include_directive
86
+ return unless @config.manage_includes
87
+
88
+ if should_remove_include_directive?
89
+ remove_include_directive
90
+ elsif Dir.exist?(@config.ssh_config_dir) && !Dir.entries(@config.ssh_config_dir).select { |f| f.end_with?('.conf') }.empty?
91
+ add_include_directive
92
+ end
93
+ end
94
+
95
+ # Parse SSH config file and find optimal location for Include
96
+ def find_include_location(content)
97
+ lines = content.lines
98
+
99
+ # Look for existing Include directives to place ours with them
100
+ include_line_index = lines.find_index { |line| line.strip.start_with?('Include ') }
101
+
102
+ if include_line_index
103
+ # Place before other Include directives
104
+ include_line_index
105
+ else
106
+ # Place at the beginning of file, after any initial comments
107
+ comment_end = 0
108
+ lines.each_with_index do |line, index|
109
+ if line.strip.empty? || line.strip.start_with?('#')
110
+ comment_end = index + 1
111
+ else
112
+ break
113
+ end
114
+ end
115
+ comment_end
116
+ end
117
+ end
118
+
119
+ # Check if main SSH config file is write-protected
120
+ def main_config_writable?
121
+ return false unless File.exist?(ssh_config_file)
122
+ File.writable?(ssh_config_file)
123
+ end
124
+
125
+ # Handle edge case: empty main config file
126
+ def handle_empty_main_config
127
+ unless File.exist?(ssh_config_file)
128
+ # Create empty SSH config file with proper permissions
129
+ FileUtils.mkdir_p(File.dirname(ssh_config_file), mode: 0700)
130
+ File.write(ssh_config_file, "")
131
+ File.chmod(0600, ssh_config_file)
132
+ @logger.info("Created empty SSH config file: #{ssh_config_file}")
133
+ end
134
+ end
135
+
136
+ # Validate SSH config file format
137
+ def validate_main_config
138
+ return true unless File.exist?(ssh_config_file)
139
+
140
+ begin
141
+ content = File.read(ssh_config_file)
142
+ # Basic validation - check for severely malformed config
143
+ lines = content.lines
144
+ lines.each_with_index do |line, index|
145
+ stripped = line.strip
146
+ next if stripped.empty? || stripped.start_with?('#')
147
+
148
+ # Check for basic SSH config format issues
149
+ if stripped.include?("\t") && stripped.start_with?("Host ")
150
+ @logger.warn("Potential SSH config format issue at line #{index + 1}: tabs in Host directive")
151
+ end
152
+ end
153
+ true
154
+ rescue => e
155
+ @logger.error("SSH config validation failed: #{e.message}")
156
+ false
157
+ end
158
+ end
159
+
160
+ private
161
+
162
+ # Create backup of main SSH config file
163
+ def create_backup
164
+ backup_path = "#{ssh_config_file}.vagrant-ssh-config-manager.backup"
165
+ FileUtils.cp(ssh_config_file, backup_path) if File.exist?(ssh_config_file)
166
+ @backup_path = backup_path
167
+ end
168
+
169
+ # Restore backup of main SSH config file
170
+ def restore_backup
171
+ if @backup_path && File.exist?(@backup_path)
172
+ FileUtils.cp(@backup_path, ssh_config_file)
173
+ File.delete(@backup_path)
174
+ @logger.info("Restored SSH config from backup")
175
+ end
176
+ end
177
+
178
+ # Add Include directive to SSH config
179
+ def add_include_to_config
180
+ handle_empty_main_config
181
+
182
+ content = File.exist?(ssh_config_file) ? File.read(ssh_config_file) : ""
183
+ lines = content.lines
184
+
185
+ # Find optimal location for Include
186
+ insert_index = find_include_location(content)
187
+
188
+ # Create Include directive with plugin markers
189
+ include_lines = [
190
+ "#{PLUGIN_MARKER_START}\n",
191
+ "Include #{@config.ssh_config_dir}/*.conf\n",
192
+ "#{PLUGIN_MARKER_END}\n",
193
+ "\n"
194
+ ]
195
+
196
+ # Insert Include directive
197
+ lines.insert(insert_index, *include_lines)
198
+
199
+ # Write back to file atomically
200
+ write_config_atomically(lines.join)
201
+ end
202
+
203
+ # Remove Include directive from SSH config
204
+ def remove_include_from_config
205
+ return unless File.exist?(ssh_config_file)
206
+
207
+ content = File.read(ssh_config_file)
208
+ lines = content.lines
209
+
210
+ # Find and remove plugin-managed Include directive
211
+ start_index = lines.find_index { |line| line.strip == PLUGIN_MARKER_START.strip }
212
+ end_index = lines.find_index { |line| line.strip == PLUGIN_MARKER_END.strip }
213
+
214
+ if start_index && end_index && end_index > start_index
215
+ # Remove lines between markers (inclusive)
216
+ lines.slice!(start_index..end_index)
217
+
218
+ # Remove trailing empty line if it exists
219
+ if lines[start_index] && lines[start_index].strip.empty?
220
+ lines.delete_at(start_index)
221
+ end
222
+ else
223
+ # Fallback: remove any Include directive for our directory
224
+ lines.reject! do |line|
225
+ line.strip.match?(/^Include\s+#{Regexp.escape(@config.ssh_config_dir)}/)
226
+ end
227
+ end
228
+
229
+ # Write back to file atomically
230
+ write_config_atomically(lines.join)
231
+ end
232
+
233
+ # Write SSH config file atomically
234
+ def write_config_atomically(content)
235
+ temp_file = Tempfile.new(File.basename(ssh_config_file), File.dirname(ssh_config_file))
236
+ begin
237
+ temp_file.write(content)
238
+ temp_file.close
239
+
240
+ # Set proper permissions before moving
241
+ File.chmod(0600, temp_file.path)
242
+
243
+ # Atomic move
244
+ FileUtils.mv(temp_file.path, ssh_config_file)
245
+ ensure
246
+ temp_file.unlink if temp_file && File.exist?(temp_file.path)
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,56 @@
1
+ require "vagrant"
2
+
3
+ module VagrantPlugins
4
+ module SshConfigManager
5
+ class Plugin < Vagrant.plugin("2")
6
+ name "SSH Config Manager"
7
+ description <<-DESC
8
+ This plugin automatically manages SSH configurations by leveraging Vagrant's
9
+ internal SSH knowledge. It creates and maintains SSH config entries when VMs
10
+ are started and cleans them up when VMs are destroyed.
11
+ DESC
12
+
13
+ # Register the configuration class
14
+ config :sshconfigmanager do
15
+ require "vagrant-ssh-config-manager/config"
16
+ Config
17
+ end
18
+
19
+ # Hook into various Vagrant actions
20
+ action_hook(:ssh_config_manager, :machine_action_up) do |hook|
21
+ require "vagrant-ssh-config-manager/action/up"
22
+ hook.after(Vagrant::Action::Builtin::WaitForCommunicator, Action::Up)
23
+ end
24
+
25
+ action_hook(:ssh_config_manager, :machine_action_destroy) do |hook|
26
+ require "vagrant-ssh-config-manager/action/destroy"
27
+ hook.before(Vagrant::Action::Builtin::DestroyConfirm, Action::Destroy)
28
+ end
29
+
30
+ action_hook(:ssh_config_manager, :machine_action_reload) do |hook|
31
+ require "vagrant-ssh-config-manager/action/reload"
32
+ hook.after(Vagrant::Action::Builtin::WaitForCommunicator, Action::Reload)
33
+ end
34
+
35
+ action_hook(:ssh_config_manager, :machine_action_halt) do |hook|
36
+ require "vagrant-ssh-config-manager/action/halt"
37
+ hook.before(Vagrant::Action::Builtin::GracefulHalt, Action::Halt)
38
+ end
39
+
40
+ action_hook(:ssh_config_manager, :machine_action_suspend) do |hook|
41
+ require "vagrant-ssh-config-manager/action/halt"
42
+ hook.before(Vagrant::Action::Builtin::Suspend, Action::Halt)
43
+ end
44
+
45
+ action_hook(:ssh_config_manager, :machine_action_resume) do |hook|
46
+ require "vagrant-ssh-config-manager/action/up"
47
+ hook.after(Vagrant::Action::Builtin::Resume, Action::Up)
48
+ end
49
+
50
+ action_hook(:ssh_config_manager, :machine_action_provision) do |hook|
51
+ require "vagrant-ssh-config-manager/action/provision"
52
+ hook.after(Vagrant::Action::Builtin::Provision, Action::Provision)
53
+ end
54
+ end
55
+ end
56
+ end