kitchen-sync 1.0.1 → 1.1.0
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 +4 -4
- data/lib/kitchen-sync/version.rb +2 -1
- data/lib/kitchen/transport/sftp.rb +218 -0
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5195fe4fdfca7efd26e6cb39af8faa9886d8efd
|
|
4
|
+
data.tar.gz: 86330af015cee42ae0075b628194982aa7f22a6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b519464648300eb50bcfbc2cc00edb2a3bd334dfe8149b870e5767c9ed1fb217c90ec87e10b94f0844bff15211570f8c4f3760d9f3bcc276eedeb989e48280b9
|
|
7
|
+
data.tar.gz: fbae6448d755ce2de522fc172b8db7d6355622c9e9b85075d4d003ceaf240304736583e3678180c13b4892d9a82f0c102a70ae688c9c967980916d953a8180ce
|
data/lib/kitchen-sync/version.rb
CHANGED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2014-2015, Noah Kantrowitz
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
require 'kitchen/transport/ssh'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
module Kitchen
|
|
21
|
+
module Transport
|
|
22
|
+
class Sftp < Ssh
|
|
23
|
+
CHECKSUMS_PATH = File.expand_path('../../../kitchen-sync/checksums.rb', __FILE__)
|
|
24
|
+
CHECKSUMS_HASH = Digest::SHA1.file(CHECKSUMS_PATH)
|
|
25
|
+
CHECKSUMS_REMOTE_PATH = "/tmp/checksums-#{CHECKSUMS_HASH}.rb" # This won't work on Windows targets
|
|
26
|
+
MAX_TRANSFERS = 64
|
|
27
|
+
|
|
28
|
+
# Copy-pasta from Ssh#create_new_connection because I need the SFTP
|
|
29
|
+
# connection class.
|
|
30
|
+
# Tracked in https://github.com/test-kitchen/test-kitchen/pull/726
|
|
31
|
+
def create_new_connection(options, &block)
|
|
32
|
+
if @connection
|
|
33
|
+
logger.debug("[SSH] shutting previous connection #{@connection}")
|
|
34
|
+
@connection.close
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@connection_options = options
|
|
38
|
+
@connection = self.class::Connection.new(options, &block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Connection < Ssh::Connection
|
|
42
|
+
# Wrap Ssh::Connection#close to also shut down the SFTP connection.
|
|
43
|
+
def close
|
|
44
|
+
if @sftp_session
|
|
45
|
+
logger.debug("[SFTP] closing connection to #{self}")
|
|
46
|
+
sftp_session.close_channel
|
|
47
|
+
end
|
|
48
|
+
ensure
|
|
49
|
+
@sftp_session = nil
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def upload(locals, remote)
|
|
54
|
+
Array(locals).each do |local|
|
|
55
|
+
full_remote = File.join(remote, File.basename(local))
|
|
56
|
+
recursive = File.directory?(local)
|
|
57
|
+
time = Benchmark.realtime do
|
|
58
|
+
sftp_upload!(local, full_remote, recursive)
|
|
59
|
+
end
|
|
60
|
+
logger.info("[SFTP] Time taken to upload #{local} to #{self}:#{full_remote}: %.2f sec" % time)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def sftp_upload!(local, remote, recursive)
|
|
67
|
+
# Fast path check, if the remote path doesn't exist at all we just run a direct transfer
|
|
68
|
+
unless safe_stat(remote)
|
|
69
|
+
logger.debug("[SFTP] Fast path upload from #{local} to #{remote}")
|
|
70
|
+
sftp_session.mkdir!(remote) if recursive
|
|
71
|
+
sftp_session.upload!(local, remote, requests: MAX_TRANSFERS)
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
# Get checksums for existing files on the remote side.
|
|
75
|
+
logger.debug("[SFTP] Slow path upload from #{local} to #{remote}")
|
|
76
|
+
copy_checksums_script!
|
|
77
|
+
checksum_cmd = "/opt/chef/embedded/bin/ruby #{CHECKSUMS_REMOTE_PATH} #{remote}"
|
|
78
|
+
logger.debug("[SFTP] Running #{checksum_cmd}")
|
|
79
|
+
checksums = JSON.parse(session.exec!(checksum_cmd))
|
|
80
|
+
# Sync files that have changed.
|
|
81
|
+
files_to_upload(checksums, local, recursive).each do |rel_path|
|
|
82
|
+
upload_file(checksums, local, remote, rel_path)
|
|
83
|
+
end
|
|
84
|
+
purge_files(checksums, remote)
|
|
85
|
+
# Wait until all xfers are complete.
|
|
86
|
+
sftp_loop(0)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Bug fix for session.loop never terminating if there is an SFTP conn active
|
|
90
|
+
# since as far as it is concerned there is still active stuff.
|
|
91
|
+
# This function is Copyright Fletcher Nichol
|
|
92
|
+
# Tracked in https://github.com/test-kitchen/test-kitchen/pull/724
|
|
93
|
+
def execute_with_exit_code(command)
|
|
94
|
+
exit_code = nil
|
|
95
|
+
session.open_channel do |channel|
|
|
96
|
+
|
|
97
|
+
channel.request_pty
|
|
98
|
+
|
|
99
|
+
channel.exec(command) do |_ch, _success|
|
|
100
|
+
|
|
101
|
+
channel.on_data do |_ch, data|
|
|
102
|
+
logger << data
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
channel.on_extended_data do |_ch, _type, data|
|
|
106
|
+
logger << data
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
channel.on_request("exit-status") do |_ch, data|
|
|
110
|
+
exit_code = data.read_long
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
session.loop { exit_code.nil? } # THERE IS A CHANGE ON THIS LINE, PAY ATTENTION!!!!!!
|
|
115
|
+
exit_code
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Create the SFTP session and block until it is ready.
|
|
119
|
+
#
|
|
120
|
+
# @return [Net::SFTP::Session]
|
|
121
|
+
def sftp_session
|
|
122
|
+
@sftp_session ||= session.sftp
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Return if the path exists (because net::sftp uses exceptions for that
|
|
126
|
+
# and it makes code gross) and also raise an exception if the path is a
|
|
127
|
+
# symlink.
|
|
128
|
+
#
|
|
129
|
+
# @param path [String] Remote path to check.
|
|
130
|
+
# @return [Boolean]
|
|
131
|
+
def safe_stat(path)
|
|
132
|
+
stat = sftp_session.lstat!(path)
|
|
133
|
+
raise "#{path} is a symlink, possible security threat, bailing out" if stat.symlink?
|
|
134
|
+
true
|
|
135
|
+
rescue Net::SFTP::StatusException
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Upload the checksum script if needed.
|
|
140
|
+
#
|
|
141
|
+
# @return [void]
|
|
142
|
+
def copy_checksums_script!
|
|
143
|
+
# Fast path because upload itself is called multiple times.
|
|
144
|
+
return if @checksums_copied
|
|
145
|
+
# Only try to transfer the script if it isn't present. a stat takes about
|
|
146
|
+
# 1/3rd the time of the transfer, so worst case here is still okay.
|
|
147
|
+
sftp_session.upload!(CHECKSUMS_PATH, CHECKSUMS_REMOTE_PATH) unless safe_stat(CHECKSUMS_REMOTE_PATH)
|
|
148
|
+
@checksums_copied = true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def files_to_upload(checksums, local, recursive)
|
|
152
|
+
glob_path = if recursive
|
|
153
|
+
File.join(local, '**', '*')
|
|
154
|
+
else
|
|
155
|
+
local
|
|
156
|
+
end
|
|
157
|
+
pending = []
|
|
158
|
+
Dir.glob(glob_path, File::FNM_PATHNAME | File::FNM_DOTMATCH).each do |path|
|
|
159
|
+
next unless File.file?(path)
|
|
160
|
+
rel_path = path[local.length..-1]
|
|
161
|
+
remote_hash = checksums.delete(rel_path)
|
|
162
|
+
pending << rel_path unless remote_hash && remote_hash == Digest::SHA1.file(path).hexdigest
|
|
163
|
+
end
|
|
164
|
+
pending
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def upload_file(checksums, local, remote, rel_path)
|
|
168
|
+
parts = rel_path.split('/')
|
|
169
|
+
parts.pop # Drop the filename since we are only checking dirs
|
|
170
|
+
parts_to_check = []
|
|
171
|
+
until parts.empty?
|
|
172
|
+
parts_to_check << parts.shift
|
|
173
|
+
path_to_check = parts_to_check.join('/')
|
|
174
|
+
unless checksums[path_to_check]
|
|
175
|
+
logger.debug("[SFTP] Creating directory #{remote}#{path_to_check}")
|
|
176
|
+
add_xfer(sftp_session.mkdir("#{remote}#{path_to_check}"))
|
|
177
|
+
checksums[path_to_check] = true
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
logger.debug("[SFTP] Uploading #{local}#{rel_path} to #{remote}#{rel_path}")
|
|
181
|
+
add_xfer(sftp_session.upload("#{local}#{rel_path}", "#{remote}#{rel_path}"))
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def purge_files(checksums, remote)
|
|
185
|
+
checksums.each do |key, value|
|
|
186
|
+
# Special case for /tmp/kitchen/cache upload not clearing cookbooks.
|
|
187
|
+
# Tracked in https://github.com/test-kitchen/test-kitchen/issues/725
|
|
188
|
+
next if remote == '/tmp/kitchen/cache' && key.start_with?('/cookbooks')
|
|
189
|
+
# Check if the file was uploaded in #upload_file.
|
|
190
|
+
if value != true
|
|
191
|
+
logger.debug("[SFTP] Removing #{remote}#{key}")
|
|
192
|
+
add_xfer(sftp_session.remove("#{remote}#{key}"))
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def sftp_xfers
|
|
198
|
+
@sftp_xfers ||= []
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def add_xfer(xfer)
|
|
202
|
+
sftp_xfers << xfer
|
|
203
|
+
sftp_loop
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def sftp_loop(n_xfers=MAX_TRANSFERS)
|
|
207
|
+
sftp_session.loop do
|
|
208
|
+
sftp_xfers.delete_if {|x| !(x.is_a?(Net::SFTP::Request) ? x.pending? : x.active?) } # Purge any completed operations, which has two different APIs for some reason
|
|
209
|
+
sftp_xfers.length > n_xfers # Run until we have fewer than max
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kitchen-sync
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Noah Kantrowitz
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2015-05-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: test-kitchen
|
|
@@ -87,6 +87,7 @@ files:
|
|
|
87
87
|
- lib/kitchen-sync/scp.rb
|
|
88
88
|
- lib/kitchen-sync/sftp.rb
|
|
89
89
|
- lib/kitchen-sync/version.rb
|
|
90
|
+
- lib/kitchen/transport/sftp.rb
|
|
90
91
|
homepage: https://github.com/coderanger/kitchen-sync
|
|
91
92
|
licenses:
|
|
92
93
|
- Apache 2.0
|
|
@@ -107,9 +108,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
107
108
|
version: '0'
|
|
108
109
|
requirements: []
|
|
109
110
|
rubyforge_project:
|
|
110
|
-
rubygems_version: 2.
|
|
111
|
+
rubygems_version: 2.4.5
|
|
111
112
|
signing_key:
|
|
112
113
|
specification_version: 4
|
|
113
114
|
summary: Improved file transfers for for test-kitchen
|
|
114
115
|
test_files: []
|
|
115
|
-
has_rdoc:
|