machinery-tool 1.16.4 → 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.git_revision +1 -1
  3. data/NEWS +10 -0
  4. data/filters/default_filters.json +21 -20
  5. data/html/assets/machinery-base.js +4 -0
  6. data/html/index.html.haml +1 -1
  7. data/html/partials/changed_managed_files.html.haml +2 -2
  8. data/html/partials/compare/changed_managed_file_list.html.haml +2 -2
  9. data/html/partials/compare/changed_managed_files.html.haml +3 -3
  10. data/html/partials/compare/config_file_list.html.haml +2 -2
  11. data/html/partials/compare/config_files.html.haml +3 -3
  12. data/html/partials/compare/packages.html.haml +21 -4
  13. data/html/partials/compare/repositories.html.haml +10 -7
  14. data/html/partials/compare/repository_list_apt.html.haml +15 -0
  15. data/html/partials/compare/repository_list_yum.html.haml +35 -0
  16. data/html/partials/compare/{repository_list.html.haml → repository_list_zypp.html.haml} +0 -0
  17. data/html/partials/compare/service_list.html.haml +1 -1
  18. data/html/partials/compare/services.html.haml +3 -3
  19. data/html/partials/compare/unmanaged_file_list.html.haml +2 -2
  20. data/html/partials/compare/unmanaged_files.html.haml +1 -1
  21. data/html/partials/config_files.html.haml +39 -41
  22. data/html/partials/repositories.html.haml +1 -23
  23. data/html/partials/repositories_apt.html.haml +15 -0
  24. data/html/partials/repositories_yum.html.haml +30 -0
  25. data/html/partials/repositories_zypp.html.haml +24 -0
  26. data/html/partials/services.html.haml +2 -2
  27. data/html/partials/unmanaged_files.html.haml +2 -2
  28. data/inspect_helpers/dpkg_unmanaged_files.sh +47 -0
  29. data/inspect_helpers/yum_repositories.py +3 -5
  30. data/lib/analyze_config_file_diffs_task.rb +11 -1
  31. data/lib/array.rb +97 -35
  32. data/lib/autoyast.rb +13 -2
  33. data/lib/cli.rb +10 -2
  34. data/lib/config.rb +4 -4
  35. data/lib/dpkg_database.rb +68 -0
  36. data/lib/element_filter.rb +2 -0
  37. data/lib/file_diff.rb +2 -2
  38. data/lib/file_scope.rb +10 -49
  39. data/lib/file_validator.rb +10 -4
  40. data/lib/filter.rb +6 -6
  41. data/lib/filter_option_parser.rb +1 -1
  42. data/lib/kiwi_config.rb +13 -10
  43. data/lib/machinery.rb +2 -0
  44. data/lib/machinery_helper.rb +1 -1
  45. data/lib/managed_files_database.rb +200 -0
  46. data/lib/object.rb +5 -3
  47. data/lib/remote_system.rb +43 -10
  48. data/lib/renderer.rb +3 -4
  49. data/lib/rpm_database.rb +7 -183
  50. data/lib/scope_file_access_archive.rb +3 -3
  51. data/lib/scope_file_access_flat.rb +1 -1
  52. data/lib/server.rb +7 -2
  53. data/lib/system.rb +50 -22
  54. data/lib/system_description.rb +3 -3
  55. data/lib/version.rb +1 -1
  56. data/lib/workload_mapper.rb +2 -2
  57. data/machinery-helper/machinery_helper.go +252 -178
  58. data/machinery-helper/machinery_helper_test.go +121 -121
  59. data/machinery-helper/mountpoints.go +28 -28
  60. data/machinery-helper/tar.go +105 -104
  61. data/machinery-helper/version.go +1 -1
  62. data/man/generated/machinery.1.gz +0 -0
  63. data/man/generated/machinery.1.html +19 -8
  64. data/plugins/changed_managed_files/changed_managed_files_inspector.rb +3 -3
  65. data/plugins/changed_managed_files/changed_managed_files_model.rb +3 -1
  66. data/plugins/changed_managed_files/changed_managed_files_renderer.rb +2 -2
  67. data/plugins/changed_managed_files/schema/system-description-changed-managed-files.schema-v6.json +168 -0
  68. data/plugins/config_files/config_files_inspector.rb +4 -4
  69. data/plugins/config_files/config_files_model.rb +3 -1
  70. data/plugins/config_files/config_files_renderer.rb +2 -2
  71. data/plugins/config_files/schema/system-description-config-files.schema-v6.json +160 -0
  72. data/plugins/environment/schema/system-description-environment.schema-v6.json +17 -0
  73. data/plugins/groups/schema/system-description-groups.schema-v6.json +49 -0
  74. data/plugins/os/schema/system-description-os.schema-v6.json +21 -0
  75. data/plugins/packages/packages_inspector.rb +76 -6
  76. data/plugins/packages/packages_model.rb +31 -12
  77. data/plugins/packages/packages_renderer.rb +5 -2
  78. data/plugins/packages/schema/system-description-packages.schema-v6.json +115 -0
  79. data/plugins/patterns/patterns_inspector.rb +26 -2
  80. data/plugins/patterns/schema/system-description-patterns.schema-v6.json +58 -0
  81. data/plugins/repositories/repositories_inspector.rb +41 -14
  82. data/plugins/repositories/repositories_model.rb +55 -12
  83. data/plugins/repositories/repositories_renderer.rb +23 -7
  84. data/plugins/repositories/schema/system-description-repositories.schema-v6.json +165 -0
  85. data/plugins/services/schema/system-description-services.schema-v6.json +93 -0
  86. data/plugins/services/services_inspector.rb +88 -22
  87. data/plugins/services/services_model.rb +9 -15
  88. data/plugins/services/services_renderer.rb +2 -2
  89. data/plugins/unmanaged_files/schema/system-description-unmanaged-files.schema-v6.json +162 -0
  90. data/plugins/unmanaged_files/unmanaged_files_inspector.rb +80 -30
  91. data/plugins/unmanaged_files/unmanaged_files_model.rb +22 -18
  92. data/plugins/unmanaged_files/unmanaged_files_renderer.rb +3 -3
  93. data/plugins/users/schema/system-description-users.schema-v6.json +86 -0
  94. data/schema/migrations/migrate5to6.rb +101 -0
  95. data/schema/system-description-global.schema-v6.json +43 -0
  96. metadata +24 -4
  97. data/html/assets/landing_page/landing_page.js +0 -10
data/lib/cli.rb CHANGED
@@ -150,7 +150,8 @@ class Cli
150
150
  "Error: System description name '#{name}' is invalid. By default Machinery" \
151
151
  " uses the image name as description name if the parameter `--name` is not" \
152
152
  " provided.\nIf the image name contains a slash the `--name=NAME` parameter" \
153
- " is mandatory. Valid characters are 'a-zA-Z0-9_:.-'."
153
+ " is mandatory. Valid characters are 'a-zA-Z0-9_:.-'.\n\nFor example run:\n" \
154
+ "#{Hint.program_name} #{ARGV.join(" ")} --name='#{image.tr("/", "_")}'"
154
155
  )
155
156
  end
156
157
  end
@@ -584,10 +585,17 @@ class Cli
584
585
  c.flag ["remote-user", :r], type: String, required: false, default_value: @config.remote_user,
585
586
  desc: "Defines the user which is used to access the inspected system via SSH."\
586
587
  "This user needs sudo access on the remote machine or be root.", arg_name: "USER"
588
+ c.flag ["ssh-port", :p], type: Integer, required: false,
589
+ desc: "The SSH port of the remote server.",
590
+ arg_name: "SSH-PORT"
591
+ c.flag ["ssh-identity-file", :i], type: String, required: false,
592
+ desc: "The private SSH key location what can be used to authenticate with the remote system.",
593
+ arg_name: "SSH-KEY"
587
594
 
588
595
  c.action do |_global_options, options, args|
589
596
  host = shift_arg(args, "HOSTNAME")
590
- system = System.for(host, options["remote-user"])
597
+ system = System.for(host, remote_user: options["remote-user"], ssh_port: options["ssh-port"],
598
+ ssh_identity_file: options["ssh-identity-file"])
591
599
  inspector_task = InspectTask.new
592
600
 
593
601
  name, scope_list, inspect_options, filter = parse_inspect_command_options(host, options)
data/lib/config.rb CHANGED
@@ -30,20 +30,20 @@ module Machinery
30
30
  default: true,
31
31
  description: "Show hints about usage of Machinery in the context of the commands ran by" \
32
32
  " the user"
33
- )
33
+ )
34
34
  entry("remote-user",
35
35
  default: "root",
36
36
  description: "Defines the user which is used to access the inspected system via SSH"
37
- )
37
+ )
38
38
  entry("experimental-features",
39
39
  default: false,
40
40
  description: "Enable experimental features. See " \
41
41
  "https://github.com/SUSE/machinery/wiki/Experimental-Features for more details"
42
- )
42
+ )
43
43
  entry("http_server_port",
44
44
  default: 7585,
45
45
  description: "TCP port used by the HTTP server for the HTML view"
46
- )
46
+ )
47
47
  end
48
48
 
49
49
  def deprecated_entries
@@ -0,0 +1,68 @@
1
+ # Copyright (c) 2013-2015 SUSE LLC
2
+ #
3
+ # This program is free software; you can redistribute it and/or
4
+ # modify it under the terms of version 3 of the GNU General Public License as
5
+ # published by the Free Software Foundation.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program; if not, contact SUSE LLC.
14
+ #
15
+ # To contact SUSE about this file by physical or electronic mail,
16
+ # you may find current contact information at www.suse.com
17
+
18
+ class DpkgDatabase < ManagedFilesDatabase
19
+ def managed_files_list(&block)
20
+ message = "The list of changed config and managed files is not complete on dpkg systems."\
21
+ " The reason for this is missing verifcation data:" \
22
+ " https://github.com/SUSE/machinery/wiki/Ubuntu-Inspection"
23
+
24
+ Machinery.logger.warn(message)
25
+ Machinery::Ui.warn("Warning: #{message}")
26
+
27
+ @system.run_command_with_progress("dpkg", "--verify", privileged: true, &block)
28
+ end
29
+
30
+ def package_for_file_path(file)
31
+ package_name = @system.run_command("dpkg", "-S", file, stdout: :capture).split(":").first
32
+ package_details = @system.run_command("dpkg", "-s", package_name, stdout: :capture)
33
+ package_version = package_details.match(/^Version: (.*)$/)[1]
34
+
35
+ [package_name, package_version]
36
+ end
37
+
38
+ def handle_verify_fail(_path)
39
+ # dpkg can only check for md5sum. Thus, dpkg will report for every file
40
+ # that not all tests (e.g. owner, mode) could be performed.
41
+ #
42
+ # We won't show a warning for each and every file but one warning
43
+ # per inspection.
44
+ #
45
+ # This method is a no-op for DpkgDatabase.
46
+ end
47
+
48
+ def parse_changes_line(line)
49
+ file, changes, type = super(line)
50
+
51
+ # dpkg doesn't report deleted files as deleted but reports md5sum changes instead
52
+ if changes.include?("md5")
53
+ begin
54
+ @system.run_command("ls", file)
55
+ rescue Cheetah::ExecutionFailed
56
+ changes = ["deleted"]
57
+ end
58
+ end
59
+
60
+ [file, changes, type]
61
+ end
62
+
63
+ def check_requirements
64
+ @system.check_requirement("dpkg", "--version")
65
+ @system.check_requirement("stat", "--version")
66
+ @system.check_requirement("find", "--version")
67
+ end
68
+ end
@@ -50,6 +50,8 @@ class ElementFilter
50
50
  value_array = value.elements
51
51
 
52
52
  (value_array - Array(matcher)).empty? && (Array(matcher) - value_array).empty?
53
+ when ::Array
54
+ (value - Array(matcher)).empty? && (Array(matcher) - value).empty?
53
55
  when String
54
56
  if matcher.is_a?(Array)
55
57
  exception = Machinery::Errors::ElementFilterTypeMismatch.new
data/lib/file_diff.rb CHANGED
@@ -19,8 +19,8 @@ class FileDiff
19
19
  def self.diff(description1, description2, scope, path)
20
20
  return nil if !description1.scope_extracted?(scope) || !description2.scope_extracted?(scope)
21
21
 
22
- file1 = description1[scope].files.find { |f| f.name == path }
23
- file2 = description2[scope].files.find { |f| f.name == path }
22
+ file1 = description1[scope].find { |f| f.name == path }
23
+ file2 = description2[scope].find { |f| f.name == path }
24
24
  return nil if !file1 || !file2
25
25
 
26
26
  if file1.binary? || file2.binary?
data/lib/file_scope.rb CHANGED
@@ -15,58 +15,19 @@
15
15
  # To contact SUSE about this file by physical or electronic mail,
16
16
  # you may find current contact information at www.suse.com
17
17
 
18
- class FileScope < Machinery::Object
18
+ class FileScope < Machinery::Array
19
19
  def compare_with(other)
20
- validate_attributes(other)
20
+ only_self, only_other, changed, common = super(other)
21
21
 
22
- only_self = self.class.new
23
- only_other = self.class.new
24
- shared = self.class.new
25
-
26
- compare_extracted(other, only_self, only_other, shared)
27
- changed = compare_files(other, only_self, only_other, shared)
28
-
29
- only_self = nil if only_self.empty?
30
- only_other = nil if only_other.empty?
31
- shared = nil if shared.empty?
32
- [only_self, only_other, changed, shared]
33
- end
34
-
35
- def length
36
- files.try(:length) || 0
37
- end
38
-
39
- private
40
-
41
- def validate_attributes(other)
42
- expected_attributes = ["extracted", "files"]
43
- actual_attributes = (attributes.keys + other.attributes.keys).uniq.sort
44
-
45
- if actual_attributes != expected_attributes
46
- unsupported = actual_attributes - expected_attributes
47
- raise Machinery::Errors::MachineryError.new(
48
- "The following attributes are not covered by FileScope#compare_with: " +
49
- unsupported.join(", ")
50
- )
51
- end
52
- end
53
-
54
- def compare_extracted(other, only_self, only_other, shared)
55
- if extracted == other.extracted
56
- shared.extracted = extracted
57
- else
58
- only_self.extracted = extracted
59
- only_other.extracted = other.extracted
22
+ if only_self && only_other
23
+ changed = Machinery::Scope.extract_changed_elements(only_self, only_other, :name)
60
24
  end
61
- end
62
-
63
- def compare_files(other, only_self, only_other, shared)
64
- own_files, other_files, changed, shared_files = files.compare_with(other.files)
65
-
66
- only_self.files = own_files if own_files
67
- only_other.files = other_files if other_files
68
- shared.files = shared_files if shared_files
69
25
 
70
- changed
26
+ [
27
+ only_self,
28
+ only_other,
29
+ changed,
30
+ common
31
+ ].map { |e| (e && !e.empty?) ? e : nil }
71
32
  end
72
33
  end
@@ -56,16 +56,22 @@ class FileValidator
56
56
  def scope_extracted?(scope)
57
57
  if @format_version == 1
58
58
  @json_hash[scope] && ScopeFileStore.new(@base_path, scope.to_s).path
59
- else
59
+ elsif @format_version < 6
60
60
  @json_hash[scope] && @json_hash[scope]["extracted"]
61
+ else
62
+ @json_hash[scope] && @json_hash[scope]["_attributes"]["extracted"]
61
63
  end
62
64
  end
63
65
 
64
66
  def expected_files(scope)
67
+ changes_proc = -> (file) { file["changes"] }
65
68
  if @format_version == 1
66
69
  files = @json_hash[scope]
67
- else
70
+ elsif @format_version < 6
68
71
  files = @json_hash[scope]["files"]
72
+ else
73
+ changes_proc = -> (file) { file["changes"]}
74
+ files = @json_hash[scope]["_elements"]
69
75
  end
70
76
 
71
77
  if scope == "unmanaged_files"
@@ -79,8 +85,8 @@ class FileValidator
79
85
  expected_files += tree_tarballs
80
86
  else
81
87
  expected_files = files.reject do |file|
82
- file["changes"].include?("deleted") || (file["type"] && file["type"] != "file")
83
- end.map { |file| file["name"] }
88
+ changes_proc.call(file).include?("deleted") || (file["type"] && file["type"] != "file")
89
+ end.map { |file| file["name"] }
84
90
  end
85
91
 
86
92
  store_base_path = ScopeFileStore.new(@base_path, scope.to_s).path
data/lib/filter.rb CHANGED
@@ -21,24 +21,24 @@
21
21
  # Filters are usually created by passing a filter definition string to the
22
22
  # constructor, e.g.
23
23
  #
24
- # filter = Filter.new("/unmanaged_files/files/name=/opt")
24
+ # filter = Filter.new("/unmanaged_files/name=/opt")
25
25
  #
26
26
  # Existing filters can be extended by amending the definition:
27
27
  #
28
- # filter.add_element_filter_from_definition("/unmanaged_files/files/name=/srv")
28
+ # filter.add_element_filter_from_definition("/unmanaged_files/name=/srv")
29
29
  #
30
30
  # or by adding ElementFilters directly:
31
31
  #
32
- # element_filter = ElementFilter.new("/unmanaged_files/files/name", ["/opt", "/srv"])
32
+ # element_filter = ElementFilter.new("/unmanaged_files/name", ["/opt", "/srv"])
33
33
  # filter.add_element_filter(element_filter)
34
34
  #
35
35
  #
36
36
  # The actual filtering can be done by passing values to Filter#matches?
37
37
  #
38
- # filter = Filter.new("/unmanaged_files/files/name=/opt*")
39
- # filter.matches?("/unmanaged_files/files/name", "/opt/foo")
38
+ # filter = Filter.new("/unmanaged_files/name=/opt*")
39
+ # filter.matches?("/unmanaged_files/name", "/opt/foo")
40
40
  # => true
41
- # filter.matches?("/unmanaged_files/files/name", "/srv/bar")
41
+ # filter.matches?("/unmanaged_files/name", "/srv/bar")
42
42
  # => false
43
43
  #
44
44
  # More details about how the filter work can be found at
@@ -63,7 +63,7 @@ class FilterOptionParser
63
63
  files.reject!(&:empty?) # Ignore empty filters
64
64
  files.map! { |file| file.chomp("/") } # List directories without the trailing /, in order to
65
65
  # not confuse the unmanaged files inspector
66
- files.map { |file| "/unmanaged_files/files/name=#{file}" }
66
+ files.map { |file| "/unmanaged_files/name=#{file}" }
67
67
  end
68
68
 
69
69
  def expand_filter_file(path)
data/lib/kiwi_config.rb CHANGED
@@ -29,6 +29,7 @@ class KiwiConfig < Exporter
29
29
  "packages",
30
30
  "os"
31
31
  )
32
+ check_exported_os
32
33
  check_existance_of_extracted_files
33
34
  check_repositories
34
35
  generate_config
@@ -101,7 +102,7 @@ class KiwiConfig < Exporter
101
102
  output_root_path = File.join(output_location, "root")
102
103
  FileUtils.mkdir_p(output_root_path)
103
104
 
104
- @system_description[scope].files.each do |file|
105
+ @system_description[scope].each do |file|
105
106
  if file.deleted?
106
107
  @sh << "rm -rf '#{quote(file.name)}'\n"
107
108
  elsif file.directory?
@@ -172,6 +173,15 @@ EOF
172
173
  end
173
174
  end
174
175
 
176
+ def check_exported_os
177
+ unless @system_description.os.is_a?(OsSuse)
178
+ raise Machinery::Errors::ExportFailed.new(
179
+ "Export is not possible because the operating system " \
180
+ "'#{@system_description.os.display_name}' is not supported."
181
+ )
182
+ end
183
+ end
184
+
175
185
  def generate_config
176
186
  @sh = <<EOF
177
187
  test -f /.kconfig && . /.kconfig
@@ -182,13 +192,6 @@ suseImportBuildKey
182
192
  suseConfig
183
193
  EOF
184
194
 
185
- unless @system_description.os.is_a?(OsSuse)
186
- raise Machinery::Errors::ExportFailed.new(
187
- "Export is not possible because the operating system " \
188
- "'#{@system_description.os.display_name}' is not supported."
189
- )
190
- end
191
-
192
195
  builder = Nokogiri::XML::Builder.new do |xml|
193
196
  xml.image(schemaversion: "5.8", name: @system_description.name) do
194
197
  xml.description(type: "system") do
@@ -292,7 +295,7 @@ EOF
292
295
 
293
296
  case init_system
294
297
  when "sysvinit"
295
- @system_description["services"].services.each do |service|
298
+ @system_description["services"].each do |service|
296
299
  if service.state == "on"
297
300
  @sh << "chkconfig #{service.name} on\n"
298
301
  else
@@ -303,7 +306,7 @@ EOF
303
306
  when "systemd"
304
307
  # possible systemd service states:
305
308
  # http://www.freedesktop.org/software/systemd/man/systemctl.html#Unit%20File%20Commands
306
- @system_description["services"].services.each do |service|
309
+ @system_description["services"].each do |service|
307
310
  case service.state
308
311
  when "enabled"
309
312
  @sh << "systemctl enable #{service.name}\n"
data/lib/machinery.rb CHANGED
@@ -109,7 +109,9 @@ require_relative "workload_mapper_dsl"
109
109
  require_relative "containerized_app"
110
110
  require_relative "move_task"
111
111
  require_relative "docker_system"
112
+ require_relative "managed_files_database"
112
113
  require_relative "rpm_database"
114
+ require_relative "dpkg_database"
113
115
 
114
116
  Dir[File.join(Machinery::ROOT, "plugins", "**", "*.rb")].each { |f| require(f) }
115
117
 
@@ -52,7 +52,7 @@ class MachineryHelper
52
52
 
53
53
  def run_helper(scope)
54
54
  json = @system.run_command(remote_helper_path, stdout: :capture, stderr: STDERR)
55
- scope.set_attributes(JSON.parse(json))
55
+ scope.insert(0, *JSON.parse(json)["files"])
56
56
  end
57
57
 
58
58
  def remove_helper
@@ -0,0 +1,200 @@
1
+ # Copyright (c) 2013-2015 SUSE LLC
2
+ #
3
+ # This program is free software; you can redistribute it and/or
4
+ # modify it under the terms of version 3 of the GNU General Public License as
5
+ # published by the Free Software Foundation.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program; if not, contact SUSE LLC.
14
+ #
15
+ # To contact SUSE about this file by physical or electronic mail,
16
+ # you may find current contact information at www.suse.com
17
+
18
+ class ManagedFilesDatabase
19
+ class ChangedFile < Machinery::Object
20
+ attr_accessor :type
21
+
22
+ def initialize(type, attrs)
23
+ super(attrs)
24
+ @type = type
25
+ end
26
+
27
+ def config_file?
28
+ @type == "c"
29
+ end
30
+ end
31
+
32
+ def initialize(system)
33
+ @system = system
34
+ end
35
+
36
+ def expected_tag?(character, position)
37
+ if @rpm_changes[position] == character
38
+ true
39
+ else
40
+ @unknown_tag ||= ![".", "?"].include?(@rpm_changes[position])
41
+ false
42
+ end
43
+ end
44
+
45
+ def changed_files(&block)
46
+ return @changed_files if @changed_files
47
+ check_requirements
48
+
49
+ result = managed_files_list(&block).each_line.map do |line|
50
+ line.chomp!
51
+ next unless line =~ /^[^ ]+[ ]+. \/.*$/
52
+
53
+ file, changes, type = parse_changes_line(line)
54
+
55
+ package_name, package_version = package_for_file_path(file)
56
+
57
+ ChangedFile.new(
58
+ type,
59
+ name: file,
60
+ package_name: package_name,
61
+ package_version: package_version,
62
+ status: "changed",
63
+ changes: changes
64
+ )
65
+ end.compact.uniq
66
+
67
+ paths = result.reject { |f| f.changes == ["deleted"] }.map(&:name)
68
+ path_data = get_path_data(paths)
69
+ result.each do |pkg|
70
+ next unless path_data[pkg.name]
71
+
72
+ path_data[pkg.name].each do |key, value|
73
+ pkg[key] = value
74
+ end
75
+ end
76
+
77
+ @changed_files = result
78
+ end
79
+
80
+ def parse_changes_line(line)
81
+ # rpm provides lines per config file where first 9 characters indicate which
82
+ # properties of the file are modified
83
+ @rpm_changes, *fields = line.split(" ")
84
+ # nine rpm changes are known
85
+ @unknown_tag = @rpm_changes.size > 9
86
+
87
+ # For config or documentation files there's an additional field which
88
+ # contains "c" or "d"
89
+ type = fields[0].start_with?("/") ? "" : fields.shift
90
+ path = fields.join(" ")
91
+
92
+ changes = []
93
+ if @rpm_changes == "missing"
94
+ changes << "deleted"
95
+ elsif @rpm_changes == "........." && path.end_with?(" (replaced)")
96
+ changes << "replaced"
97
+ path.slice!(/ \(replaced\)$/)
98
+ else
99
+ changes << "size" if expected_tag?("S", 0)
100
+ changes << "mode" if expected_tag?("M", 1)
101
+ changes << "md5" if expected_tag?("5", 2)
102
+ changes << "device_number" if expected_tag?("D", 3)
103
+ changes << "link_path" if expected_tag?("L", 4)
104
+ changes << "user" if expected_tag?("U", 5)
105
+ changes << "group" if expected_tag?("G", 6)
106
+ changes << "time" if expected_tag?("T", 7)
107
+ changes << "capabilities" if @rpm_changes.size > 8 && expected_tag?("P", 8)
108
+ end
109
+
110
+ if @unknown_tag
111
+ changes << "other_rpm_changes"
112
+ end
113
+
114
+ handle_verify_fail(path) if @rpm_changes.include?("?")
115
+
116
+ [path, changes, type]
117
+ end
118
+
119
+ def parse_stat_line(line)
120
+ mode, user, group, uid, gid, type, *path_line = line.split(":")
121
+ path = path_line.join(":").chomp
122
+
123
+ user = uid if user == "UNKNOWN"
124
+ group = gid if group == "UNKNOWN"
125
+
126
+ type = case type
127
+ when "directory"
128
+ "dir"
129
+ when "symbolic link"
130
+ "link"
131
+ when /file$/
132
+ "file"
133
+ else
134
+ raise(
135
+ "The inspection failed because of the unknown type `#{type}` of file `#{path}`."
136
+ )
137
+ end
138
+
139
+ [path, {
140
+ mode: mode,
141
+ user: user,
142
+ group: group,
143
+ type: type
144
+ }]
145
+ end
146
+
147
+ def get_link_target(link)
148
+ @system.run_command(
149
+ "find", link, "-prune", "-printf", "%l",
150
+ stdout: :capture,
151
+ privileged: true
152
+ ).strip
153
+ end
154
+
155
+ # get path data for list of files
156
+ # cur_files is guaranteed to not exceed max command line length
157
+ def get_file_properties(cur_files)
158
+ ret = {}
159
+ out = @system.run_command(
160
+ "stat", "--printf", "%a:%U:%G:%u:%g:%F:%n\\n",
161
+ *cur_files,
162
+ stdout: :capture,
163
+ privileged: true
164
+ )
165
+ out.each_line do |l|
166
+ path, values = parse_stat_line(l)
167
+ ret[path] = values
168
+ ret[path][:target] = get_link_target(path) if values[:type] == "link"
169
+ end
170
+ ret
171
+ end
172
+
173
+ def get_path_data(paths)
174
+ ret = {}
175
+ path_index = 0
176
+ # arbitrary number for maximum command line length that should always work
177
+ max_len = 50000
178
+ cur_files = []
179
+ cur_len = 0
180
+ while path_index < paths.size
181
+ if cur_files.empty? || paths[path_index].size + cur_len + 1 < max_len
182
+ cur_files << paths[path_index]
183
+ cur_len += paths[path_index].size + 1
184
+ path_index += 1
185
+ else
186
+ ret.merge!(get_file_properties(cur_files))
187
+ cur_files.clear
188
+ cur_len = 0
189
+ end
190
+ end
191
+ ret.merge!(get_file_properties(cur_files)) unless cur_files.empty?
192
+ ret
193
+ end
194
+
195
+ def handle_verify_fail(path)
196
+ message = "Could not perform all tests on rpm changes for file '#{path}'."
197
+ Machinery.logger.warn(message)
198
+ Machinery::Ui.warn("Warning: #{message}")
199
+ end
200
+ end