1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
#
# Converts capistrano-style ssh configuration (which uses Net::SSH) into a OpenSSH command line flags suitable for rsync.
#
# For a list of the options normally support by Net::SSH (and thus Capistrano), see
# http://net-ssh.github.com/net-ssh/classes/Net/SSH.html#method-c-start
#
# Also, to see how Net::SSH does the opposite of the conversion we are doing here, check out:
# https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/config.rb
#
# API mismatch:
#
# * many OpenSSH options not supported
# * some options only make sense for Net::SSH
# * compression: for Net::SSH, this option is supposed to accept true, false, or algorithm. OpenSSH accepts 'yes' or 'no'
#
class RsyncCommand
class SshOptions
def initialize(options={})
@options = parse_options(options)
end
def to_flags
if @options.empty?
nil
else
%[-e "ssh #{@options.join(' ')}"]
end
end
private
def parse_options(options)
options.map do |key, value|
next if value.nil?
# Convert Net::SSH options into OpenSSH options.
case key
when :auth_methods then opt_auth_methods(value)
when :bind_address then opt('BindAddress', value)
when :compression then opt('Compression', value ? 'yes' : 'no')
when :compression_level then opt('CompressionLevel', value.to_i)
when :config then value ? "-F '#{value}'" : nil
when :encryption then opt('Ciphers', [value].flatten.join(','))
when :forward_agent then opt('ForwardAgent', value)
when :global_known_hosts_file then opt('GlobalKnownHostsFile', value)
when :hmac then opt('MACs', [value].flatten.join(','))
when :host_key then opt('HostKeyAlgorithms', [value].flatten.join(','))
when :host_key_alias then opt('HostKeyAlias', value)
when :host_name then opt('HostName', value)
when :kex then opt('KexAlgorithms', [value].flatten.join(','))
when :key_data then nil # not supported
when :keys then [value].flatten.select { |k| File.exist?(k) }.map { |k| "-i '#{k}'" }
when :keys_only then opt('IdentitiesOnly', value ? 'yes' : 'no')
when :languages then nil # not applicable
when :logger then nil # not applicable
when :paranoid then opt('StrictHostKeyChecking', value ? 'yes' : 'no')
when :passphrase then nil # not supported
when :password then nil # not supported
when :port then "-p #{value.to_i}"
when :properties then nil # not applicable
when :proxy then nil # not applicable
when :rekey_blocks_limit then nil # not supported
when :rekey_limit then opt('RekeyLimit', reverse_interpret_size(value))
when :rekey_packet_limit then nil # not supported
when :timeout then opt('ConnectTimeout', value.to_i)
when :user then "-l #{value}"
when :user_known_hosts_file then multi_opt('UserKnownHostsFile', value)
when :verbose then opt('LogLevel', interpret_log_level(value))
end
end.compact
end
private
def opt(option_name, option_value)
"-o #{option_name}='#{option_value}'"
end
def multi_opt(option_name, option_values)
[option_values].flatten.map do |value|
opt(option_name, value)
end.join(' ')
end
#
# In OpenSSH, password and pubkey default to 'yes', hostbased defaults to 'no'.
# Regardless, if :auth_method is configured, then we explicitly set the auth method.
#
def opt_auth_methods(value)
value = [value].flatten
opts = []
if value.any?
if value.include? 'password'
opts << opt('PasswordAuthentication', 'yes')
else
opts << opt('PasswordAuthentication', 'no')
end
if value.include? 'publickey'
opts << opt('PubkeyAuthentication', 'yes')
else
opts << opt('PubkeyAuthentication', 'no')
end
if value.include? 'hostbased'
opts << opt('HostbasedAuthentication', 'yes')
else
opts << opt('HostbasedAuthentication', 'no')
end
end
if opts.any?
return opts.join(' ')
else
nil
end
end
#
# Converts the given integer size in bytes into a string with 'K', 'M', 'G' suffix, as appropriate.
#
# reverse of interpret_size in https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/config.rb
#
def reverse_interpret_size(size)
size = size.to_i
if size < 1024
"#{size}"
elsif size < 1024 * 1024
"#{size/1024}K"
elsif size < 1024 * 1024 * 1024
"#{size/(1024*1024)}M"
else
"#{size/(1024*1024*1024)}G"
end
end
def interpret_log_level(level)
if level.is_a? Symbol
case level
when :debug then "DEBUG"
when :info then "INFO"
when :warn then "ERROR"
when :error then "ERROR"
when :fatal then "FATAL"
else "INFO"
end
elsif level.is_a?(Integer) && defined?(Logger)
case level
when Logger::DEBUG then "DEBUG"
when Logger::INFO then "INFO"
when Logger::WARN then "ERROR"
when Logger::ERROR then "ERROR"
when Logger::FATAL then "FATAL"
else "INFO"
end
else
"INFO"
end
end
end
end
|