train-core 1.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +780 -0
  3. data/Gemfile +36 -0
  4. data/LICENSE +201 -0
  5. data/README.md +197 -0
  6. data/lib/train.rb +158 -0
  7. data/lib/train/errors.rb +32 -0
  8. data/lib/train/extras.rb +11 -0
  9. data/lib/train/extras/command_wrapper.rb +137 -0
  10. data/lib/train/extras/stat.rb +132 -0
  11. data/lib/train/file.rb +151 -0
  12. data/lib/train/file/local.rb +75 -0
  13. data/lib/train/file/local/unix.rb +96 -0
  14. data/lib/train/file/local/windows.rb +63 -0
  15. data/lib/train/file/remote.rb +36 -0
  16. data/lib/train/file/remote/aix.rb +21 -0
  17. data/lib/train/file/remote/linux.rb +19 -0
  18. data/lib/train/file/remote/qnx.rb +41 -0
  19. data/lib/train/file/remote/unix.rb +106 -0
  20. data/lib/train/file/remote/windows.rb +94 -0
  21. data/lib/train/options.rb +80 -0
  22. data/lib/train/platforms.rb +84 -0
  23. data/lib/train/platforms/common.rb +34 -0
  24. data/lib/train/platforms/detect.rb +12 -0
  25. data/lib/train/platforms/detect/helpers/os_common.rb +145 -0
  26. data/lib/train/platforms/detect/helpers/os_linux.rb +75 -0
  27. data/lib/train/platforms/detect/helpers/os_windows.rb +120 -0
  28. data/lib/train/platforms/detect/scanner.rb +84 -0
  29. data/lib/train/platforms/detect/specifications/api.rb +15 -0
  30. data/lib/train/platforms/detect/specifications/os.rb +578 -0
  31. data/lib/train/platforms/detect/uuid.rb +34 -0
  32. data/lib/train/platforms/family.rb +26 -0
  33. data/lib/train/platforms/platform.rb +101 -0
  34. data/lib/train/plugins.rb +40 -0
  35. data/lib/train/plugins/base_connection.rb +169 -0
  36. data/lib/train/plugins/transport.rb +49 -0
  37. data/lib/train/transports/local.rb +232 -0
  38. data/lib/train/version.rb +7 -0
  39. data/train-core.gemspec +27 -0
  40. metadata +116 -0
@@ -0,0 +1,94 @@
1
+ # encoding: utf-8
2
+
3
+ module Train
4
+ class File
5
+ class Remote
6
+ class Windows < Train::File::Remote
7
+ attr_reader :path
8
+ # Ensures we do not use invalid characters for file names
9
+ # @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions
10
+ def sanitize_filename(path)
11
+ return if path.nil?
12
+ # we do not filter :, backslash and forward slash, since they are part of the path
13
+ @spath = path.gsub(/[<>"|?*]/, '')
14
+ end
15
+
16
+ def basename(suffix = nil, sep = '\\')
17
+ super(suffix, sep)
18
+ end
19
+
20
+ def content
21
+ return @content if defined?(@content)
22
+ @content = @backend.run_command("Get-Content(\"#{@spath}\") | Out-String").stdout
23
+ return @content unless @content.empty?
24
+ @content = nil if directory? # or size.nil? or size > 0
25
+ @content
26
+ end
27
+
28
+ def exist?
29
+ return @exist if defined?(@exist)
30
+ @exist = @backend.run_command(
31
+ "(Test-Path -Path \"#{@spath}\").ToString()").stdout.chomp == 'True'
32
+ end
33
+
34
+ def owner
35
+ owner = @backend.run_command(
36
+ "Get-Acl \"#{@spath}\" | select -expand Owner").stdout.strip
37
+ return if owner.empty?
38
+ owner
39
+ end
40
+
41
+ def type
42
+ if attributes.include?('Archive') && !attributes.include?('Directory')
43
+ return :file
44
+ elsif attributes.include?('ReparsePoint')
45
+ return :symlink
46
+ elsif attributes.include?('Directory')
47
+ return :directory
48
+ end
49
+ :unknown
50
+ end
51
+
52
+ def size
53
+ if file?
54
+ @backend.run_command("((Get-Item '#{@spath}').Length)").stdout.strip.to_i
55
+ end
56
+ end
57
+
58
+ def product_version
59
+ @product_version ||= @backend.run_command(
60
+ "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").ProductVersion").stdout.chomp
61
+ end
62
+
63
+ def file_version
64
+ @file_version ||= @backend.run_command(
65
+ "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").FileVersion").stdout.chomp
66
+ end
67
+
68
+ %w{
69
+ mode group uid gid mtime selinux_label
70
+ }.each do |field|
71
+ define_method field.to_sym do
72
+ nil
73
+ end
74
+ end
75
+
76
+ def link_path
77
+ nil
78
+ end
79
+
80
+ def mounted
81
+ nil
82
+ end
83
+
84
+ private
85
+
86
+ def attributes
87
+ return @attributes if defined?(@attributes)
88
+ @attributes = @backend.run_command(
89
+ "(Get-ItemProperty -Path \"#{@spath}\").attributes.ToString()").stdout.chomp.split(/\s*,\s*/)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,80 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Dominik Richter (<dominik.richter@gmail.com>)
4
+ # Author:: Christoph Hartmann (<chris@lollyrock.com>)
5
+
6
+ module Train
7
+ module Options
8
+ def self.attach(target)
9
+ target.class.method(:include).call(ClassOptions)
10
+ target.method(:include).call(InstanceOptions)
11
+ end
12
+
13
+ module ClassOptions
14
+ def option(name, conf = nil, &block)
15
+ d = conf || {}
16
+ unless d.is_a? Hash
17
+ fail Train::ClientError,
18
+ "The transport plugin #{self} declared an option #{name} "\
19
+ "and didn't provide a valid configuration hash."
20
+ end
21
+
22
+ if !conf.nil? and !conf[:default].nil? and block_given?
23
+ fail Train::ClientError,
24
+ "The transport plugin #{self} declared an option #{name} "\
25
+ 'with both a default value and block. Only use one of these.'
26
+ end
27
+
28
+ d[:default] = block if block_given?
29
+
30
+ default_options[name] = d
31
+ end
32
+
33
+ def default_options
34
+ @default_options = {} unless defined? @default_options
35
+ @default_options
36
+ end
37
+
38
+ def include_options(other)
39
+ unless other.respond_to?(:default_options)
40
+ fail "Trying to include options from module #{other.inspect}, "\
41
+ "which doesn't seem to support options."
42
+ end
43
+ default_options.merge!(other.default_options)
44
+ end
45
+ end
46
+
47
+ module InstanceOptions
48
+ # @return [Hash] options, which created this Transport
49
+ attr_reader :options
50
+
51
+ def default_options
52
+ self.class.default_options
53
+ end
54
+
55
+ def merge_options(base, opts)
56
+ res = base.merge(opts || {})
57
+ default_options.each do |field, hm|
58
+ next unless res[field].nil? and hm.key?(:default)
59
+ default = hm[:default]
60
+ if default.is_a? Proc
61
+ res[field] = default.call(res)
62
+ else
63
+ res[field] = default
64
+ end
65
+ end
66
+ res
67
+ end
68
+
69
+ def validate_options(opts)
70
+ default_options.each do |field, hm|
71
+ if opts[field].nil? and hm[:required]
72
+ fail Train::ClientError,
73
+ "You must provide a value for #{field.to_s.inspect}."
74
+ end
75
+ end
76
+ opts
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,84 @@
1
+ # encoding: utf-8
2
+
3
+ require 'train/platforms/common'
4
+ require 'train/platforms/detect'
5
+ require 'train/platforms/detect/scanner'
6
+ require 'train/platforms/detect/specifications/os'
7
+ require 'train/platforms/detect/specifications/api'
8
+ require 'train/platforms/detect/uuid'
9
+ require 'train/platforms/family'
10
+ require 'train/platforms/platform'
11
+
12
+ module Train::Platforms
13
+ class << self
14
+ # Retrieve the current platform list
15
+ #
16
+ # @return [Hash] map with platform names and their objects
17
+ def list
18
+ @list ||= {}
19
+ end
20
+
21
+ # Retrieve the current family list
22
+ #
23
+ # @return [Hash] map with family names and their objects
24
+ def families
25
+ @families ||= {}
26
+ end
27
+ end
28
+
29
+ # Create or update a platform
30
+ #
31
+ # @return Train::Platform
32
+ def self.name(name, condition = {})
33
+ # Check the list to see if one is already created
34
+ plat = list[name]
35
+ unless plat.nil?
36
+ # Pass the condition incase we are adding a family relationship
37
+ plat.condition = condition unless condition.nil?
38
+ return plat
39
+ end
40
+
41
+ Train::Platforms::Platform.new(name, condition)
42
+ end
43
+
44
+ # Create or update a family
45
+ #
46
+ # @return Train::Platforms::Family
47
+ def self.family(name, condition = {})
48
+ # Check the families to see if one is already created
49
+ family = families[name]
50
+ unless family.nil?
51
+ # Pass the condition incase we are adding a family relationship
52
+ family.condition = condition unless condition.nil?
53
+ return family
54
+ end
55
+
56
+ Train::Platforms::Family.new(name, condition)
57
+ end
58
+
59
+ # Find the families or top level platforms
60
+ #
61
+ # @return [Hash] with top level family and platforms
62
+ def self.top_platforms
63
+ top_platforms = list.select { |_key, value| value.families.empty? }
64
+ top_platforms.merge!(families.select { |_key, value| value.families.empty? })
65
+ top_platforms
66
+ end
67
+
68
+ # List all platforms and families in a readable output
69
+ def self.list_all
70
+ top_platforms = self.top_platforms
71
+ top_platforms.each_value do |platform|
72
+ puts platform.title
73
+ print_children(platform) if defined?(platform.children)
74
+ end
75
+ end
76
+
77
+ def self.print_children(parent, pad = 2)
78
+ parent.children.each do |key, value|
79
+ obj = key
80
+ puts "#{' ' * pad}-> #{obj.title}#{value unless value.empty?}"
81
+ print_children(obj, pad + 2) if defined?(obj.children) && !obj.children.nil?
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+
3
+ module Train::Platforms
4
+ module Common
5
+ # Add a family connection. This will create a family
6
+ # if it does not exist and add a child relationship.
7
+ def in_family(family)
8
+ if self.class == Train::Platforms::Family && @name == family
9
+ fail "Unable to add family #{@name} to itself: '#{@name}.in_family(#{family})'"
10
+ end
11
+
12
+ # add family to the family list
13
+ family = Train::Platforms.family(family)
14
+ family.children[self] = @condition
15
+
16
+ @families[family] = @condition
17
+ @condition = nil
18
+ self
19
+ end
20
+
21
+ def detect(&block)
22
+ if block_given?
23
+ @detect = block
24
+ self
25
+ elsif @detect.nil?
26
+ # we are returning a block that just returns false here
27
+ # to skip the family/platform evaluation if detect is not set
28
+ ->(_) { false }
29
+ else
30
+ @detect
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ module Train::Platforms
4
+ module Detect
5
+ # Main detect method to scan all platforms for a match
6
+ #
7
+ # @return Train::Platform instance or error if none found
8
+ def self.scan(backend)
9
+ Scanner.new(backend).scan
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,145 @@
1
+ # encoding: utf-8
2
+
3
+ require 'train/platforms/detect/helpers/os_linux'
4
+ require 'train/platforms/detect/helpers/os_windows'
5
+ require 'rbconfig'
6
+
7
+ module Train::Platforms::Detect::Helpers
8
+ module OSCommon # rubocop:disable Metrics/ModuleLength
9
+ include Train::Platforms::Detect::Helpers::Linux
10
+ include Train::Platforms::Detect::Helpers::Windows
11
+
12
+ def ruby_host_os(regex)
13
+ ::RbConfig::CONFIG['host_os'] =~ regex
14
+ end
15
+
16
+ def winrm?
17
+ Object.const_defined?('Train::Transports::WinRM::Connection') &&
18
+ @backend.class == Train::Transports::WinRM::Connection
19
+ end
20
+
21
+ def unix_file_contents(path)
22
+ # keep a log of files incase multiple checks call the same one
23
+ return @files[path] if @files.key?(path)
24
+
25
+ res = @backend.run_command("test -f #{path} && cat #{path}")
26
+ # ignore files that can't be read
27
+ @files[path] = res.exit_status.zero? ? res.stdout : nil
28
+ @files[path]
29
+ end
30
+
31
+ def unix_file_exist?(path)
32
+ @backend.run_command("test -f #{path}").exit_status.zero?
33
+ end
34
+
35
+ def command_output(cmd)
36
+ res = @backend.run_command(cmd).stdout
37
+ res.strip! unless res.nil?
38
+ res
39
+ end
40
+
41
+ def unix_uname_s
42
+ return @uname[:s] if @uname.key?(:s)
43
+ @uname[:s] = command_output('uname -s')
44
+ end
45
+
46
+ def unix_uname_r
47
+ return @uname[:r] if @uname.key?(:r)
48
+ @uname[:r] = command_output('uname -r')
49
+ end
50
+
51
+ def unix_uname_m
52
+ return @uname[:m] if @uname.key?(:m)
53
+ @uname[:m] = command_output('uname -m')
54
+ end
55
+
56
+ def brocade_version
57
+ return @cache[:brocade] if @cache.key?(:brocade)
58
+ res = command_output('version')
59
+
60
+ m = res.match(/^Fabric OS:\s+v(\S+)$/)
61
+ unless m.nil?
62
+ return @cache[:brocade] = { version: m[1], type: 'fos' }
63
+ end
64
+
65
+ @cache[:brocade] = nil
66
+ end
67
+
68
+ def cisco_show_version
69
+ return @cache[:cisco] if @cache.key?(:cisco)
70
+ res = command_output('show version')
71
+
72
+ m = res.match(/^Cisco IOS Software, [^,]+? \(([^,]+?)\), Version (\d+\.\d+)/)
73
+ unless m.nil?
74
+ return @cache[:cisco] = { version: m[2], model: m[1], type: 'ios' }
75
+ end
76
+
77
+ m = res.match(/^Cisco IOS Software, IOS-XE Software, [^,]+? \(([^,]+?)\), Version (\d+\.\d+\.\d+[A-Z]*)/)
78
+ unless m.nil?
79
+ return @cache[:cisco] = { version: m[2], model: m[1], type: 'ios-xe' }
80
+ end
81
+
82
+ m = res.match(/^Cisco Nexus Operating System \(NX-OS\) Software/)
83
+ unless m.nil?
84
+ v = res[/^\s*system:\s+version (\d+\.\d+)/, 1]
85
+ return @cache[:cisco] = { version: v, type: 'nexus' }
86
+ end
87
+
88
+ @cache[:cisco] = nil
89
+ end
90
+
91
+ def unix_uuid
92
+ uuid = unix_uuid_from_chef
93
+ uuid = unix_uuid_from_machine_file if uuid.nil?
94
+ uuid = uuid_from_command if uuid.nil?
95
+ raise Train::TransportError, 'Cannot find a UUID for your node.' if uuid.nil?
96
+ uuid
97
+ end
98
+
99
+ def unix_uuid_from_chef
100
+ file = @backend.file('/var/chef/cache/data_collector_metadata.json')
101
+ if file.exist? && !file.size.zero?
102
+ json = ::JSON.parse(file.content)
103
+ return json['node_uuid'] if json['node_uuid']
104
+ end
105
+ end
106
+
107
+ def unix_uuid_from_machine_file
108
+ %W(
109
+ /etc/chef/chef_guid
110
+ #{ENV['HOME']}/.chef/chef_guid
111
+ /etc/machine-id
112
+ /var/lib/dbus/machine-id
113
+ /var/db/dbus/machine-id
114
+ ).each do |path|
115
+ file = @backend.file(path)
116
+ next unless file.exist? && !file.size.zero?
117
+ return file.content.chomp if path =~ /guid/
118
+ return uuid_from_string(file.content.chomp)
119
+ end
120
+ nil
121
+ end
122
+
123
+ # This takes a command from the platform detect block to run.
124
+ # We expect the command to return a unique identifier which
125
+ # we turn into a UUID.
126
+ def uuid_from_command
127
+ return unless @platform[:uuid_command]
128
+ result = @backend.run_command(@platform[:uuid_command])
129
+ uuid_from_string(result.stdout.chomp) if result.exit_status.zero? && !result.stdout.empty?
130
+ end
131
+
132
+ # This hashes the passed string into SHA1.
133
+ # Then it downgrades the 160bit SHA1 to a 128bit
134
+ # then we format it as a valid UUIDv5.
135
+ def uuid_from_string(string)
136
+ hash = Digest::SHA1.new
137
+ hash.update(string)
138
+ ary = hash.digest.unpack('NnnnnN')
139
+ ary[2] = (ary[2] & 0x0FFF) | (5 << 12)
140
+ ary[3] = (ary[3] & 0x3FFF) | 0x8000
141
+ # rubocop:disable Style/FormatString
142
+ '%08x-%04x-%04x-%04x-%04x%08x' % ary
143
+ end
144
+ end
145
+ end