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.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.gitignore +5 -0
- data/Gemfile +15 -0
- data/README.md +267 -0
- data/Rakefile +22 -0
- data/lib/vagrant-ssh-config-manager/action/destroy.rb +84 -0
- data/lib/vagrant-ssh-config-manager/action/halt.rb +68 -0
- data/lib/vagrant-ssh-config-manager/action/provision.rb +82 -0
- data/lib/vagrant-ssh-config-manager/action/reload.rb +106 -0
- data/lib/vagrant-ssh-config-manager/action/up.rb +99 -0
- data/lib/vagrant-ssh-config-manager/config.rb +194 -0
- data/lib/vagrant-ssh-config-manager/file_locker.rb +140 -0
- data/lib/vagrant-ssh-config-manager/file_manager.rb +245 -0
- data/lib/vagrant-ssh-config-manager/include_manager.rb +251 -0
- data/lib/vagrant-ssh-config-manager/plugin.rb +56 -0
- data/lib/vagrant-ssh-config-manager/ssh_config_manager.rb +2150 -0
- data/lib/vagrant-ssh-config-manager/ssh_info_extractor.rb +443 -0
- data/lib/vagrant-ssh-config-manager/version.rb +5 -0
- data/lib/vagrant-ssh-config-manager.rb +30 -0
- data/vagrant-ssh-config-manager.gemspec +35 -0
- metadata +133 -0
@@ -0,0 +1,99 @@
|
|
1
|
+
module VagrantPlugins
|
2
|
+
module SshConfigManager
|
3
|
+
module Action
|
4
|
+
class Up
|
5
|
+
def initialize(app, env)
|
6
|
+
@app = app
|
7
|
+
@env = env
|
8
|
+
@logger = Log4r::Logger.new("vagrant::plugins::ssh_config_manager::action::up")
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
# Call the next middleware first
|
13
|
+
@app.call(env)
|
14
|
+
|
15
|
+
# Only proceed if the machine is running and SSH is ready
|
16
|
+
machine = env[:machine]
|
17
|
+
return unless machine
|
18
|
+
return unless machine.state.id == :running
|
19
|
+
|
20
|
+
# Check if plugin is enabled
|
21
|
+
config = machine.config.sshconfigmanager
|
22
|
+
return unless config && config.enabled
|
23
|
+
|
24
|
+
@logger.info("SSH Config Manager: Creating SSH config file for machine: #{machine.name}")
|
25
|
+
|
26
|
+
# Handle SSH config file creation
|
27
|
+
handle_ssh_config_creation(machine, config)
|
28
|
+
rescue => e
|
29
|
+
@logger.error("SSH Config Manager: Error in Up action: #{e.message}")
|
30
|
+
@logger.debug("Backtrace: #{e.backtrace.join("\n")}")
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def handle_ssh_config_creation(machine, config)
|
36
|
+
begin
|
37
|
+
@logger.info("Creating SSH config file for machine: #{machine.name}")
|
38
|
+
|
39
|
+
# Lazy load required classes with error handling
|
40
|
+
begin
|
41
|
+
require 'vagrant-ssh-config-manager/ssh_info_extractor'
|
42
|
+
require 'vagrant-ssh-config-manager/file_manager'
|
43
|
+
require 'vagrant-ssh-config-manager/include_manager'
|
44
|
+
rescue LoadError => e
|
45
|
+
@logger.error("Failed to load required classes: #{e.message}")
|
46
|
+
machine.ui.warn("SSH config manager: Failed to load required components, skipping SSH config creation")
|
47
|
+
return
|
48
|
+
end
|
49
|
+
|
50
|
+
# Extract SSH information
|
51
|
+
extractor = SshInfoExtractor.new(machine)
|
52
|
+
|
53
|
+
# Check if machine supports SSH
|
54
|
+
unless extractor.ssh_capable?
|
55
|
+
@logger.debug("Machine #{machine.name} does not support SSH, skipping")
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
# Create file manager and include manager
|
60
|
+
file_manager = FileManager.new(config)
|
61
|
+
include_manager = IncludeManager.new(config)
|
62
|
+
|
63
|
+
# Write SSH config file
|
64
|
+
@logger.info("Attempting to create SSH config file for #{machine.name}")
|
65
|
+
|
66
|
+
if file_manager.write_ssh_config_file(machine)
|
67
|
+
host_name = file_manager.send(:generate_host_name, machine)
|
68
|
+
machine.ui.info("SSH config file created for machine '#{machine.name}' as '#{host_name}'")
|
69
|
+
machine.ui.info("You can now connect with: ssh #{host_name}")
|
70
|
+
@logger.info("Successfully created SSH config file for #{machine.name}")
|
71
|
+
|
72
|
+
# Manage Include directive after file creation
|
73
|
+
include_manager.manage_include_directive
|
74
|
+
else
|
75
|
+
machine.ui.warn("Failed to create SSH config file for machine: #{machine.name}")
|
76
|
+
@logger.warn("Failed to create SSH config file for #{machine.name}")
|
77
|
+
end
|
78
|
+
|
79
|
+
rescue Errno::EACCES => e
|
80
|
+
@logger.error("Permission denied accessing SSH config for #{machine.name}: #{e.message}")
|
81
|
+
machine.ui.warn("SSH config manager: Permission denied. Check file permissions.")
|
82
|
+
rescue Errno::ENOSPC => e
|
83
|
+
@logger.error("No space left on device for #{machine.name}: #{e.message}")
|
84
|
+
machine.ui.warn("SSH config manager: No space left on device.")
|
85
|
+
rescue Errno::EIO => e
|
86
|
+
@logger.error("I/O error for #{machine.name}: #{e.message}")
|
87
|
+
machine.ui.warn("SSH config manager: I/O error accessing SSH config files.")
|
88
|
+
rescue => e
|
89
|
+
@logger.error("Error creating SSH config for #{machine.name}: #{e.message}")
|
90
|
+
@logger.debug("Backtrace: #{e.backtrace.join("\n")}")
|
91
|
+
|
92
|
+
# Don't fail the vagrant up process, just warn
|
93
|
+
machine.ui.warn("SSH config manager encountered an error: #{e.message}")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'vagrant'
|
3
|
+
|
4
|
+
module VagrantPlugins
|
5
|
+
module SshConfigManager
|
6
|
+
class Config < Vagrant.plugin("2", :config)
|
7
|
+
# Plugin enabled/disabled flag
|
8
|
+
attr_accessor :enabled
|
9
|
+
|
10
|
+
# SSH config directory configuration
|
11
|
+
attr_accessor :ssh_config_dir
|
12
|
+
attr_accessor :manage_includes
|
13
|
+
attr_accessor :auto_create_dir
|
14
|
+
attr_accessor :cleanup_empty_dir
|
15
|
+
|
16
|
+
# Additional configuration options
|
17
|
+
attr_accessor :auto_remove_on_destroy
|
18
|
+
attr_accessor :update_on_reload
|
19
|
+
attr_accessor :refresh_on_provision
|
20
|
+
attr_accessor :keep_config_on_halt
|
21
|
+
attr_accessor :project_isolation
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@enabled = Vagrant::Plugin::V2::Config::UNSET_VALUE
|
25
|
+
@ssh_config_dir = Vagrant::Plugin::V2::Config::UNSET_VALUE
|
26
|
+
@manage_includes = Vagrant::Plugin::V2::Config::UNSET_VALUE
|
27
|
+
@auto_create_dir = Vagrant::Plugin::V2::Config::UNSET_VALUE
|
28
|
+
@cleanup_empty_dir = Vagrant::Plugin::V2::Config::UNSET_VALUE
|
29
|
+
@auto_remove_on_destroy = Vagrant::Plugin::V2::Config::UNSET_VALUE
|
30
|
+
@update_on_reload = Vagrant::Plugin::V2::Config::UNSET_VALUE
|
31
|
+
@refresh_on_provision = Vagrant::Plugin::V2::Config::UNSET_VALUE
|
32
|
+
@keep_config_on_halt = Vagrant::Plugin::V2::Config::UNSET_VALUE
|
33
|
+
@project_isolation = Vagrant::Plugin::V2::Config::UNSET_VALUE
|
34
|
+
end
|
35
|
+
|
36
|
+
def finalize!
|
37
|
+
# Set default values for unset configuration options
|
38
|
+
@enabled = true if @enabled == Vagrant::Plugin::V2::Config::UNSET_VALUE
|
39
|
+
@ssh_config_dir = File.expand_path("~/.ssh/config.d/vagrant") if @ssh_config_dir == Vagrant::Plugin::V2::Config::UNSET_VALUE
|
40
|
+
@manage_includes = false if @manage_includes == Vagrant::Plugin::V2::Config::UNSET_VALUE
|
41
|
+
@auto_create_dir = true if @auto_create_dir == Vagrant::Plugin::V2::Config::UNSET_VALUE
|
42
|
+
@cleanup_empty_dir = true if @cleanup_empty_dir == Vagrant::Plugin::V2::Config::UNSET_VALUE
|
43
|
+
@auto_remove_on_destroy = true if @auto_remove_on_destroy == Vagrant::Plugin::V2::Config::UNSET_VALUE
|
44
|
+
@update_on_reload = true if @update_on_reload == Vagrant::Plugin::V2::Config::UNSET_VALUE
|
45
|
+
@refresh_on_provision = true if @refresh_on_provision == Vagrant::Plugin::V2::Config::UNSET_VALUE
|
46
|
+
@keep_config_on_halt = true if @keep_config_on_halt == Vagrant::Plugin::V2::Config::UNSET_VALUE
|
47
|
+
@project_isolation = true if @project_isolation == Vagrant::Plugin::V2::Config::UNSET_VALUE
|
48
|
+
|
49
|
+
# Expand and validate file paths
|
50
|
+
@ssh_config_dir = File.expand_path(@ssh_config_dir) if @ssh_config_dir.is_a?(String)
|
51
|
+
|
52
|
+
# Ensure SSH config directory exists if auto_create_dir is enabled
|
53
|
+
ensure_ssh_config_directory if @auto_create_dir && @ssh_config_dir
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate(machine)
|
57
|
+
errors = _detected_errors
|
58
|
+
|
59
|
+
# Validate enabled flag
|
60
|
+
unless [true, false].include?(@enabled)
|
61
|
+
errors << "sshconfigmanager.enabled must be true or false"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Validate SSH config directory
|
65
|
+
if @ssh_config_dir
|
66
|
+
unless @ssh_config_dir.is_a?(String)
|
67
|
+
errors << "sshconfigmanager.ssh_config_dir must be a string path"
|
68
|
+
else
|
69
|
+
# Validate directory path format
|
70
|
+
expanded_path = File.expand_path(@ssh_config_dir)
|
71
|
+
if expanded_path.include?("..") || expanded_path.include?("//")
|
72
|
+
errors << "sshconfigmanager.ssh_config_dir contains invalid path components: #{@ssh_config_dir}"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check if the directory exists or can be created
|
76
|
+
unless File.directory?(@ssh_config_dir)
|
77
|
+
if @auto_create_dir
|
78
|
+
begin
|
79
|
+
# Try to create the directory to validate the path
|
80
|
+
FileUtils.mkdir_p(@ssh_config_dir, mode: 0700)
|
81
|
+
rescue => e
|
82
|
+
errors << "sshconfigmanager.ssh_config_dir cannot be created: #{e.message}"
|
83
|
+
end
|
84
|
+
else
|
85
|
+
errors << "sshconfigmanager.ssh_config_dir does not exist and auto_create_dir is disabled: #{@ssh_config_dir}"
|
86
|
+
end
|
87
|
+
else
|
88
|
+
# Check directory permissions
|
89
|
+
unless File.readable?(@ssh_config_dir) && File.writable?(@ssh_config_dir)
|
90
|
+
errors << "sshconfigmanager.ssh_config_dir is not readable/writable: #{@ssh_config_dir}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Validate boolean options
|
97
|
+
boolean_options = {
|
98
|
+
'auto_remove_on_destroy' => @auto_remove_on_destroy,
|
99
|
+
'update_on_reload' => @update_on_reload,
|
100
|
+
'refresh_on_provision' => @refresh_on_provision,
|
101
|
+
'keep_config_on_halt' => @keep_config_on_halt,
|
102
|
+
'project_isolation' => @project_isolation,
|
103
|
+
'manage_includes' => @manage_includes,
|
104
|
+
'auto_create_dir' => @auto_create_dir,
|
105
|
+
'cleanup_empty_dir' => @cleanup_empty_dir
|
106
|
+
}
|
107
|
+
|
108
|
+
boolean_options.each do |option_name, value|
|
109
|
+
unless [true, false].include?(value)
|
110
|
+
errors << "sshconfigmanager.#{option_name} must be true or false"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Return validation results
|
115
|
+
{ "SSH Config Manager" => errors }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get configuration summary for debugging
|
119
|
+
def to_hash
|
120
|
+
{
|
121
|
+
enabled: @enabled,
|
122
|
+
ssh_config_dir: @ssh_config_dir,
|
123
|
+
manage_includes: @manage_includes,
|
124
|
+
auto_create_dir: @auto_create_dir,
|
125
|
+
cleanup_empty_dir: @cleanup_empty_dir,
|
126
|
+
auto_remove_on_destroy: @auto_remove_on_destroy,
|
127
|
+
update_on_reload: @update_on_reload,
|
128
|
+
refresh_on_provision: @refresh_on_provision,
|
129
|
+
keep_config_on_halt: @keep_config_on_halt,
|
130
|
+
project_isolation: @project_isolation
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
# Check if the plugin should operate for a given action
|
135
|
+
def enabled_for_action?(action_name)
|
136
|
+
return false unless @enabled
|
137
|
+
|
138
|
+
case action_name.to_sym
|
139
|
+
when :up, :resume
|
140
|
+
true
|
141
|
+
when :destroy
|
142
|
+
@auto_remove_on_destroy
|
143
|
+
when :reload
|
144
|
+
@update_on_reload
|
145
|
+
when :provision
|
146
|
+
@refresh_on_provision
|
147
|
+
when :halt, :suspend
|
148
|
+
@keep_config_on_halt
|
149
|
+
else
|
150
|
+
false
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Merge configuration from another config object (for inheritance)
|
155
|
+
def merge(other)
|
156
|
+
result = self.class.new
|
157
|
+
|
158
|
+
# Merge each attribute, preferring the other config's values if set
|
159
|
+
result.enabled = other.enabled != UNSET_VALUE ? other.enabled : @enabled
|
160
|
+
result.ssh_config_dir = other.ssh_config_dir != UNSET_VALUE ? other.ssh_config_dir : @ssh_config_dir
|
161
|
+
result.manage_includes = other.manage_includes != UNSET_VALUE ? other.manage_includes : @manage_includes
|
162
|
+
result.auto_create_dir = other.auto_create_dir != UNSET_VALUE ? other.auto_create_dir : @auto_create_dir
|
163
|
+
result.cleanup_empty_dir = other.cleanup_empty_dir != UNSET_VALUE ? other.cleanup_empty_dir : @cleanup_empty_dir
|
164
|
+
result.auto_remove_on_destroy = other.auto_remove_on_destroy != UNSET_VALUE ? other.auto_remove_on_destroy : @auto_remove_on_destroy
|
165
|
+
result.update_on_reload = other.update_on_reload != UNSET_VALUE ? other.update_on_reload : @update_on_reload
|
166
|
+
result.refresh_on_provision = other.refresh_on_provision != UNSET_VALUE ? other.refresh_on_provision : @refresh_on_provision
|
167
|
+
result.keep_config_on_halt = other.keep_config_on_halt != UNSET_VALUE ? other.keep_config_on_halt : @keep_config_on_halt
|
168
|
+
result.project_isolation = other.project_isolation != UNSET_VALUE ? other.project_isolation : @project_isolation
|
169
|
+
|
170
|
+
result
|
171
|
+
end
|
172
|
+
|
173
|
+
# Create SSH config directory with proper permissions
|
174
|
+
def ensure_ssh_config_directory
|
175
|
+
return false unless @auto_create_dir
|
176
|
+
return true if File.directory?(@ssh_config_dir)
|
177
|
+
|
178
|
+
begin
|
179
|
+
FileUtils.mkdir_p(@ssh_config_dir, mode: 0700)
|
180
|
+
true
|
181
|
+
rescue => e
|
182
|
+
false
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Get the appropriate manager instance
|
187
|
+
def get_ssh_manager_instance(machine)
|
188
|
+
# Use separate file approach with FileManager
|
189
|
+
require_relative 'file_manager'
|
190
|
+
FileManager.new(self)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'fcntl'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module VagrantPlugins
|
5
|
+
module SshConfigManager
|
6
|
+
class FileLocker
|
7
|
+
# Default timeout for acquiring locks (in seconds)
|
8
|
+
DEFAULT_TIMEOUT = 30
|
9
|
+
|
10
|
+
# Lock types
|
11
|
+
LOCK_SHARED = File::LOCK_SH
|
12
|
+
LOCK_EXCLUSIVE = File::LOCK_EX
|
13
|
+
LOCK_NON_BLOCKING = File::LOCK_NB
|
14
|
+
|
15
|
+
def initialize(file_path, logger = nil)
|
16
|
+
@file_path = file_path
|
17
|
+
@logger = logger || Log4r::Logger.new("vagrant::plugins::ssh_config_manager::file_locker")
|
18
|
+
@lock_file = nil
|
19
|
+
@locked = false
|
20
|
+
end
|
21
|
+
|
22
|
+
# Acquire an exclusive lock on the file
|
23
|
+
def with_exclusive_lock(timeout: DEFAULT_TIMEOUT)
|
24
|
+
with_lock(LOCK_EXCLUSIVE, timeout: timeout) do
|
25
|
+
yield
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Acquire a shared lock on the file
|
30
|
+
def with_shared_lock(timeout: DEFAULT_TIMEOUT)
|
31
|
+
with_lock(LOCK_SHARED, timeout: timeout) do
|
32
|
+
yield
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Check if file is currently locked by another process
|
37
|
+
def locked?
|
38
|
+
return false unless File.exist?(@file_path)
|
39
|
+
|
40
|
+
begin
|
41
|
+
File.open(@file_path, 'r') do |file|
|
42
|
+
# Try to acquire a non-blocking exclusive lock
|
43
|
+
file.flock(LOCK_EXCLUSIVE | LOCK_NON_BLOCKING)
|
44
|
+
false # Not locked if we could acquire the lock
|
45
|
+
end
|
46
|
+
rescue Errno::EAGAIN, Errno::EACCES
|
47
|
+
true # File is locked
|
48
|
+
rescue => e
|
49
|
+
@logger.debug("Error checking lock status: #{e.message}")
|
50
|
+
false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def with_lock(lock_type, timeout: DEFAULT_TIMEOUT)
|
57
|
+
acquire_lock(lock_type, timeout: timeout)
|
58
|
+
|
59
|
+
begin
|
60
|
+
yield
|
61
|
+
ensure
|
62
|
+
release_lock
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def acquire_lock(lock_type, timeout: DEFAULT_TIMEOUT)
|
67
|
+
ensure_directory_exists
|
68
|
+
|
69
|
+
@logger.debug("Acquiring #{lock_type_name(lock_type)} lock on #{@file_path}")
|
70
|
+
|
71
|
+
# Use timeout to prevent infinite waiting
|
72
|
+
Timeout.timeout(timeout) do
|
73
|
+
@lock_file = File.open(@file_path, File::RDWR | File::CREAT, 0600)
|
74
|
+
@lock_file.flock(lock_type)
|
75
|
+
@locked = true
|
76
|
+
@logger.debug("Successfully acquired lock on #{@file_path}")
|
77
|
+
end
|
78
|
+
|
79
|
+
rescue Timeout::Error
|
80
|
+
cleanup_lock_file
|
81
|
+
raise LockTimeoutError.new("Timeout waiting for lock on #{@file_path} (waited #{timeout}s)")
|
82
|
+
rescue => e
|
83
|
+
cleanup_lock_file
|
84
|
+
@logger.error("Failed to acquire lock on #{@file_path}: #{e.message}")
|
85
|
+
raise LockAcquisitionError.new("Could not acquire lock: #{e.message}")
|
86
|
+
end
|
87
|
+
|
88
|
+
def release_lock
|
89
|
+
return unless @locked && @lock_file
|
90
|
+
|
91
|
+
@logger.debug("Releasing lock on #{@file_path}")
|
92
|
+
|
93
|
+
begin
|
94
|
+
@lock_file.flock(File::LOCK_UN)
|
95
|
+
rescue => e
|
96
|
+
@logger.warn("Error releasing lock: #{e.message}")
|
97
|
+
ensure
|
98
|
+
cleanup_lock_file
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def cleanup_lock_file
|
103
|
+
if @lock_file
|
104
|
+
begin
|
105
|
+
@lock_file.close unless @lock_file.closed?
|
106
|
+
rescue => e
|
107
|
+
@logger.debug("Error closing lock file: #{e.message}")
|
108
|
+
ensure
|
109
|
+
@lock_file = nil
|
110
|
+
@locked = false
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def ensure_directory_exists
|
116
|
+
dir = File.dirname(@file_path)
|
117
|
+
unless File.directory?(dir)
|
118
|
+
FileUtils.mkdir_p(dir, mode: 0700)
|
119
|
+
@logger.debug("Created directory: #{dir}")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def lock_type_name(lock_type)
|
124
|
+
case lock_type
|
125
|
+
when LOCK_SHARED
|
126
|
+
"shared"
|
127
|
+
when LOCK_EXCLUSIVE
|
128
|
+
"exclusive"
|
129
|
+
else
|
130
|
+
"unknown"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Custom exception classes
|
136
|
+
class LockError < StandardError; end
|
137
|
+
class LockTimeoutError < LockError; end
|
138
|
+
class LockAcquisitionError < LockError; end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'digest'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'log4r'
|
5
|
+
|
6
|
+
module VagrantPlugins
|
7
|
+
module SshConfigManager
|
8
|
+
class FileManager
|
9
|
+
# Initialize FileManager with configuration
|
10
|
+
def initialize(config)
|
11
|
+
@config = config
|
12
|
+
@logger = Log4r::Logger.new("vagrant::plugins::sshconfigmanager::filemanager")
|
13
|
+
end
|
14
|
+
|
15
|
+
# Generate unique filename for VM SSH config
|
16
|
+
# Format: {project_hash}-{vm_name}.conf
|
17
|
+
def generate_filename(machine)
|
18
|
+
project_hash = generate_project_hash(machine.env.root_path.to_s)
|
19
|
+
vm_name = machine.name.to_s
|
20
|
+
"#{project_hash}-#{vm_name}.conf"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get full path for VM SSH config file
|
24
|
+
def get_file_path(machine)
|
25
|
+
filename = generate_filename(machine)
|
26
|
+
File.join(@config.ssh_config_dir, filename)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Generate SSH config content for a VM
|
30
|
+
def generate_ssh_config_content(machine)
|
31
|
+
ssh_info = machine.ssh_info
|
32
|
+
return nil unless ssh_info
|
33
|
+
|
34
|
+
content = []
|
35
|
+
content << "# Managed by vagrant-ssh-config-manager plugin"
|
36
|
+
content << "# Project: #{File.basename(machine.env.root_path)}"
|
37
|
+
content << "# VM: #{machine.name}"
|
38
|
+
content << "# Generated: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
39
|
+
content << ""
|
40
|
+
|
41
|
+
host_name = generate_host_name(machine)
|
42
|
+
content << "Host #{host_name}"
|
43
|
+
content << " HostName #{ssh_info[:host]}"
|
44
|
+
content << " Port #{ssh_info[:port]}"
|
45
|
+
content << " User #{ssh_info[:username]}"
|
46
|
+
|
47
|
+
if ssh_info[:private_key_path] && ssh_info[:private_key_path].first
|
48
|
+
content << " IdentityFile #{ssh_info[:private_key_path].first}"
|
49
|
+
content << " IdentitiesOnly yes"
|
50
|
+
end
|
51
|
+
|
52
|
+
content << " UserKnownHostsFile /dev/null"
|
53
|
+
content << " StrictHostKeyChecking no"
|
54
|
+
content << " PasswordAuthentication no"
|
55
|
+
content << " LogLevel FATAL"
|
56
|
+
content << ""
|
57
|
+
|
58
|
+
content.join("\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
# Write SSH config file for VM with atomic operation
|
62
|
+
def write_ssh_config_file(machine)
|
63
|
+
return false unless @config.enabled
|
64
|
+
|
65
|
+
file_path = get_file_path(machine)
|
66
|
+
content = generate_ssh_config_content(machine)
|
67
|
+
return false unless content
|
68
|
+
|
69
|
+
begin
|
70
|
+
# Ensure directory exists
|
71
|
+
FileUtils.mkdir_p(File.dirname(file_path), mode: 0700)
|
72
|
+
|
73
|
+
# Use atomic write with temporary file
|
74
|
+
write_file_atomically(file_path, content)
|
75
|
+
|
76
|
+
@logger.info("SSH config file created: #{file_path}")
|
77
|
+
true
|
78
|
+
rescue => e
|
79
|
+
@logger.error("Failed to write SSH config file #{file_path}: #{e.message}")
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Remove SSH config file for VM
|
85
|
+
def remove_ssh_config_file(machine)
|
86
|
+
file_path = get_file_path(machine)
|
87
|
+
|
88
|
+
begin
|
89
|
+
if File.exist?(file_path)
|
90
|
+
File.delete(file_path)
|
91
|
+
@logger.info("SSH config file removed: #{file_path}")
|
92
|
+
|
93
|
+
# Clean up empty directory if configured
|
94
|
+
cleanup_empty_directory if @config.cleanup_empty_dir
|
95
|
+
true
|
96
|
+
else
|
97
|
+
@logger.debug("SSH config file does not exist: #{file_path}")
|
98
|
+
false
|
99
|
+
end
|
100
|
+
rescue => e
|
101
|
+
@logger.error("Failed to remove SSH config file #{file_path}: #{e.message}")
|
102
|
+
false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Check if SSH config file exists for VM
|
107
|
+
def ssh_config_file_exists?(machine)
|
108
|
+
File.exist?(get_file_path(machine))
|
109
|
+
end
|
110
|
+
|
111
|
+
# Validate SSH config file content
|
112
|
+
def validate_ssh_config_content(content)
|
113
|
+
return false if content.nil? || content.empty?
|
114
|
+
|
115
|
+
# Basic validation - check for required SSH config elements
|
116
|
+
content.include?("Host ") && content.include?("HostName ") && content.include?("Port ")
|
117
|
+
end
|
118
|
+
|
119
|
+
# Detect and clean up orphaned SSH config files
|
120
|
+
def cleanup_orphaned_files
|
121
|
+
return unless Dir.exist?(@config.ssh_config_dir)
|
122
|
+
|
123
|
+
orphaned_files = []
|
124
|
+
config_files = Dir.glob(File.join(@config.ssh_config_dir, "*.conf"))
|
125
|
+
|
126
|
+
config_files.each do |file_path|
|
127
|
+
filename = File.basename(file_path, ".conf")
|
128
|
+
|
129
|
+
# Parse filename to extract project hash and VM name
|
130
|
+
if filename.match(/^([a-f0-9]{8})-(.+)$/)
|
131
|
+
project_hash = $1
|
132
|
+
vm_name = $2
|
133
|
+
|
134
|
+
# Check if this looks like an orphaned file
|
135
|
+
# (This is a basic heuristic - in practice, you might want more sophisticated detection)
|
136
|
+
file_age = Time.now - File.mtime(file_path)
|
137
|
+
|
138
|
+
# Consider files older than 30 days as potentially orphaned
|
139
|
+
if file_age > (30 * 24 * 60 * 60) # 30 days in seconds
|
140
|
+
orphaned_files << {
|
141
|
+
path: file_path,
|
142
|
+
project_hash: project_hash,
|
143
|
+
vm_name: vm_name,
|
144
|
+
age_days: (file_age / (24 * 60 * 60)).round
|
145
|
+
}
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Log detected orphaned files
|
151
|
+
unless orphaned_files.empty?
|
152
|
+
@logger.info("Detected #{orphaned_files.length} potentially orphaned SSH config files")
|
153
|
+
orphaned_files.each do |file_info|
|
154
|
+
@logger.debug("Orphaned file: #{file_info[:path]} (#{file_info[:age_days]} days old)")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
orphaned_files
|
159
|
+
end
|
160
|
+
|
161
|
+
# Remove orphaned SSH config files
|
162
|
+
def remove_orphaned_files
|
163
|
+
orphaned_files = cleanup_orphaned_files
|
164
|
+
removed_count = 0
|
165
|
+
|
166
|
+
orphaned_files.each do |file_info|
|
167
|
+
begin
|
168
|
+
File.delete(file_info[:path])
|
169
|
+
@logger.info("Removed orphaned SSH config file: #{file_info[:path]}")
|
170
|
+
removed_count += 1
|
171
|
+
rescue => e
|
172
|
+
@logger.error("Failed to remove orphaned file #{file_info[:path]}: #{e.message}")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Clean up empty directory if configured
|
177
|
+
cleanup_empty_directory if @config.cleanup_empty_dir && removed_count > 0
|
178
|
+
|
179
|
+
removed_count
|
180
|
+
end
|
181
|
+
|
182
|
+
# Get all config files in the directory
|
183
|
+
def get_all_config_files
|
184
|
+
return [] unless Dir.exist?(@config.ssh_config_dir)
|
185
|
+
Dir.glob(File.join(@config.ssh_config_dir, "*.conf"))
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
# Generate unique project hash from root path
|
191
|
+
def generate_project_hash(root_path)
|
192
|
+
Digest::MD5.hexdigest(root_path)[0, 8]
|
193
|
+
end
|
194
|
+
|
195
|
+
# Generate SSH host name for machine
|
196
|
+
def generate_host_name(machine)
|
197
|
+
if @config.project_isolation
|
198
|
+
project_name = File.basename(machine.env.root_path)
|
199
|
+
"#{project_name}-#{machine.name}"
|
200
|
+
else
|
201
|
+
machine.name.to_s
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Write file atomically using temporary file and rename
|
206
|
+
def write_file_atomically(file_path, content)
|
207
|
+
temp_file = Tempfile.new(File.basename(file_path), File.dirname(file_path))
|
208
|
+
begin
|
209
|
+
temp_file.write(content)
|
210
|
+
temp_file.close
|
211
|
+
|
212
|
+
# Set proper permissions before moving
|
213
|
+
File.chmod(0600, temp_file.path)
|
214
|
+
|
215
|
+
# Atomic move
|
216
|
+
FileUtils.mv(temp_file.path, file_path)
|
217
|
+
ensure
|
218
|
+
temp_file.unlink if temp_file && File.exist?(temp_file.path)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Clean up empty directory if no config files remain
|
223
|
+
def cleanup_empty_directory
|
224
|
+
return unless Dir.exist?(@config.ssh_config_dir)
|
225
|
+
|
226
|
+
entries = Dir.entries(@config.ssh_config_dir) - %w[. ..]
|
227
|
+
if entries.empty?
|
228
|
+
begin
|
229
|
+
# Remove Include directive before removing directory
|
230
|
+
if @config.manage_includes
|
231
|
+
require_relative 'include_manager'
|
232
|
+
include_manager = IncludeManager.new(@config)
|
233
|
+
include_manager.remove_include_directive
|
234
|
+
end
|
235
|
+
|
236
|
+
Dir.rmdir(@config.ssh_config_dir)
|
237
|
+
@logger.info("Removed empty SSH config directory: #{@config.ssh_config_dir}")
|
238
|
+
rescue => e
|
239
|
+
@logger.error("Failed to remove empty directory #{@config.ssh_config_dir}: #{e.message}")
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|