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
data/lib/train.rb ADDED
@@ -0,0 +1,100 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Dominik Richter (<dominik.richter@gmail.com>)
4
+
5
+ require 'train/version'
6
+ require 'train/plugins'
7
+ require 'train/errors'
8
+ require 'uri'
9
+
10
+ module Train
11
+ # Create a new transport instance, with the plugin indicated by the
12
+ # given name.
13
+ #
14
+ # @param [String] name of the plugin
15
+ # @param [Array] *args list of arguments for the plugin
16
+ # @return [Transport] instance of the new transport or nil
17
+ def self.create(name, *args)
18
+ cls = load_transport(name)
19
+ cls.new(*args) unless cls.nil?
20
+ end
21
+
22
+ # Retrieve the configuration options of a transport plugin.
23
+ #
24
+ # @param [String] name of the plugin
25
+ # @return [Hash] map of default options
26
+ def self.options(name)
27
+ cls = load_transport(name)
28
+ cls.default_options unless cls.nil?
29
+ end
30
+
31
+ # Load the transport plugin indicated by name. If the plugin is not
32
+ # yet found in the plugin registry, it will be attempted to load from
33
+ # `train/transports/plugin_name`.
34
+ #
35
+ # @param [String] name of the plugin
36
+ # @return [Train::Transport] the transport plugin
37
+ def self.load_transport(name)
38
+ res = Train::Plugins.registry[name.to_s]
39
+ return res unless res.nil?
40
+
41
+ # if the plugin wasnt loaded yet:
42
+ require 'train/transports/' + name.to_s
43
+ Train::Plugins.registry[name.to_s]
44
+ rescue LoadError => _
45
+ raise Train::UserError,
46
+ "Can't find train plugin #{name.inspect}. Please install it first."
47
+ end
48
+
49
+ # Resolve target configuration in URI-scheme into
50
+ # all respective fields and merge with existing configuration.
51
+ # e.g. ssh://bob@remote => backend: ssh, user: bob, host: remote
52
+ def self.target_config(config = nil) # rubocop:disable Metrics/AbcSize
53
+ conf = config.nil? ? {} : config.dup
54
+
55
+ # symbolize keys
56
+ conf = conf.each_with_object({}) do |(k, v), acc|
57
+ acc[k.to_sym] = v
58
+ acc
59
+ end
60
+
61
+ group_keys_and_keyfiles(conf)
62
+
63
+ return conf if conf[:target].to_s.empty?
64
+
65
+ uri = URI.parse(conf[:target].to_s)
66
+ unless uri.host.nil? and uri.scheme.nil?
67
+ conf[:backend] ||= uri.scheme
68
+ conf[:host] ||= uri.host
69
+ conf[:port] ||= uri.port
70
+ conf[:user] ||= uri.user
71
+ conf[:password] ||= uri.password
72
+ conf[:path] ||= uri.path
73
+ end
74
+
75
+ # ensure path is nil, if its empty; e.g. required to reset defaults for winrm
76
+ conf[:path] = nil if !conf[:path].nil? && conf[:path].to_s.empty?
77
+
78
+ # return the updated config
79
+ conf
80
+ end
81
+
82
+ private
83
+
84
+ def self.group_keys_and_keyfiles(conf)
85
+ # in case the user specified a key-file, register it that way
86
+ # we will clear the list of keys and put keys and key_files separately
87
+ keys_mixed = conf[:keys]
88
+ return if keys_mixed.nil?
89
+
90
+ conf[:key_files] = []
91
+ conf[:keys] = []
92
+ keys_mixed.each do |key|
93
+ if !key.nil? and File.file?(key)
94
+ conf[:key_files].push(key)
95
+ else
96
+ conf[:keys].push(key)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,23 @@
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) 2013, Fletcher Nichol
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+
11
+ module Train
12
+ # Base exception class for all exceptions that are caused by user input
13
+ # errors.
14
+ class UserError < ::StandardError; end
15
+
16
+ # Base exception class for all exceptions that are caused by incorrect use
17
+ # of an API.
18
+ class ClientError < ::StandardError; end
19
+
20
+ # Base exception class for all exceptions that are caused by other failures
21
+ # in the transport layer.
22
+ class TransportError < ::StandardError; end
23
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Dominik Richter (<dominik.richter@gmail.com>)
4
+
5
+ module Train::Extras
6
+ autoload :CommandWrapper, 'train/extras/command_wrapper'
7
+ autoload :FileCommon, 'train/extras/file_common'
8
+ autoload :LinuxFile, 'train/extras/linux_file'
9
+ autoload :WindowsFile, 'train/extras/windows_file'
10
+ autoload :OSCommon, 'train/extras/os_common'
11
+ autoload :Stat, 'train/extras/stat'
12
+
13
+ CommandResult = Struct.new(:stdout, :stderr, :exit_status)
14
+ LoginCommand = Struct.new(:command, :arguments)
15
+ end
@@ -0,0 +1,105 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'base64'
6
+ require 'train/errors'
7
+
8
+ module Train::Extras
9
+ # Define the interface of all command wrappers.
10
+ class CommandWrapperBase
11
+ # Verify that the command wrapper is initialized properly and working.
12
+ #
13
+ # @return [Any] verification result, nil if all went well, otherwise a message
14
+ def verify
15
+ fail Train::ClientError, "#{self.class} does not implement #verify()"
16
+ end
17
+
18
+ # Wrap a command and return the augmented command which can be executed.
19
+ #
20
+ # @param [Strin] command that will be wrapper
21
+ # @return [String] result of wrapping the command
22
+ def run(_command)
23
+ fail Train::ClientError, "#{self.class} does not implement #run(command)"
24
+ end
25
+ end
26
+
27
+ # Wrap linux commands and add functionality like sudo.
28
+ class LinuxCommand < CommandWrapperBase
29
+ Train::Options.attach(self)
30
+
31
+ option :sudo, default: false
32
+ option :sudo_options, default: nil
33
+ option :sudo_password, default: nil
34
+ option :user
35
+
36
+ def initialize(backend, options)
37
+ @backend = backend
38
+ validate_options(options)
39
+
40
+ @sudo = options[:sudo]
41
+ @sudo_options = options[:sudo_options]
42
+ @sudo_password = options[:sudo_password]
43
+ @user = options[:user]
44
+ @prefix = build_prefix
45
+ end
46
+
47
+ # (see CommandWrapperBase::verify)
48
+ def verify
49
+ res = @backend.run_command(run('echo'))
50
+ return nil if res.exit_status == 0
51
+ rawerr = res.stdout + ' ' + res.stderr
52
+
53
+ rawerr = 'Wrong sudo password.' if rawerr.include? 'Sorry, try again'
54
+ if rawerr.include? 'sudo: no tty present and no askpass program specified'
55
+ rawerr = 'Sudo requires a password, please configure it.'
56
+ end
57
+ if rawerr.include? 'sudo: command not found'
58
+ rawerr = "Can't find sudo command. Please either install and "\
59
+ 'configure it on the target or deactivate sudo.'
60
+ end
61
+
62
+ rawerr
63
+ end
64
+
65
+ # (see CommandWrapperBase::run)
66
+ def run(command)
67
+ @prefix + command
68
+ end
69
+
70
+ def self.active?(options)
71
+ options.is_a?(Hash) && options[:sudo]
72
+ end
73
+
74
+ private
75
+
76
+ def build_prefix
77
+ return '' unless @sudo
78
+ return '' if @user == 'root'
79
+
80
+ res = 'sudo '
81
+
82
+ unless @sudo_password.nil?
83
+ b64pw = Base64.strict_encode64(@sudo_password + "\n")
84
+ res = "echo #{b64pw} | base64 -d | sudo -S "
85
+ end
86
+
87
+ res << @sudo_options.to_s + ' ' unless @sudo_options.nil?
88
+
89
+ res
90
+ end
91
+ end
92
+
93
+ class CommandWrapper
94
+ include_options LinuxCommand
95
+
96
+ def self.load(transport, options)
97
+ return nil unless LinuxCommand.active?(options)
98
+ return nil unless transport.os.unix?
99
+ res = LinuxCommand.new(transport, options)
100
+ msg = res.verify
101
+ fail Train::UserError, "Sudo failed: #{msg}" unless msg.nil?
102
+ res
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,131 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'digest/sha2'
6
+ require 'digest/md5'
7
+
8
+ module Train::Extras
9
+ class FileCommon
10
+ # interface methods: these fields should be implemented by every
11
+ # backend File
12
+ %w{
13
+ exist? mode owner group link_target link_path content mtime size
14
+ selinux_label product_version file_version path
15
+ }.each do |m|
16
+ define_method m.to_sym do
17
+ fail NotImplementedError, "File must implement the #{m}() method."
18
+ end
19
+ end
20
+
21
+ def type
22
+ :unknown
23
+ end
24
+
25
+ # The following methods can be overwritten by a derived class
26
+ # if desired, to e.g. achieve optimizations.
27
+
28
+ def md5sum
29
+ res = Digest::MD5.new
30
+ res.update(content)
31
+ res.hexdigest
32
+ rescue TypeError => _
33
+ nil
34
+ end
35
+
36
+ def sha256sum
37
+ res = Digest::SHA256.new
38
+ res.update(content)
39
+ res.hexdigest
40
+ rescue TypeError => _
41
+ nil
42
+ end
43
+
44
+ # Additional methods for convenience
45
+
46
+ def file?
47
+ target_type == :file
48
+ end
49
+
50
+ def block_device?
51
+ target_type == :block_device
52
+ end
53
+
54
+ def character_device?
55
+ target_type == :character_device
56
+ end
57
+
58
+ def socket?
59
+ target_type == :socket
60
+ end
61
+
62
+ def directory?
63
+ target_type == :directory
64
+ end
65
+
66
+ def symlink?
67
+ type == :symlink
68
+ end
69
+
70
+ def pipe?
71
+ target_type == :pipe
72
+ end
73
+
74
+ def mode?(sth)
75
+ mode == sth
76
+ end
77
+
78
+ def owned_by?(sth)
79
+ owner == sth
80
+ end
81
+
82
+ def grouped_into?(sth)
83
+ group == sth
84
+ end
85
+
86
+ def linked_to?(dst)
87
+ link_path == dst
88
+ end
89
+
90
+ def version?(version)
91
+ product_version == version or
92
+ file_version == version
93
+ end
94
+
95
+ def unix_mode_mask(owner, type)
96
+ o = UNIX_MODE_OWNERS[owner.to_sym]
97
+ return nil if o.nil?
98
+
99
+ t = UNIX_MODE_TYPES[type.to_sym]
100
+ return nil if t.nil?
101
+
102
+ t & o
103
+ end
104
+
105
+ # helper methods provided to any implementing class
106
+
107
+ private
108
+
109
+ def target_type
110
+ # Just return the type unless this is a symlink
111
+ return type unless type == :symlink
112
+ # Get the link's target type, i.e. the real destination's type
113
+ return link_target.type unless link_target.nil?
114
+ # Return unknown if we don't know where this is pointing to
115
+ :unknown
116
+ end
117
+
118
+ UNIX_MODE_OWNERS = {
119
+ all: 00777,
120
+ owner: 00700,
121
+ group: 00070,
122
+ other: 00007,
123
+ }
124
+
125
+ UNIX_MODE_TYPES = {
126
+ r: 00444,
127
+ w: 00222,
128
+ x: 00111,
129
+ }
130
+ end
131
+ end
@@ -0,0 +1,74 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'shellwords'
6
+ require 'train/extras/stat'
7
+
8
+ module Train::Extras
9
+ class LinuxFile < FileCommon
10
+ attr_reader :path
11
+ def initialize(backend, path)
12
+ @backend = backend
13
+ @path = path
14
+ @spath = Shellwords.escape(@path)
15
+ end
16
+
17
+ def content
18
+ return @content if defined?(@content)
19
+ @content = @backend.run_command(
20
+ "cat #{@spath} || echo -n").stdout
21
+ return @content unless @content.empty?
22
+ @content = nil if directory? or size.nil? or size > 0
23
+ @content
24
+ end
25
+
26
+ def exist?
27
+ @exist ||= (
28
+ @backend.run_command("test -e #{@spath}")
29
+ .exit_status == 0
30
+ )
31
+ end
32
+
33
+ def link_target
34
+ return @link_target if defined? @link_target
35
+ return @link_target = nil if link_path.nil?
36
+ @link_target = @backend.file(link_path)
37
+ end
38
+
39
+ def link_path
40
+ return nil unless symlink?
41
+ @link_path ||= (
42
+ @backend.run_command("readlink #{@spath}").stdout.chomp
43
+ )
44
+ end
45
+
46
+ def mounted?
47
+ @mounted ||= (
48
+ !@backend.run_command("mount | grep -- ' on #{@spath}'")
49
+ .stdout.empty?
50
+ )
51
+ end
52
+
53
+ %w{
54
+ type mode owner group mtime size selinux_label
55
+ }.each do |field|
56
+ define_method field.to_sym do
57
+ stat[field.to_sym]
58
+ end
59
+ end
60
+
61
+ def product_version
62
+ nil
63
+ end
64
+
65
+ def file_version
66
+ nil
67
+ end
68
+
69
+ def stat
70
+ return @stat if defined?(@stat)
71
+ @stat = Train::Extras::Stat.stat(@spath, @backend)
72
+ end
73
+ end
74
+ end