r-train 0.9.1

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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +45 -0
  4. data/.travis.yml +12 -0
  5. data/Gemfile +22 -0
  6. data/LICENSE +201 -0
  7. data/README.md +137 -0
  8. data/Rakefile +39 -0
  9. data/lib/train.rb +100 -0
  10. data/lib/train/errors.rb +23 -0
  11. data/lib/train/extras.rb +15 -0
  12. data/lib/train/extras/command_wrapper.rb +105 -0
  13. data/lib/train/extras/file_common.rb +131 -0
  14. data/lib/train/extras/linux_file.rb +74 -0
  15. data/lib/train/extras/linux_lsb.rb +60 -0
  16. data/lib/train/extras/os_common.rb +131 -0
  17. data/lib/train/extras/os_detect_darwin.rb +32 -0
  18. data/lib/train/extras/os_detect_linux.rb +126 -0
  19. data/lib/train/extras/os_detect_unix.rb +77 -0
  20. data/lib/train/extras/os_detect_windows.rb +73 -0
  21. data/lib/train/extras/stat.rb +92 -0
  22. data/lib/train/extras/windows_file.rb +85 -0
  23. data/lib/train/options.rb +80 -0
  24. data/lib/train/plugins.rb +40 -0
  25. data/lib/train/plugins/base_connection.rb +86 -0
  26. data/lib/train/plugins/transport.rb +49 -0
  27. data/lib/train/transports/docker.rb +102 -0
  28. data/lib/train/transports/local.rb +52 -0
  29. data/lib/train/transports/local_file.rb +77 -0
  30. data/lib/train/transports/local_os.rb +51 -0
  31. data/lib/train/transports/mock.rb +125 -0
  32. data/lib/train/transports/ssh.rb +163 -0
  33. data/lib/train/transports/ssh_connection.rb +216 -0
  34. data/lib/train/transports/winrm.rb +187 -0
  35. data/lib/train/transports/winrm_connection.rb +258 -0
  36. data/lib/train/version.rb +7 -0
  37. data/test/integration/.kitchen.yml +43 -0
  38. data/test/integration/Berksfile +3 -0
  39. data/test/integration/bootstrap.sh +17 -0
  40. data/test/integration/chefignore +1 -0
  41. data/test/integration/cookbooks/test/metadata.rb +1 -0
  42. data/test/integration/cookbooks/test/recipes/default.rb +101 -0
  43. data/test/integration/docker_run.rb +153 -0
  44. data/test/integration/docker_test.rb +24 -0
  45. data/test/integration/docker_test_container.rb +24 -0
  46. data/test/integration/helper.rb +58 -0
  47. data/test/integration/sudo/nopasswd.rb +16 -0
  48. data/test/integration/sudo/passwd.rb +21 -0
  49. data/test/integration/sudo/run_as.rb +12 -0
  50. data/test/integration/test-runner.yaml +24 -0
  51. data/test/integration/test_local.rb +19 -0
  52. data/test/integration/test_ssh.rb +24 -0
  53. data/test/integration/tests/path_block_device_test.rb +74 -0
  54. data/test/integration/tests/path_character_device_test.rb +74 -0
  55. data/test/integration/tests/path_file_test.rb +79 -0
  56. data/test/integration/tests/path_folder_test.rb +88 -0
  57. data/test/integration/tests/path_missing_test.rb +77 -0
  58. data/test/integration/tests/path_pipe_test.rb +78 -0
  59. data/test/integration/tests/path_symlink_test.rb +83 -0
  60. data/test/integration/tests/run_command_test.rb +28 -0
  61. data/test/unit/extras/command_wrapper_test.rb +41 -0
  62. data/test/unit/extras/file_common_test.rb +133 -0
  63. data/test/unit/extras/linux_file_test.rb +98 -0
  64. data/test/unit/extras/os_common_test.rb +258 -0
  65. data/test/unit/extras/stat_test.rb +105 -0
  66. data/test/unit/helper.rb +6 -0
  67. data/test/unit/plugins/connection_test.rb +44 -0
  68. data/test/unit/plugins/transport_test.rb +111 -0
  69. data/test/unit/plugins_test.rb +22 -0
  70. data/test/unit/train_test.rb +132 -0
  71. data/test/unit/transports/local_file_test.rb +112 -0
  72. data/test/unit/transports/local_test.rb +73 -0
  73. data/test/unit/transports/mock_test.rb +76 -0
  74. data/test/unit/transports/ssh_test.rb +95 -0
  75. data/test/unit/version_test.rb +8 -0
  76. data/train.gemspec +32 -0
  77. metadata +299 -0
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+ #
3
+ # author: Dominik Richter
4
+ # author: Christoph Hartmann
5
+
6
+ require 'train/plugins'
7
+ require 'mixlib/shellout'
8
+
9
+ module Train::Transports
10
+ class Local < Train.plugin(1)
11
+ name 'local'
12
+
13
+ include_options Train::Extras::CommandWrapper
14
+
15
+ autoload :File, 'train/transports/local_file'
16
+ autoload :OS, 'train/transports/local_os'
17
+
18
+ def connection(_ = nil)
19
+ @connection ||= Connection.new(@options)
20
+ end
21
+
22
+ class Connection < BaseConnection
23
+ def initialize(options)
24
+ super(options)
25
+ @files = {}
26
+ @cmd_wrapper = nil
27
+ @cmd_wrapper = CommandWrapper.load(self, options)
28
+ end
29
+
30
+ def run_command(cmd)
31
+ cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
32
+ res = Mixlib::ShellOut.new(cmd)
33
+ res.run_command
34
+ CommandResult.new(res.stdout, res.stderr, res.exitstatus)
35
+ rescue Errno::ENOENT => _
36
+ CommandResult.new('', '', 1)
37
+ end
38
+
39
+ def os
40
+ @os ||= OS.new(self)
41
+ end
42
+
43
+ def file(path)
44
+ @files[path] ||= File.new(self, path)
45
+ end
46
+
47
+ def login_command
48
+ nil # none, open your shell
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+ #
3
+ # author: Dominik Richter
4
+ # author: Christoph Hartmann
5
+
6
+ require 'train/extras'
7
+
8
+ class Train::Transports::Local::Connection
9
+ class File < LinuxFile
10
+ def content
11
+ @content ||= ::File.read(@path, encoding: 'UTF-8')
12
+ rescue StandardError => _
13
+ nil
14
+ end
15
+
16
+ %w{
17
+ exist? file? socket? directory? symlink? pipe?
18
+ }.each do |m|
19
+ define_method m.to_sym do
20
+ ::File.method(m.to_sym).call(@path)
21
+ end
22
+ end
23
+
24
+ def link_path
25
+ return nil unless symlink?
26
+ @link_path ||= ::File.readlink(@path)
27
+ end
28
+
29
+ def block_device?
30
+ ::File.blockdev?(@path)
31
+ end
32
+
33
+ def character_device?
34
+ ::File.chardev?(@path)
35
+ end
36
+
37
+ private
38
+
39
+ def pw_username(uid)
40
+ Etc.getpwuid(uid).name
41
+ rescue ArgumentError => _
42
+ nil
43
+ end
44
+
45
+ def pw_groupname(gid)
46
+ Etc.getgrgid(gid).name
47
+ rescue ArgumentError => _
48
+ nil
49
+ end
50
+
51
+ def stat
52
+ return @stat if defined? @stat
53
+
54
+ begin
55
+ file_stat = ::File.lstat(@path)
56
+ rescue StandardError => _err
57
+ return @stat = {}
58
+ end
59
+
60
+ @stat = {
61
+ type: Train::Extras::Stat.find_type(file_stat.mode),
62
+ mode: file_stat.mode & 00777,
63
+ mtime: file_stat.mtime.to_i,
64
+ size: file_stat.size,
65
+ owner: pw_username(file_stat.uid),
66
+ group: pw_groupname(file_stat.gid),
67
+ }
68
+
69
+ res = @backend.run_command("stat #{@spath} 2>/dev/null --printf '%C'")
70
+ if res.exit_status == 0 && !res.stdout.empty? && res.stdout != '?'
71
+ @stat[:selinux_label] = res.stdout.strip
72
+ end
73
+
74
+ @stat
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+ #
5
+ # This is heavily based on:
6
+ #
7
+ # OHAI https://github.com/chef/ohai
8
+ # by Adam Jacob, Chef Software Inc
9
+ #
10
+
11
+ require 'rbconfig'
12
+
13
+ class Train::Transports::Local
14
+ class OS < OSCommon
15
+ def initialize(backend)
16
+ super(backend, { family: detect_local_os })
17
+ end
18
+
19
+ private
20
+
21
+ def detect_local_os
22
+ case ::RbConfig::CONFIG['host_os']
23
+ when /aix(.+)$/
24
+ return 'aix'
25
+ when /darwin(.+)$/
26
+ return 'darwin'
27
+ when /hpux(.+)$/
28
+ return 'hpux'
29
+ when /linux/
30
+ return 'linux'
31
+ when /freebsd(.+)$/
32
+ return 'freebsd'
33
+ when /openbsd(.+)$/
34
+ return 'openbsd'
35
+ when /netbsd(.*)$/
36
+ return 'netbsd'
37
+ when /solaris2/
38
+ return 'solaris2'
39
+ when /mswin|mingw32|windows/
40
+ # After long discussion in IRC the "powers that be" have come to a consensus
41
+ # that no Windows platform exists that was not based on the
42
+ # Windows_NT kernel, so we herby decree that "windows" will refer to all
43
+ # platforms built upon the Windows_NT kernel and have access to win32 or win64
44
+ # subsystems.
45
+ return 'windows'
46
+ else
47
+ return ::RbConfig::CONFIG['host_os']
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,125 @@
1
+ # encoding: utf-8
2
+ #
3
+ # author: Dominik Richter
4
+ # author: Christoph Hartmann
5
+
6
+ require 'train/plugins'
7
+ require 'digest'
8
+
9
+ module Train::Transports
10
+ class Mock < Train.plugin(1)
11
+ name 'mock'
12
+
13
+ def initialize(conf = nil)
14
+ @conf = conf || {}
15
+ trace_calls if @conf[:verbose]
16
+ end
17
+
18
+ def connection
19
+ @connection ||= Connection.new
20
+ end
21
+
22
+ def to_s
23
+ 'Mock Transport'
24
+ end
25
+
26
+ private
27
+
28
+ def trace_calls
29
+ interface_methods = {
30
+ 'Train::Transports::Mock' => Train::Transports::Mock.instance_methods(false),
31
+ 'Train::Transports::Mock::File' => FileCommon.instance_methods(false),
32
+ 'Train::Transports::Mock::OS' => OSCommon.instance_methods(false),
33
+ }
34
+
35
+ # rubocop:disable Metrics/ParameterLists
36
+ # rubocop:disable Lint/Eval
37
+ set_trace_func proc { |event, _file, _line, id, binding, classname|
38
+ unless classname.to_s.start_with?('Train::Transports::Mock') and
39
+ event == 'call' and
40
+ interface_methods[classname.to_s].include?(id)
41
+ next
42
+ end
43
+ # kindly borrowed from the wonderful simple-tracer by matugm
44
+ arg_names = eval(
45
+ 'method(__method__).parameters.map { |arg| arg[1].to_s }',
46
+ binding)
47
+ args = eval("#{arg_names}.map { |arg| eval(arg) }", binding).join(', ')
48
+ prefix = '-' * (classname.to_s.count(':') - 2) + '> '
49
+ puts("#{prefix}#{id} #{args}")
50
+ }
51
+ # rubocop:enable all
52
+ end
53
+ end
54
+ end
55
+
56
+ class Train::Transports::Mock
57
+ class Connection < BaseConnection
58
+ attr_accessor :files, :commands
59
+ attr_reader :os
60
+
61
+ def initialize(conf = nil)
62
+ @conf = conf || {}
63
+ @files = {}
64
+ @os = OS.new(self, family: 'unknown')
65
+ @commands = {}
66
+ trace_calls if @conf[:verbose]
67
+ end
68
+
69
+ def mock_os(value)
70
+ @os = OS.new(self, value)
71
+ end
72
+
73
+ def mock_command(cmd, stdout = nil, stderr = nil, exit_status = 0)
74
+ @commands[cmd] = Command.new(stdout || '', stderr || '', exit_status)
75
+ end
76
+
77
+ def run_command(cmd)
78
+ @commands[cmd] ||
79
+ @commands[Digest::SHA256.hexdigest cmd] ||
80
+ mock_command(cmd)
81
+ end
82
+
83
+ def file(path)
84
+ @files[path] ||= File.new(self, path)
85
+ end
86
+
87
+ def to_s
88
+ 'Mock Connection'
89
+ end
90
+ end
91
+ end
92
+
93
+ class Train::Transports::Mock::Connection
94
+ Command = Struct.new(:stdout, :stderr, :exit_status)
95
+ end
96
+
97
+ class Train::Transports::Mock::Connection
98
+ class OS < OSCommon
99
+ def initialize(backend, desc)
100
+ super(backend, desc)
101
+ end
102
+
103
+ def detect_family
104
+ # no op, we do not need to detect the os
105
+ end
106
+ end
107
+ end
108
+
109
+ class Train::Transports::Mock::Connection
110
+ class File < FileCommon
111
+ %w{
112
+ exist? mode owner group link_target link_path content mtime size
113
+ selinux_label product_version file_version path
114
+ type
115
+ }.each do |m|
116
+ attr_accessor m.tr('?', '').to_sym
117
+ end
118
+
119
+ def initialize(_runtime, path)
120
+ @path = path
121
+ @type = :unknown
122
+ @exist = false
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,163 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ # Author:: Dominik Richter (<dominik.richter@gmail.com>)
5
+ # Author:: Christoph Hartmann (<chris@lollyrock.com>)
6
+ #
7
+ # Copyright (C) 2014, Fletcher Nichol
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+ # you may not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+ #
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+ #
15
+ # Unless required by applicable law or agreed to in writing, software
16
+ # distributed under the License is distributed on an "AS IS" BASIS,
17
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ # See the License for the specific language governing permissions and
19
+ # limitations under the License.
20
+
21
+ require 'net/ssh'
22
+ require 'net/scp'
23
+ require 'train/errors'
24
+
25
+ module Train::Transports
26
+ # Wrapped exception for any internally raised SSH-related errors.
27
+ #
28
+ # @author Fletcher Nichol <fnichol@nichol.ca>
29
+ class SSHFailed < Train::TransportError; end
30
+
31
+ # A Transport which uses the SSH protocol to execute commands and transfer
32
+ # files.
33
+ #
34
+ # @author Fletcher Nichol <fnichol@nichol.ca>
35
+ class SSH < Train.plugin(1)
36
+ name 'ssh'
37
+
38
+ autoload :Connection, 'train/transports/ssh_connection'
39
+
40
+ # add options for submodules
41
+ include_options Train::Extras::CommandWrapper
42
+
43
+ # common target configuration
44
+ option :host, required: true
45
+ option :port, default: 22, required: true
46
+ option :user, default: 'root', required: true
47
+ option :key_files, default: nil
48
+ option :password, default: nil
49
+
50
+ # additional ssh options
51
+ option :keepalive, default: true
52
+ option :keepalive_interval, default: 60
53
+ option :connection_timeout, default: 15
54
+ option :connection_retries, default: 5
55
+ option :connection_retry_sleep, default: 1
56
+ option :max_wait_until_ready, default: 600
57
+ option :compression, default: true
58
+
59
+ option :compression_level do |opts|
60
+ # on nil or false: set compression level to 0
61
+ opts[:compression] ? 6 : 0
62
+ end
63
+
64
+ # (see Base#connection)
65
+ def connection(state = {}, &block)
66
+ opts = merge_options(options, state || {})
67
+ validate_options(opts)
68
+ conn_opts = connection_options(opts)
69
+
70
+ if defined?(@connection) && @connection_options == conn_opts
71
+ reuse_connection(&block)
72
+ else
73
+ create_new_connection(conn_opts, &block)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def validate_options(options)
80
+ super(options)
81
+
82
+ key_files = Array(options[:key_files])
83
+ if key_files.empty? and options[:password].nil?
84
+ fail Train::ClientError,
85
+ 'You must configure at least one authentication method for SSH:'\
86
+ ' Password or key.'
87
+ end
88
+
89
+ options[:auth_methods] ||= ['none']
90
+
91
+ unless key_files.empty?
92
+ options[:auth_methods].push('publickey')
93
+ options[:keys_only] = true if options[:password].nil?
94
+ options[:key_files] = key_files
95
+ end
96
+
97
+ unless options[:password].nil? # rubocop:disable Style/IfUnlessModifier
98
+ options[:auth_methods].push('password')
99
+ end
100
+
101
+ super
102
+ self
103
+ end
104
+
105
+ # Builds the hash of options needed by the Connection object on
106
+ # construction.
107
+ #
108
+ # @param opts [Hash] merged configuration and mutable state data
109
+ # @return [Hash] hash of connection options
110
+ # @api private
111
+ def connection_options(opts)
112
+ {
113
+ logger: logger,
114
+ user_known_hosts_file: '/dev/null',
115
+ paranoid: false,
116
+ hostname: opts[:host],
117
+ port: opts[:port],
118
+ username: opts[:user],
119
+ compression: opts[:compression],
120
+ compression_level: opts[:compression_level],
121
+ keepalive: opts[:keepalive],
122
+ keepalive_interval: opts[:keepalive_interval],
123
+ timeout: opts[:connection_timeout],
124
+ connection_retries: opts[:connection_retries],
125
+ connection_retry_sleep: opts[:connection_retry_sleep],
126
+ max_wait_until_ready: opts[:max_wait_until_ready],
127
+ auth_methods: opts[:auth_methods],
128
+ keys_only: opts[:keys_only],
129
+ keys: opts[:key_files],
130
+ password: opts[:password],
131
+ forward_agent: opts[:forward_agent],
132
+ transport_options: opts,
133
+ }
134
+ end
135
+
136
+ # Creates a new SSH Connection instance and save it for potential future
137
+ # reuse.
138
+ #
139
+ # @param options [Hash] conneciton options
140
+ # @return [Ssh::Connection] an SSH Connection instance
141
+ # @api private
142
+ def create_new_connection(options, &block)
143
+ if defined?(@connection)
144
+ logger.debug("[SSH] shutting previous connection #{@connection}")
145
+ @connection.close
146
+ end
147
+
148
+ @connection_options = options
149
+ conn = Connection.new(options, &block)
150
+ @connection = conn unless conn.nil?
151
+ end
152
+
153
+ # Return the last saved SSH connection instance.
154
+ #
155
+ # @return [Ssh::Connection] an SSH Connection instance
156
+ # @api private
157
+ def reuse_connection
158
+ logger.debug("[SSH] reusing existing connection #{@connection}")
159
+ yield @connection if block_given?
160
+ @connection
161
+ end
162
+ end
163
+ end