kitchen-sshtgz 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c4e92ac86f8af2cf922d6b29205848e55cfe99e1a3ba9575d326656fdc19411e
4
+ data.tar.gz: 9d2909d14f74b03585c544457431708a72772781ddbc9f3602554dada8276eb4
5
+ SHA512:
6
+ metadata.gz: 6f020f9ad7d934deef00c1ae0f513bd560f668406e806f2c4aa3d82e3a5e64bfb24dd5897de99262c25147f147b0d61c8a70bccd4814eae5f4e1ad7d66e20683
7
+ data.tar.gz: 9d3e8b4ad111528c6aa4487a0361abaa9ee2c1290d23a7fe34b6c018c70564a01c3afb7fc55f9f9671d869d5098d9d82ca326fda9af88f87ca8878ba56ba87b9
@@ -0,0 +1,176 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Peter Smith (<peter@petersmith.net>)
4
+ #
5
+ # Copyright (C) 2015, Peter Smith
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
+ require "tempfile"
20
+ require "zlib"
21
+ require "rubygems/package"
22
+
23
+ module Kitchen
24
+ #
25
+ # Error thrown if the Tgz has an invalid format.
26
+ #
27
+ class GzipFormatError < StandardError; end
28
+
29
+ #
30
+ # Represents a Tar-Gzip file, allowing multiple files to be combined
31
+ # into a single file. This is useful for transmitting a large number
32
+ # of files across high-latency networks.
33
+ #
34
+ class Tgz
35
+ #
36
+ # @return [String] the file system path of the generated tar-gzip file.
37
+ #
38
+ attr_reader :path
39
+
40
+ #
41
+ # Create a new Tgz object, in preparation for adding files to the
42
+ # tar-gzip archive.
43
+ #
44
+ # @param tgz_file_name [String, nil] The output file name, in
45
+ # tar-gzipped format. If not specified, a temporary file name will be generated.
46
+ #
47
+ def initialize(tgz_file_name = nil)
48
+ if tgz_file_name
49
+ # user-provided output file name
50
+ @tgz_file = File.open(tgz_file_name, "wb+")
51
+ @path = tgz_file_name
52
+ else
53
+ # auto-generated output file name
54
+ @tgz_file = Tempfile.new("tgz")
55
+ @tgz_file.binmode
56
+ @path = @tgz_file.path
57
+ end
58
+
59
+ #
60
+ # Intermediate file for writing the 'tar' content (which will then be
61
+ # gzipped into the output 'tgz' file)
62
+ #
63
+ @tar_file = Tempfile.new("tar")
64
+ @tar_file.binmode
65
+ @tar_writer = Gem::Package::TarWriter.new(@tar_file)
66
+ end
67
+
68
+ #
69
+ # Add a set of files or directories into the Tgz archive. Directories will
70
+ # be traversed, with all files and subdirectories being added (symlinks are ignored).
71
+ #
72
+ # For example:
73
+ #
74
+ # tgz.add_files('/home/me/my_dir', ['fileA', 'dirA/fileB'])
75
+ #
76
+ # @param dir [String] the directory containing the files/sub-dirs to archive.
77
+ # @param files [Array<String>] the files/sub-directories to be added, specified
78
+ # relative to the containing directory (dir).
79
+ #
80
+ def add_files(dir, files)
81
+ files.each do |file_name|
82
+ full_path = "#{dir}/#{file_name}"
83
+ next if File.symlink?(full_path)
84
+ if File.directory?(full_path)
85
+ add_directory_to_tar(dir, file_name, full_path)
86
+ else
87
+ add_file_to_tar(full_path, file_name)
88
+ end
89
+ end
90
+ end
91
+
92
+ #
93
+ # Close the Tgz object, generating the tgz file (from the tar file), and ensuring
94
+ # that it's flushed to disk.
95
+ #
96
+ def close
97
+ # ensure tar_writer flushes everything to our temporary 'tar' file.
98
+ @tar_writer.close
99
+
100
+ # proceed to convert the 'tar' file into a 'tgz' file.
101
+ @tar_file.rewind
102
+ create_tgz_file(@tar_file, @tgz_file)
103
+ @tar_file.close
104
+ end
105
+
106
+ #
107
+ # Class-methods
108
+ #
109
+ class << self
110
+ #
111
+ # Return the original size of the uncompressed file.
112
+ #
113
+ # @param file_name [String] name of the compressed file, in .tgz format.
114
+ # @return [Integer] the original (uncompressed) file's size
115
+ # @raise [GzipFormatError] if the file is not a valid Gzip file.
116
+ #
117
+ def original_size(file_name)
118
+ File.open(file_name, "r") do |file|
119
+ # the first three bytes of a gzip file must be 0x1f, 0x8b, 0x08
120
+ fail unless (file.readbyte == 0x1f) && (file.readbyte == 0x8b) &&
121
+ (file.readbyte == 0x08)
122
+ return original_file_length_field(file)
123
+ end
124
+ rescue
125
+ raise GzipFormatError, "Gzip file could not be opened, or has invalid format: #{file_name}"
126
+ end
127
+
128
+ private
129
+
130
+ #
131
+ # Gzip files have their original/uncompressed length in the last 4 bytes
132
+ # (little endian format)
133
+ #
134
+ def original_file_length_field(file)
135
+ file.seek(-4, IO::SEEK_END)
136
+ (file.readbyte) | (file.readbyte << 8) | (file.readbyte << 16) | (file.readbyte << 24)
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ #
143
+ # Recursively traverse/add a sub-directory to the tar-gzip file.
144
+ #
145
+ def add_directory_to_tar(dir, file_name, full_path)
146
+ entries = Dir.entries(full_path)
147
+ entries.delete(".")
148
+ entries.delete("..")
149
+ add_files(dir, entries.map { |entry| "#{file_name}/#{entry}" })
150
+ end
151
+
152
+ #
153
+ # Add a single file to the tar-gzip file.
154
+ #
155
+ def add_file_to_tar(full_path, file_name)
156
+ stat = File.stat(full_path)
157
+ @tar_writer.add_file(file_name, stat.mode) do |file|
158
+ File.open(full_path, "rb") do |input|
159
+ while (buff = input.read(4096))
160
+ file.write(buff)
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ #
167
+ # The temporary tar file has been fully populated, so run the compress operation
168
+ # to generate the final tar-gzip file.
169
+ #
170
+ def create_tgz_file(source_file, dest_file)
171
+ Zlib::GzipWriter.wrap(dest_file) do |gzip_writer|
172
+ FileUtils.copy_stream(source_file, gzip_writer)
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,139 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Peter Smith (<peter@petersmith.net>)
4
+ #
5
+ # Copyright (C) 2015, Peter Smith.
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
+ require "kitchen"
20
+ require "kitchen/tgz"
21
+ require "kitchen/transport/ssh"
22
+
23
+ require "net/ssh"
24
+ require "net/scp"
25
+ require "pathname"
26
+
27
+ module Kitchen
28
+ module Transport
29
+ #
30
+ # A Transport which uses the SSH protocol to execute commands and transfer files.
31
+ # In addition, files are tar-gzipped to improve performance over high-latency
32
+ # network links.
33
+ #
34
+ # Most of this class reuses functionality from the base Ssh class.
35
+ #
36
+ # @author Peter Smith <peter@petersmith.net>
37
+ #
38
+ class SshTgz < Kitchen::Transport::Ssh
39
+
40
+ #
41
+ # Manage a connection for the SshTgz transport mechanism. This is essentially
42
+ # the same as for the Ssh::Connection class, except we compress before
43
+ # uploading.
44
+ #
45
+ class Connection < Kitchen::Transport::Ssh::Connection
46
+
47
+ # (see Ssh::Connection#upload)
48
+ def upload(locals, remote)
49
+ # attempt tar-gzip upload, to improve performance.
50
+ return if Array(locals).empty? || upload_via_tgz(Array(locals), remote)
51
+
52
+ # if tgz upload fails (e.g. not supported on target platform), fall back to
53
+ # file-by-file upload.
54
+ logger.warn("Tgz upload failed. Resorting to file-by-file upload.")
55
+ super
56
+ end
57
+
58
+ private
59
+
60
+ #
61
+ # Upload a set of files onto the remote machine. Rather than uploading
62
+ # file by file, we first package all files into a tar-gzip file (.tgz)
63
+ # and send a single file. This avoids the slow file-copy time we'd
64
+ # otherwise see over a high-latency network.
65
+ #
66
+ # @param locals [Array<String>] array of path names for each of the files to
67
+ # be included.
68
+ # @param remote [String] directory on the remote host into which the files will
69
+ # be uploaded.
70
+ # @return [true, false] true on success, else false.
71
+ #
72
+ def upload_via_tgz(locals, remote)
73
+ # tar-gzip all the input files into a single .tgz file.
74
+ tgz = create_tgz_file(locals)
75
+
76
+ # upload the tar-gzip file to the remote server.
77
+ session.scp.upload!(tgz.path, "#{remote}/kitchen.tgz", {}) do |_ch, name, sent, total|
78
+ logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total
79
+ end
80
+
81
+ # extract the tar-gzip file, on the remote, into individual files.
82
+ untar_file_on_remote(remote)
83
+ File.unlink(tgz.path)
84
+
85
+ # indicate success - the files extracted correctly and won't need to be
86
+ # uploaded individually.
87
+ true
88
+ rescue => e
89
+ # on any failure, return false to indicate that upload via tgz failed and we
90
+ # should default to copying individual files.
91
+ logger.debug(".tgz upload failed. Reason: #{e}")
92
+ false
93
+ end
94
+
95
+ #
96
+ # Create a single tar-gzipped file, containing all of the individual files.
97
+ #
98
+ # @param locals [Array<String>] array of path names for each of the files to
99
+ # be included.
100
+ # @return [Kitchen::Tgz] the Tgz object, representing the tar-gzip file we created.
101
+ #
102
+ def create_tgz_file(locals)
103
+ tgz = Kitchen::Tgz.new
104
+ locals.each do |local|
105
+ pathname = Pathname.new(local)
106
+ tgz.add_files(pathname.dirname.to_s, [pathname.basename.to_s])
107
+ end
108
+ tgz.close
109
+ tgz
110
+ end
111
+
112
+ #
113
+ # Untar the tgz file on the remote.
114
+ #
115
+ # @param remote [String] file system directory on remote host into which
116
+ # the tar-gzip file was uploaded uploaded.
117
+ # @raise [Kitchen::SSHFailed] if the untar fails for some reason.
118
+ #
119
+ def untar_file_on_remote(remote)
120
+ cmd = "tar -C #{remote} -xmzf #{remote}/kitchen.tgz"
121
+ session.exec!(cmd) do |_ch, stream, data|
122
+ raise SSHFailed, "Unable to untar files on remote: #{data}" if stream == :stderr
123
+ end
124
+ end
125
+ end
126
+
127
+ # (see Ssh#create_new_connection)
128
+ def create_new_connection(options, &block)
129
+ if @connection
130
+ logger.debug("[SSH] shutting previous connection #{@connection}")
131
+ @connection.close
132
+ end
133
+
134
+ @connection_options = options
135
+ @connection = Kitchen::Transport::SshTgz::Connection.new(options, &block)
136
+ end
137
+ end
138
+ end
139
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kitchen-sshtgz
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Jacque
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-03-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: test-kitchen
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.5'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.5.4
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '2.5'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.5.4
33
+ - !ruby/object:Gem::Dependency
34
+ name: net-ssh
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 6.1.0
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '6.1'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 6.1.0
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '6.1'
53
+ - !ruby/object:Gem::Dependency
54
+ name: net-scp
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 3.0.0
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '3.0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 3.0.0
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: '3.0'
73
+ - !ruby/object:Gem::Dependency
74
+ name: zlib
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ type: :runtime
81
+ prerelease: false
82
+ version_requirements: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ - !ruby/object:Gem::Dependency
88
+ name: tempfile
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ type: :runtime
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ - !ruby/object:Gem::Dependency
102
+ name: bundler
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ type: :development
109
+ prerelease: false
110
+ version_requirements: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ - !ruby/object:Gem::Dependency
116
+ name: rake
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ description: Improved file transfers for test-kitchen using tgz archives
130
+ email:
131
+ - jeremyjacque@algolia.com
132
+ executables: []
133
+ extensions: []
134
+ extra_rdoc_files: []
135
+ files:
136
+ - lib/kitchen/tgz.rb
137
+ - lib/kitchen/transport/ssh_tgz.rb
138
+ homepage: https://github.com/algolia/kitchen-sshtgz
139
+ licenses:
140
+ - Apache-2.0
141
+ metadata: {}
142
+ post_install_message:
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubygems_version: 3.0.3
158
+ signing_key:
159
+ specification_version: 4
160
+ summary: Improved file transfers for test-kitchen
161
+ test_files: []