Skip to content

Commit a4e2ae3

Browse files
committed
Use more native methods of SmartProxy for validation
This uses more built in validators. It also creates a setting for the public key file and derives it if not specified. The only downside is that the validate_readable doesn't print a helpful command to generate it if needed. Programmable settings can be used to generate it on the fly if the parent directory is readable. Another alternative is to introduce another custom validator. It slightly abuses the dependency injection hook to configure the task launcher registry.
1 parent b3d0c0d commit a4e2ae3

File tree

7 files changed

+101
-88
lines changed

7 files changed

+101
-88
lines changed

lib/smart_proxy_remote_execution_ssh.rb

Lines changed: 0 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -6,83 +6,6 @@
66
module Proxy::RemoteExecution
77
module Ssh
88
class << self
9-
def validate!
10-
unless private_key_file
11-
raise "settings for `ssh_identity_key` not set"
12-
end
13-
14-
unless File.exist?(private_key_file)
15-
raise "Ssh public key file #{private_key_file} doesn't exist.\n"\
16-
"You can generate one with `ssh-keygen -t rsa -b 4096 -f #{private_key_file} -N ''`"
17-
end
18-
19-
unless File.exist?(public_key_file)
20-
raise "Ssh public key file #{public_key_file} doesn't exist"
21-
end
22-
23-
validate_mode!
24-
validate_ssh_log_level!
25-
validate_mqtt_settings!
26-
end
27-
28-
def private_key_file
29-
File.expand_path(Plugin.settings.ssh_identity_key_file)
30-
end
31-
32-
def public_key_file
33-
File.expand_path("#{private_key_file}.pub")
34-
end
35-
36-
def validate_mode!
37-
Plugin.settings.mode = Plugin.settings.mode.to_sym
38-
39-
unless Plugin::MODES.include? Plugin.settings.mode
40-
raise "Mode has to be one of #{Plugin::MODES.join(', ')}, given #{Plugin.settings.mode}"
41-
end
42-
43-
if Plugin.settings.async_ssh
44-
Plugin.logger.warn('Option async_ssh is deprecated, use ssh-async mode instead.')
45-
46-
case Plugin.settings.mode
47-
when :ssh
48-
Plugin.logger.warn('Deprecated option async_ssh used together with ssh mode, switching mode to ssh-async.')
49-
Plugin.settings.mode = :'ssh-async'
50-
when :'async-ssh'
51-
# This is a noop
52-
else
53-
Plugin.logger.warn('Deprecated option async_ssh used together with incompatible mode, ignoring.')
54-
end
55-
end
56-
end
57-
58-
def validate_mqtt_settings!
59-
return unless Plugin.settings.mode == :'pull-mqtt'
60-
61-
raise 'mqtt_broker has to be set when pull-mqtt mode is used' if Plugin.settings.mqtt_broker.nil?
62-
raise 'mqtt_port has to be set when pull-mqtt mode is used' if Plugin.settings.mqtt_port.nil?
63-
end
64-
65-
def validate_ssh_log_level!
66-
wanted_level = Plugin.settings.ssh_log_level.to_s
67-
levels = Plugin::SSH_LOG_LEVELS
68-
unless levels.include? wanted_level
69-
raise "Wrong value '#{Plugin.settings.ssh_log_level}' for ssh_log_level, must be one of #{levels.join(', ')}"
70-
end
71-
72-
current = ::Proxy::SETTINGS.log_level.to_s.downcase
73-
74-
# regular log levels correspond to upcased ssh logger levels
75-
ssh, regular = [wanted_level, current].map do |wanted|
76-
levels.each_with_index.find { |value, _index| value == wanted }.last
77-
end
78-
79-
if ssh < regular
80-
raise 'ssh_log_level cannot be more verbose than regular log level'
81-
end
82-
83-
Plugin.settings.ssh_log_level = Plugin.settings.ssh_log_level.to_sym
84-
end
85-
869
def job_storage
8710
@job_storage ||= Proxy::RemoteExecution::Ssh::JobStorage.new
8811
end

lib/smart_proxy_remote_execution_ssh/api.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class Api < ::Sinatra::Base
1010
include Proxy::Dynflow::Helpers
1111

1212
get "/pubkey" do
13-
File.read(Ssh.public_key_file)
13+
File.read(Proxy::RemoteExecution::Plugin.settings.ssh_identity_public_key_file)
1414
rescue Errno::ENOENT
1515
halt 404
1616
end

lib/smart_proxy_remote_execution_ssh/cockpit.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ def params
242242
end
243243

244244
def key_file
245-
@key_file ||= Proxy::RemoteExecution::Ssh.private_key_file
245+
@key_file ||= Proxy::RemoteExecution::Plugin.settings.ssh_identity_key_file
246246
end
247247

248248
def buf_socket

lib/smart_proxy_remote_execution_ssh/plugin.rb

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require_relative 'validators'
2+
13
module Proxy::RemoteExecution::Ssh
24
class Plugin < Proxy::Plugin
35
SSH_LOG_LEVELS = %w[debug info error fatal].freeze
@@ -19,9 +21,49 @@ class Plugin < Proxy::Plugin
1921
# :mqtt_port => nil,
2022
:mode => :ssh
2123

24+
load_validators ssh_log_level: Proxy::RemoteExecution::Ssh::Validators::SshLogLevel,
25+
rex_ssh_mode: Proxy::RemoteExecution::Ssh::Validators::RexSshMode
26+
27+
load_programmable_settings do |settings|
28+
if settings[:ssh_identity_key_file]
29+
settings[:ssh_identity_key_file] = File.expand_path(settings[:ssh_identity_key_file])
30+
end
31+
32+
if settings[:ssh_identity_public_key_file]
33+
settings[:ssh_identity_public_key_file] = File.expand_path(settings[:ssh_identity_public_key_file])
34+
elsif settings[:ssh_identity_key_file]
35+
settings[:ssh_identity_public_key_file] = "#{settings[:ssh_identity_key_file]}.pub"
36+
end
37+
38+
if settings[:async_ssh]
39+
Plugin.logger.warn('Option async_ssh is deprecated, use ssh-async mode instead.')
40+
41+
case setting_value
42+
when :ssh
43+
Plugin.logger.warn('Deprecated option async_ssh used together with ssh mode, switching mode to ssh-async.')
44+
settings[:mode] = :'ssh-async'
45+
when :'async-ssh'
46+
# This is a noop
47+
else
48+
Plugin.logger.warn('Deprecated option async_ssh used together with incompatible mode, ignoring.')
49+
end
50+
end
51+
52+
settings[:mode] = settings[:mode].to_sym
53+
settings[:ssh_log_level] = settings[:ssh_log_level].to_sym
54+
end
55+
56+
validate_readable :ssh_identity_key_file, :ssh_identity_public_key_file
57+
validate :ssh_log_level, ssh_log_level: SSH_LOG_LEVELS
58+
validate :mode, rex_ssh_mode: MODES
59+
validate_presence :mqtt_broker, :mqtt_port, if: ->(settings) { settings[:mode] == :'pull-mqtt' }
60+
2261
plugin :ssh, Proxy::RemoteExecution::Ssh::VERSION
23-
after_activation do
62+
63+
load_classes do
2464
require 'smart_proxy_dynflow'
65+
require 'smart_proxy_dynflow/task_launcher'
66+
require 'smart_proxy_dynflow/runner'
2567
require 'smart_proxy_remote_execution_ssh/version'
2668
require 'smart_proxy_remote_execution_ssh/cockpit'
2769
require 'smart_proxy_remote_execution_ssh/api'
@@ -31,9 +73,10 @@ class Plugin < Proxy::Plugin
3173
require 'smart_proxy_remote_execution_ssh/runners'
3274
require 'smart_proxy_remote_execution_ssh/utils'
3375
require 'smart_proxy_remote_execution_ssh/job_storage'
76+
end
3477

35-
Proxy::RemoteExecution::Ssh.validate!
36-
78+
# Not really Smart Proxy dependency injection, but similar enough
79+
load_dependency_injection_wirings do |container_instance, settings|
3780
Proxy::Dynflow::TaskLauncherRegistry.register('ssh', Proxy::Dynflow::TaskLauncher::Batch)
3881
end
3982

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module Proxy
2+
module RemoteExecution
3+
module Ssh
4+
module Validators
5+
class SshLogLevel < ::Proxy::PluginValidators::Base
6+
def validate!(settings)
7+
setting_value = settings[@setting_name].to_s
8+
9+
unless @params.include?(setting_value)
10+
raise ::Proxy::Error::ConfigurationError, "Parameter '#{@setting_name}' must be one of #{@params.join(', ')}"
11+
end
12+
13+
current = ::Proxy::SETTINGS.log_level.to_s.downcase
14+
15+
# regular log levels correspond to upcased ssh logger levels
16+
ssh, regular = [setting_value, current].map do |wanted|
17+
@params.each_with_index.find { |value, _index| value == wanted }.last
18+
end
19+
20+
if ssh < regular
21+
raise ::Proxy::Error::ConfigurationError, "Parameter '#{@setting_name}' cannot be more verbose than regular log level (#{current})"
22+
end
23+
24+
true
25+
end
26+
end
27+
28+
class RexSshMode < ::Proxy::PluginValidators::Base
29+
def validate!(settings)
30+
setting_value = settings[@setting_name]
31+
32+
unless @params.include?(setting_value)
33+
raise ::Proxy::Error::ConfigurationError, "Parameter '#{@setting_name}' must be one of #{@params.join(', ')}"
34+
end
35+
36+
true
37+
end
38+
end
39+
end
40+
end
41+
end
42+
end

test/api_test.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ class ApiTest < MiniTest::Spec
2727

2828
def setup
2929
super
30-
Proxy::RemoteExecution::Ssh::Plugin.load_test_settings(ssh_identity_key_file: FAKE_PRIVATE_KEY_FILE)
30+
Proxy::RemoteExecution::Ssh::Plugin.load_test_settings(ssh_identity_key_file: FAKE_PRIVATE_KEY_FILE, ssh_identity_public_key_file: FAKE_PUBLIC_KEY_FILE)
3131
end
3232

3333
let(:app) { Proxy::RemoteExecution::Ssh::Api.new }
3434

3535
describe '/pubkey' do
3636
it 'returns the content of the public key' do
3737
get '/pubkey'
38+
_(last_response.status).must_equal 200, last_response.body
3839
_(last_response.body).must_equal '===public-key==='
3940
end
4041
end

test/integration_test.rb

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ def app
1414

1515
def test_features_for_default_mode_without_dynflow
1616
Proxy::LegacyModuleLoader.any_instance.expects(:load_configuration_file).with('dynflow.yml').returns(enabled: false)
17-
Proxy::LegacyModuleLoader.any_instance.expects(:load_configuration_file).with('remote_execution_ssh.yml').returns(
17+
Proxy::DefaultModuleLoader.any_instance.expects(:load_configuration_file).with('remote_execution_ssh.yml').returns(
1818
enabled: true,
1919
ssh_identity_key_file: FAKE_PRIVATE_KEY_FILE,
20+
mqtt_broker: 'broker.example.com',
21+
mqtt_port: 1883,
2022
)
2123

2224
get '/features'
@@ -30,7 +32,7 @@ def test_features_for_default_mode_without_dynflow
3032

3133
def test_features_for_default_mode_with_dynflow
3234
Proxy::LegacyModuleLoader.any_instance.expects(:load_configuration_file).with('dynflow.yml').returns(enabled: true)
33-
Proxy::LegacyModuleLoader.any_instance.expects(:load_configuration_file).with('remote_execution_ssh.yml').returns(
35+
Proxy::DefaultModuleLoader.any_instance.expects(:load_configuration_file).with('remote_execution_ssh.yml').returns(
3436
enabled: true,
3537
ssh_identity_key_file: FAKE_PRIVATE_KEY_FILE,
3638
)
@@ -52,7 +54,7 @@ def test_features_for_default_mode_with_dynflow
5254

5355
def test_features_for_pull_mqtt_mode_without_required_options
5456
Proxy::LegacyModuleLoader.any_instance.expects(:load_configuration_file).with('dynflow.yml').returns(enabled: true)
55-
Proxy::LegacyModuleLoader.any_instance.expects(:load_configuration_file).with('remote_execution_ssh.yml').returns(
57+
Proxy::DefaultModuleLoader.any_instance.expects(:load_configuration_file).with('remote_execution_ssh.yml').returns(
5658
enabled: true,
5759
ssh_identity_key_file: FAKE_PRIVATE_KEY_FILE,
5860
mode: 'pull-mqtt',
@@ -68,12 +70,14 @@ def test_features_for_pull_mqtt_mode_without_required_options
6870

6971
mod = response['ssh']
7072
refute_nil(mod)
71-
assert_equal('running', mod['state'], Proxy::LogBuffer::Buffer.instance.info[:failed_modules][:ssh])
73+
assert_equal('failed', mod['state'], Proxy::LogBuffer::Buffer.instance.info[:failed_modules][:ssh])
74+
assert_equal("Disabling all modules in the group ['ssh'] due to a failure in one of them: Parameter 'mqtt_broker' is expected to have a non-empty value",
75+
Proxy::LogBuffer::Buffer.instance.info[:failed_modules][:ssh])
7276
end
7377

7478
def test_features_with_dynflow_and_required_options
7579
Proxy::LegacyModuleLoader.any_instance.expects(:load_configuration_file).with('dynflow.yml').returns(enabled: true)
76-
Proxy::LegacyModuleLoader.any_instance.expects(:load_configuration_file).with('remote_execution_ssh.yml').returns(
80+
Proxy::DefaultModuleLoader.any_instance.expects(:load_configuration_file).with('remote_execution_ssh.yml').returns(
7781
enabled: true,
7882
ssh_identity_key_file: FAKE_PRIVATE_KEY_FILE,
7983
mode: 'pull-mqtt',

0 commit comments

Comments
 (0)