kitchen-sync 1.1.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6f2b1b3b4c221ab2f468651f000053dd60b61610
4
- data.tar.gz: 9bfcb40e1719e254e2c34219bda3c8f98cc26b23
3
+ metadata.gz: de3176802b5d0d2d9e1f1596f6dea7c3b779a5e8
4
+ data.tar.gz: fb8c7e2f628f05a4b02f4412b3a17fc815d6f88c
5
5
  SHA512:
6
- metadata.gz: 640a071aa0972fb700e665f2c53d5ab88aeeac87b91d3da416a6bf5ec62ab52bd202248fb0636f6eea3331c3a6652ee7f91368c078f19ed8a4671793f0350625
7
- data.tar.gz: 54bf3f84fc40c258d9023b5494f73eeb60f017f041ec54f0b828bf03ea4b409469797669e8234362370c814443202dd228c76d175792bb5e3f663d394c5e9fd2
6
+ metadata.gz: 4227cbf1e1a90875146c48ad09a508a550ef19adba081cde50b14449ff9a55b0b22791da4a4c85a5fc94c3f52a6def3874a2d2dc31093a5f75bcaf64774ca032
7
+ data.tar.gz: e75859c18388457ae8df42177cfa22a8fdfe6982dcedcca4afaf933133173cbf8a30fd6d87d7e3ca0f6b3330f2d77bc8946a22daa4a9fd7e25e6ea108edabb20
@@ -0,0 +1,21 @@
1
+ # Kitchen-Sync Changelog
2
+
3
+ ## v2.0.0
4
+
5
+ Fully revamped at last for Test Kitchen's new modular transports.
6
+
7
+ ## v1.1.1
8
+
9
+ Bugfix for the new SFTP transport.
10
+
11
+ ## v1.1.0
12
+
13
+ First stab at a Test Kitchen 1.4 transport plugin.
14
+
15
+ ## v1.0.1
16
+
17
+ Bugfix for the SFTP transport provider.
18
+
19
+ ## v1.0.0
20
+
21
+ Initial release!
data/README.md CHANGED
@@ -9,43 +9,49 @@ most of which are faster than the default, thus speeding up your test runs.
9
9
  Quick Start
10
10
  -----------
11
11
 
12
- Add `gem 'kitchen-sync'` to your Gemfile and then at the top of your
13
- `.kitchen.yml`:
12
+ Run `chef gem install kitchen-sync` and then set your transport to `sftp`:
14
13
 
15
14
  ```
16
- #<% require 'kitchen-sync' %>
15
+ transport:
16
+ name: sftp
17
17
  ```
18
18
 
19
19
  Available Transfer Methods
20
20
  --------------------------
21
21
 
22
- You can select the transfer mode using the `KITCHEN_SYNC_MODE` environment
23
- variable. If not present, it defaults to `sftp`.
24
-
25
- ### SFTP
22
+ ### `sftp`
26
23
 
27
24
  The default mode uses SFTP for file transfers, as well as a helper script to
28
25
  avoid recopying files that are already present on the test host. If SFTP is
29
26
  disabled, this will automatically fall back to the SCP mode.
30
27
 
31
- ### SCP
32
-
33
- The SCP mode is just a copy of the implementation from test-kitchen. It is
34
- present as a fallback and for benchmark comparisons, and generally won't be
35
- used directly.
28
+ ### `rsync`
36
29
 
37
- ### Rsync
38
-
39
- The rsync mode is based on the work done by [Mikhail Bautin](https://github.com/test-kitchen/test-kitchen/pull/359).
30
+ The Rsync mode is based on the work done by [Mikhail Bautin](https://github.com/test-kitchen/test-kitchen/pull/359).
40
31
  This is the fastest mode, but it does have a few downsides. The biggest is that
41
32
  you must be using `ssh-agent` and have an identity loaded for it to use. It also
42
33
  requires that rsync be available on the remote side. Consider this implementation
43
- more experimental than the others at this time.
34
+ more experimental than `sftp` at this time.
35
+
36
+ Windows Guests
37
+ --------------
38
+
39
+ Windows is not specifically supported at this time, though if you have an SSH
40
+ server it will probably work. There is no support for WinRM.
41
+
42
+ Upgrading from 1.x
43
+ ------------------
44
+
45
+ As of version 2.0, kitchen-sync uses Test Kitchen's modular transport system
46
+ rather than monkey patch overrides. To upgrade, remove the `<% require 'kitchen-sync' %>`
47
+ from your `.kitchen.yml` and add the transport configuration mentioned above.
48
+ The `$KITCHEN_SYNC_MODE` environment variable is no longer needed as configuration
49
+ can happen in the normal Yaml file.
44
50
 
45
51
  License
46
52
  -------
47
53
 
48
- Copyright 2014, Noah Kantrowitz
54
+ Copyright 2014-2016, Noah Kantrowitz
49
55
 
50
56
  Licensed under the Apache License, Version 2.0 (the "License");
51
57
  you may not use this file except in compliance with the License.
@@ -1,7 +1,5 @@
1
1
  #
2
- # Author:: Noah Kantrowitz <noah@coderanger.net>
3
- #
4
- # Copyright 2014, Noah Kantrowitz
2
+ # Copyright 2014-2016, Noah Kantrowitz
5
3
  #
6
4
  # Licensed under the Apache License, Version 2.0 (the "License");
7
5
  # you may not use this file except in compliance with the License.
@@ -16,65 +14,7 @@
16
14
  # limitations under the License.
17
15
  #
18
16
 
19
- require 'benchmark'
20
- require 'digest/sha1'
21
- require 'json'
22
-
23
- require 'kitchen/ssh'
24
- require 'kitchen/provisioner/chef_base'
25
- require 'net/sftp'
26
-
27
- require 'kitchen-sync/core_ext'
28
- require 'kitchen-sync/rsync'
29
- require 'kitchen-sync/scp'
30
- require 'kitchen-sync/sftp'
31
- require 'kitchen-sync/version'
32
-
33
17
 
34
18
  class KitchenSync
35
- IMPLEMENTATIONS = {
36
- 'rsync' => Rsync,
37
- 'scp' => SCP,
38
- 'sftp' => SFTP,
39
- }
40
-
41
- def initialize(logger, session, options)
42
- @logger = logger
43
- @session = session
44
- @options = options
45
- @impl = load_implementation
46
- end
47
-
48
- def load_implementation(default_mode='sftp')
49
- mode = (ENV['KITCHEN_SYNC_MODE'] || default_mode).downcase
50
- @logger.debug("[sync] Using transfer mode #{mode}")
51
- impl_class = IMPLEMENTATIONS[mode]
52
- raise "Sync implementation for #{mode} not found" unless impl_class
53
- # Create the instance, any error during init means we use SCP instead
54
- begin
55
- impl_class.new(@logger, @session, @options)
56
- rescue Exception
57
- if impl_class != SCP
58
- @logger.debug("[sync] Falling back to SCP")
59
- impl_class = SCP
60
- retry
61
- else
62
- raise
63
- end
64
- end
65
- end
66
-
67
- def upload(local, remote, options)
68
- # This is set even for single files, so make it something that matters again
69
- options[:recursive] = File.directory?(local)
70
- time = Benchmark.realtime do
71
- @impl.upload(local, remote, options[:recursive])
72
- end
73
- @logger.info("[sync] Time taken to upload #{local} to #{@session}:#{remote}: " +
74
- "%.2f sec" % time)
75
- end
76
-
77
- def shutdown
78
- @impl.shutdown
79
- end
19
+ autoload :VERSION, 'kitchen-sync/version'
80
20
  end
@@ -1,7 +1,5 @@
1
1
  #
2
- # Author:: Noah Kantrowitz <noah@coderanger.net>
3
- #
4
- # Copyright 2014, Noah Kantrowitz
2
+ # Copyright 2014-2016, Noah Kantrowitz
5
3
  #
6
4
  # Licensed under the Apache License, Version 2.0 (the "License");
7
5
  # you may not use this file except in compliance with the License.
@@ -18,55 +16,6 @@
18
16
 
19
17
  # ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻
20
18
  module Kitchen
21
- class SSH
22
-
23
- #old_upload = instance_method(:upload!)
24
- define_method(:upload!) do |local, remote, options = {}, &progress|
25
- @kitchen_sync ||= KitchenSync.new(logger, session, @options)
26
- @kitchen_sync.upload(local, remote, options)
27
- end
28
-
29
- # Monkey patch the shutdown to tear down the SFTP connection too.
30
- old_shutdown = instance_method(:shutdown)
31
- define_method(:shutdown) do
32
- begin
33
- @kitchen_sync.shutdown if @kitchen_sync
34
- ensure
35
- old_shutdown.bind(self).call
36
- end
37
- end
38
-
39
- private
40
-
41
- # Bug fix for session.loop never terminating if there is an SFTP conn active
42
- # since as far as it is concerned there is still active stuff.
43
- # This function is Copyright Fletcher Nichol
44
- def exec_with_exit(cmd)
45
- exit_code = nil
46
- session.open_channel do |channel|
47
-
48
- channel.request_pty
49
-
50
- channel.exec(cmd) do |ch, success|
51
-
52
- channel.on_data do |ch, data|
53
- logger << data
54
- end
55
-
56
- channel.on_extended_data do |ch, type, data|
57
- logger << data
58
- end
59
-
60
- channel.on_request("exit-status") do |ch, data|
61
- exit_code = data.read_long
62
- end
63
- end
64
- end
65
- session.loop { !exit_code } # THERE IS A CHANGE ON THIS LINE, PAY ATTENTION!!!!!!
66
- exit_code
67
- end
68
- end
69
-
70
19
  # Monkey patch to prevent the deletion of everything
71
20
  module Provisioner
72
21
  class ChefBase < Base
@@ -1,7 +1,5 @@
1
1
  #
2
- # Author:: Noah Kantrowitz <noah@coderanger.net>
3
- #
4
- # Copyright 2014, Noah Kantrowitz
2
+ # Copyright 2014-2016, Noah Kantrowitz
5
3
  #
6
4
  # Licensed under the Apache License, Version 2.0 (the "License");
7
5
  # you may not use this file except in compliance with the License.
@@ -18,5 +16,5 @@
18
16
 
19
17
 
20
18
  class KitchenSync
21
- VERSION = '1.1.1'
19
+ VERSION = '2.0.0'
22
20
  end
@@ -0,0 +1,106 @@
1
+ #
2
+ # Copyright 2014-2016, 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 'base64'
18
+
19
+ require 'kitchen/transport/ssh'
20
+ require 'net/ssh'
21
+
22
+ require 'kitchen-sync/core_ext'
23
+
24
+ module Kitchen
25
+ module Transport
26
+ class Rsync < Ssh
27
+ # Copy-pasta from Ssh#create_new_connection because I need the Rsync
28
+ # connection class.
29
+ # Tracked in https://github.com/test-kitchen/test-kitchen/pull/726
30
+ def create_new_connection(options, &block)
31
+ if @connection
32
+ logger.debug("[SSH] shutting previous connection #{@connection}")
33
+ @connection.close
34
+ end
35
+
36
+ @connection_options = options
37
+ @connection = self.class::Connection.new(options, &block)
38
+ end
39
+
40
+ class Connection < Ssh::Connection
41
+ def upload(locals, remote)
42
+ if @rsync_failed || !File.exists?('/usr/bin/rsync')
43
+ logger.debug('[rsync] Rsync already failed or not installed, not trying it')
44
+ return super
45
+ end
46
+
47
+ locals = Array(locals)
48
+ # We only try to sync folders for now.
49
+ rsync_candidates = locals.select {|path| File.directory?(path) }
50
+ ssh_command = "ssh #{ssh_args.join(' ')}"
51
+ copy_identity
52
+ rsync_cmd = "/usr/bin/rsync -e '#{ssh_command}' -az#{logger.level == :debug ? 'vv' : ''} #{rsync_candidates.join(' ')} #{@session.options[:user]}@#{@session.host}:#{remote}"
53
+ logger.debug("[rsync] Running rsync command: #{rsync_cmd}")
54
+ ret = []
55
+ time = Benchmark.realtime do
56
+ ret << system(rsync_cmd)
57
+ end
58
+ logger.info("[rsync] Time taken to upload #{rsync_candidates.join(';')} to #{self}:#{remote}: %.2f sec" % time)
59
+ unless ret.first
60
+ logger.warn("[rsync] rsync exited with status #{$?.exitstatus}, using SCP instead")
61
+ @rsync_failed = true
62
+ end
63
+
64
+ # Fall back to SCP
65
+ remaining = if @rsync_failed
66
+ locals
67
+ else
68
+ locals - rsync_candidates
69
+ end
70
+ logger.debug("[rsync] Using fallback to upload #{remaining.join(';')}")
71
+ super(remaining, remote) unless remaining.empty?
72
+ end
73
+
74
+ # Copy your SSH identity, creating a new one if needed
75
+ def copy_identity
76
+ return if @copied_identity
77
+ identities = Net::SSH::Authentication::Agent.connect.identities
78
+ raise 'No SSH identities found. Please run ssh-add.' if identities.empty?
79
+ key = identities.first
80
+ enc_key = Base64.encode64(key.to_blob).gsub("\n", '')
81
+ identitiy = "ssh-rsa #{enc_key} #{key.comment}"
82
+ @session.exec! <<-EOT
83
+ test -e ~/.ssh || mkdir ~/.ssh
84
+ test -e ~/.ssh/authorized_keys || touch ~/.ssh/authorized_keys
85
+ if ! grep -q "#{identitiy}" ~/.ssh/authorized_keys ; then
86
+ chmod go-w ~ ~/.ssh ~/.ssh/authorized_keys ; \
87
+ echo "#{identitiy}" >> ~/.ssh/authorized_keys
88
+ fi
89
+ EOT
90
+ @copied_identity = true
91
+ end
92
+
93
+ def ssh_args
94
+ args = %W{ -o UserKnownHostsFile=/dev/null }
95
+ args += %W{ -o StrictHostKeyChecking=no }
96
+ args += %W{ -o IdentitiesOnly=yes } if @options[:keys]
97
+ args += %W{ -o LogLevel=#{@logger.debug? ? "VERBOSE" : "ERROR"} }
98
+ args += %W{ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} } if @options.key? :forward_agent
99
+ Array(@options[:keys]).each { |ssh_key| args += %W{ -i #{ssh_key}} }
100
+ args += %W{ -p #{@session.options[:port]}}
101
+ end
102
+ end
103
+
104
+ end
105
+ end
106
+ end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2014-2015, Noah Kantrowitz
2
+ # Copyright 2014-2016, Noah Kantrowitz
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -14,9 +14,15 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
+ require 'benchmark'
18
+ require 'digest/sha1'
19
+ require 'json'
20
+
17
21
  require 'kitchen/transport/ssh'
18
22
  require 'net/sftp'
19
23
 
24
+ require 'kitchen-sync/core_ext'
25
+
20
26
 
21
27
  module Kitchen
22
28
  module Transport
@@ -184,9 +190,6 @@ module Kitchen
184
190
 
185
191
  def purge_files(checksums, remote)
186
192
  checksums.each do |key, value|
187
- # Special case for /tmp/kitchen/cache upload not clearing cookbooks.
188
- # Tracked in https://github.com/test-kitchen/test-kitchen/issues/725
189
- next if remote == '/tmp/kitchen/cache' && key.start_with?('/cookbooks')
190
193
  # Check if the file was uploaded in #upload_file.
191
194
  if value != true
192
195
  logger.debug("[SFTP] Removing #{remote}#{key}")
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.1.1
4
+ version: 2.0.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: 2015-05-24 00:00:00.000000000 Z
11
+ date: 2016-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: test-kitchen
@@ -74,19 +74,17 @@ extensions: []
74
74
  extra_rdoc_files: []
75
75
  files:
76
76
  - ".gitignore"
77
+ - CHANGELOG.md
77
78
  - Gemfile
78
79
  - LICENSE
79
80
  - README.md
80
81
  - Rakefile
81
82
  - kitchen-sync.gemspec
82
83
  - lib/kitchen-sync.rb
83
- - lib/kitchen-sync/base.rb
84
84
  - lib/kitchen-sync/checksums.rb
85
85
  - lib/kitchen-sync/core_ext.rb
86
- - lib/kitchen-sync/rsync.rb
87
- - lib/kitchen-sync/scp.rb
88
- - lib/kitchen-sync/sftp.rb
89
86
  - lib/kitchen-sync/version.rb
87
+ - lib/kitchen/transport/rsync.rb
90
88
  - lib/kitchen/transport/sftp.rb
91
89
  homepage: https://github.com/coderanger/kitchen-sync
92
90
  licenses:
@@ -108,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
106
  version: '0'
109
107
  requirements: []
110
108
  rubyforge_project:
111
- rubygems_version: 2.4.5
109
+ rubygems_version: 2.4.8
112
110
  signing_key:
113
111
  specification_version: 4
114
112
  summary: Improved file transfers for for test-kitchen
@@ -1,35 +0,0 @@
1
- #
2
- # Author:: Noah Kantrowitz <noah@coderanger.net>
3
- #
4
- # Copyright 2014, Noah Kantrowitz
5
- #
6
- # Licensed under the Apache License, Version 2.0 (the "License");
7
- # you may not use this file except in compliance with the License.
8
- # You may obtain a copy of the License at
9
- #
10
- # http://www.apache.org/licenses/LICENSE-2.0
11
- #
12
- # Unless required by applicable law or agreed to in writing, software
13
- # distributed under the License is distributed on an "AS IS" BASIS,
14
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- # See the License for the specific language governing permissions and
16
- # limitations under the License.
17
- #
18
-
19
- class KitchenSync
20
- class Base
21
- def initialize(logger, session, options)
22
- @logger = logger
23
- @session = session
24
- @options = options
25
- end
26
-
27
- def upload(local, remote, recursive=true)
28
- raise NotImplementedError
29
- end
30
-
31
- def shutdown
32
- # This space left intentionally blank
33
- end
34
- end
35
- end
@@ -1,70 +0,0 @@
1
- #
2
- # Author:: Noah Kantrowitz <noah@coderanger.net>
3
- #
4
- # Copyright 2013-2014, Fletcher Nichol
5
- # Copyright 2014, Noah Kantrowitz
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
- #
19
-
20
- require 'kitchen-sync/scp'
21
-
22
- class KitchenSync
23
- class Rsync < SCP
24
- def upload(local, remote, recursive=true)
25
- upload_done = false
26
- if !@rsync_failed && recursive && File.exists?('/usr/bin/rsync')
27
- ssh_command = "ssh #{ssh_args.join(' ')}"
28
- copy_identity
29
- rsync_cmd = "/usr/bin/rsync -e '#{ssh_command}' -az #{local} #{@session.options[:user]}@#{@session.host}:#{remote}"
30
- @logger.info("[sync:rsync] Running rsync command: #{rsync_cmd}")
31
- if system(rsync_cmd)
32
- upload_done = true
33
- else
34
- @logger.warn("[sync:rsync] rsync exited with status #{$?.exitstatus}, using Net::SCP instead")
35
- @rsync_failed = true
36
- end
37
- end
38
-
39
- # Fall back to SCP
40
- super unless upload_done
41
- end
42
-
43
- # Copy your SSH identity, creating a new one if needed
44
- def copy_identity
45
- return if @copied_identity
46
- key = Net::SSH::Authentication::Agent.connect.identities.first
47
- enc_key = Base64.encode64(key.to_blob).gsub("\n", '')
48
- identitiy = "ssh-rsa #{enc_key} #{key.comment}"
49
- @session.exec! <<-EOT
50
- test -e ~/.ssh || mkdir ~/.ssh
51
- test -e ~/.ssh/authorized_keys || touch ~/.ssh/authorized_keys
52
- if ! grep -q "#{identitiy}" ~/.ssh/authorized_keys ; then
53
- chmod go-w ~ ~/.ssh ~/.ssh/authorized_keys ; \
54
- echo "#{identitiy}" >> ~/.ssh/authorized_keys
55
- fi
56
- EOT
57
- @copied_identity = true
58
- end
59
-
60
- def ssh_args
61
- args = %W{ -o UserKnownHostsFile=/dev/null }
62
- args += %W{ -o StrictHostKeyChecking=no }
63
- args += %W{ -o IdentitiesOnly=yes } if @options[:keys]
64
- args += %W{ -o LogLevel=#{@logger.debug? ? "VERBOSE" : "ERROR"} }
65
- args += %W{ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} } if @options.key? :forward_agent
66
- Array(@options[:keys]).each { |ssh_key| args += %W{ -i #{ssh_key}} }
67
- args += %W{ -p #{@session.options[:port]}}
68
- end
69
- end
70
- end
@@ -1,36 +0,0 @@
1
- #
2
- # Author:: Noah Kantrowitz <noah@coderanger.net>
3
- #
4
- # Copyright 2013-2014, Fletcher Nichol
5
- # Copyright 2014, Noah Kantrowitz
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
- #
19
-
20
- require 'kitchen-sync/base'
21
-
22
- require 'net/scp'
23
-
24
- class KitchenSync
25
- class SCP < Base
26
- def upload(local, remote, recursive=true)
27
- true_remote = File.join(remote, File.basename(local))
28
- @session.exec!("rm -rf #{true_remote}")
29
- @session.scp.upload!(local, remote, recursive: recursive) do |ch, name, sent, total|
30
- if sent == total
31
- @logger.debug("Uploaded #{name} (#{total} bytes)")
32
- end
33
- end
34
- end
35
- end
36
- end
@@ -1,138 +0,0 @@
1
- #
2
- # Author:: Noah Kantrowitz <noah@coderanger.net>
3
- #
4
- # Copyright 2014, Noah Kantrowitz
5
- #
6
- # Licensed under the Apache License, Version 2.0 (the "License");
7
- # you may not use this file except in compliance with the License.
8
- # You may obtain a copy of the License at
9
- #
10
- # http://www.apache.org/licenses/LICENSE-2.0
11
- #
12
- # Unless required by applicable law or agreed to in writing, software
13
- # distributed under the License is distributed on an "AS IS" BASIS,
14
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- # See the License for the specific language governing permissions and
16
- # limitations under the License.
17
- #
18
-
19
- require 'kitchen-sync/base'
20
-
21
- require 'net/sftp'
22
-
23
- class KitchenSync
24
- class SFTP < Base
25
- CHECKSUMS_PATH = File.expand_path('../checksums.rb', __FILE__)
26
- CHECKSUMS_HASH = Digest::SHA1.file(CHECKSUMS_PATH)
27
- CHECKSUMS_REMOTE_PATH = "/tmp/checksums-#{CHECKSUMS_HASH}.rb" # This won't work on Windows targets
28
- MAX_TRANSFERS = 64
29
-
30
- def initialize(*args)
31
- super
32
- @sftp = @session.sftp
33
- @xfers = []
34
- end
35
-
36
- def upload(local, remote, recursive=true)
37
- remote = File.join(remote, File.basename(local))
38
- # Fast path check, if the remote path doesn't exist at all we just run a direct transfer
39
- unless safe_stat(remote)
40
- @logger.debug("[sync:sftp] Fast path upload from #{local} to #{remote}")
41
- @sftp.mkdir!(remote) if recursive
42
- @sftp.upload!(local, remote, requests: MAX_TRANSFERS)
43
- return
44
- end
45
- copy_checksums_script
46
- # Get our checksums
47
- checksum_cmd = "/opt/chef/embedded/bin/ruby #{CHECKSUMS_REMOTE_PATH} #{remote}"
48
- @logger.info("[sync:sftp] Running #{checksum_cmd}")
49
- checksums = JSON.parse(@session.exec!(checksum_cmd))
50
- files_to_upload(checksums, local, recursive).each do |rel_path|
51
- upload_file(checksums, local, remote, rel_path)
52
- end
53
- purge_files(checksums, remote)
54
- sftp_loop(0) # Wait until all xfers are complete
55
- end
56
-
57
- def shutdown
58
- @logger.debug("[sync:sftp] closing connection to #{@session}")
59
- @sftp.close_channel
60
- @sftp = nil
61
- end
62
-
63
- private
64
-
65
- # Return if the path exists (because net::sftp uses exceptions for that and
66
- # it makes code gross) and also raise an exception if the path is a symlink.
67
- def safe_stat(path)
68
- stat = @sftp.lstat!(path)
69
- raise "#{path} is a symlink, possible security threat, bailing out" if stat.symlink?
70
- true
71
- rescue Net::SFTP::StatusException
72
- false
73
- end
74
-
75
- def copy_checksums_script
76
- return if @checksums_copied
77
- # Only try to transfer the script if it isn't present. a stat takes about
78
- # 1/3rd the time of the transfer, so worst case here is still okay.
79
- @sftp.upload!(CHECKSUMS_PATH, CHECKSUMS_REMOTE_PATH) unless safe_stat(CHECKSUMS_REMOTE_PATH)
80
- @checksums_copied = true
81
- end
82
-
83
- def files_to_upload(checksums, local, recursive)
84
- glob_path = if recursive
85
- File.join(local, '**', '*')
86
- else
87
- local
88
- end
89
- pending = []
90
- Dir.glob(glob_path, File::FNM_PATHNAME | File::FNM_DOTMATCH).each do |path|
91
- next unless File.file?(path)
92
- rel_path = path[local.length..-1]
93
- remote_hash = checksums.delete(rel_path)
94
- pending << rel_path unless remote_hash && remote_hash == Digest::SHA1.file(path).hexdigest
95
- end
96
- pending
97
- end
98
-
99
- def upload_file(checksums, local, remote, rel_path)
100
- parts = rel_path.split('/')
101
- parts.pop # Drop the filename since we are only checking dirs
102
- parts_to_check = []
103
- until parts.empty?
104
- parts_to_check << parts.shift
105
- path_to_check = parts_to_check.join('/')
106
- unless checksums[path_to_check]
107
- @logger.debug("[sync:sftp] Creating directory #{remote}#{path_to_check}")
108
- add_xfer(@sftp.mkdir("#{remote}#{path_to_check}"))
109
- checksums[path_to_check] = true
110
- end
111
- end
112
- @logger.debug("[sync:sftp] Uploading #{local}#{rel_path} to #{remote}#{rel_path}")
113
- add_xfer(@sftp.upload("#{local}#{rel_path}", "#{remote}#{rel_path}"))
114
- end
115
-
116
- def purge_files(checksums, remote)
117
- checksums.each do |key, value|
118
- if value != true
119
- @logger.debug("[sync:sftp] Removing #{remote}#{key}")
120
- add_xfer(@sftp.remove("#{remote}#{key}"))
121
- end
122
- end
123
- end
124
-
125
- def add_xfer(xfer)
126
- @xfers << xfer
127
- sftp_loop
128
- end
129
-
130
- def sftp_loop(n_xfers=MAX_TRANSFERS)
131
- @sftp.loop do
132
- @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
133
- @xfers.length > n_xfers # Run until we have fewer than max
134
- end
135
- end
136
-
137
- end
138
- end