kitchen-sshtgz 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/kitchen/tgz.rb +176 -0
- data/lib/kitchen/transport/ssh_tgz.rb +139 -0
- metadata +161 -0
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
|
data/lib/kitchen/tgz.rb
ADDED
@@ -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: []
|