r-train 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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