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.
- checksums.yaml +4 -4
- data/.git_revision +1 -1
- data/NEWS +10 -0
- data/filters/default_filters.json +21 -20
- data/html/assets/machinery-base.js +4 -0
- data/html/index.html.haml +1 -1
- data/html/partials/changed_managed_files.html.haml +2 -2
- data/html/partials/compare/changed_managed_file_list.html.haml +2 -2
- data/html/partials/compare/changed_managed_files.html.haml +3 -3
- data/html/partials/compare/config_file_list.html.haml +2 -2
- data/html/partials/compare/config_files.html.haml +3 -3
- data/html/partials/compare/packages.html.haml +21 -4
- data/html/partials/compare/repositories.html.haml +10 -7
- data/html/partials/compare/repository_list_apt.html.haml +15 -0
- data/html/partials/compare/repository_list_yum.html.haml +35 -0
- data/html/partials/compare/{repository_list.html.haml → repository_list_zypp.html.haml} +0 -0
- data/html/partials/compare/service_list.html.haml +1 -1
- data/html/partials/compare/services.html.haml +3 -3
- data/html/partials/compare/unmanaged_file_list.html.haml +2 -2
- data/html/partials/compare/unmanaged_files.html.haml +1 -1
- data/html/partials/config_files.html.haml +39 -41
- data/html/partials/repositories.html.haml +1 -23
- data/html/partials/repositories_apt.html.haml +15 -0
- data/html/partials/repositories_yum.html.haml +30 -0
- data/html/partials/repositories_zypp.html.haml +24 -0
- data/html/partials/services.html.haml +2 -2
- data/html/partials/unmanaged_files.html.haml +2 -2
- data/inspect_helpers/dpkg_unmanaged_files.sh +47 -0
- data/inspect_helpers/yum_repositories.py +3 -5
- data/lib/analyze_config_file_diffs_task.rb +11 -1
- data/lib/array.rb +97 -35
- data/lib/autoyast.rb +13 -2
- data/lib/cli.rb +10 -2
- data/lib/config.rb +4 -4
- data/lib/dpkg_database.rb +68 -0
- data/lib/element_filter.rb +2 -0
- data/lib/file_diff.rb +2 -2
- data/lib/file_scope.rb +10 -49
- data/lib/file_validator.rb +10 -4
- data/lib/filter.rb +6 -6
- data/lib/filter_option_parser.rb +1 -1
- data/lib/kiwi_config.rb +13 -10
- data/lib/machinery.rb +2 -0
- data/lib/machinery_helper.rb +1 -1
- data/lib/managed_files_database.rb +200 -0
- data/lib/object.rb +5 -3
- data/lib/remote_system.rb +43 -10
- data/lib/renderer.rb +3 -4
- data/lib/rpm_database.rb +7 -183
- data/lib/scope_file_access_archive.rb +3 -3
- data/lib/scope_file_access_flat.rb +1 -1
- data/lib/server.rb +7 -2
- data/lib/system.rb +50 -22
- data/lib/system_description.rb +3 -3
- data/lib/version.rb +1 -1
- data/lib/workload_mapper.rb +2 -2
- data/machinery-helper/machinery_helper.go +252 -178
- data/machinery-helper/machinery_helper_test.go +121 -121
- data/machinery-helper/mountpoints.go +28 -28
- data/machinery-helper/tar.go +105 -104
- data/machinery-helper/version.go +1 -1
- data/man/generated/machinery.1.gz +0 -0
- data/man/generated/machinery.1.html +19 -8
- data/plugins/changed_managed_files/changed_managed_files_inspector.rb +3 -3
- data/plugins/changed_managed_files/changed_managed_files_model.rb +3 -1
- data/plugins/changed_managed_files/changed_managed_files_renderer.rb +2 -2
- data/plugins/changed_managed_files/schema/system-description-changed-managed-files.schema-v6.json +168 -0
- data/plugins/config_files/config_files_inspector.rb +4 -4
- data/plugins/config_files/config_files_model.rb +3 -1
- data/plugins/config_files/config_files_renderer.rb +2 -2
- data/plugins/config_files/schema/system-description-config-files.schema-v6.json +160 -0
- data/plugins/environment/schema/system-description-environment.schema-v6.json +17 -0
- data/plugins/groups/schema/system-description-groups.schema-v6.json +49 -0
- data/plugins/os/schema/system-description-os.schema-v6.json +21 -0
- data/plugins/packages/packages_inspector.rb +76 -6
- data/plugins/packages/packages_model.rb +31 -12
- data/plugins/packages/packages_renderer.rb +5 -2
- data/plugins/packages/schema/system-description-packages.schema-v6.json +115 -0
- data/plugins/patterns/patterns_inspector.rb +26 -2
- data/plugins/patterns/schema/system-description-patterns.schema-v6.json +58 -0
- data/plugins/repositories/repositories_inspector.rb +41 -14
- data/plugins/repositories/repositories_model.rb +55 -12
- data/plugins/repositories/repositories_renderer.rb +23 -7
- data/plugins/repositories/schema/system-description-repositories.schema-v6.json +165 -0
- data/plugins/services/schema/system-description-services.schema-v6.json +93 -0
- data/plugins/services/services_inspector.rb +88 -22
- data/plugins/services/services_model.rb +9 -15
- data/plugins/services/services_renderer.rb +2 -2
- data/plugins/unmanaged_files/schema/system-description-unmanaged-files.schema-v6.json +162 -0
- data/plugins/unmanaged_files/unmanaged_files_inspector.rb +80 -30
- data/plugins/unmanaged_files/unmanaged_files_model.rb +22 -18
- data/plugins/unmanaged_files/unmanaged_files_renderer.rb +3 -3
- data/plugins/users/schema/system-description-users.schema-v6.json +86 -0
- data/schema/migrations/migrate5to6.rb +101 -0
- data/schema/system-description-global.schema-v6.json +43 -0
- metadata +24 -4
- 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
|
data/lib/element_filter.rb
CHANGED
@@ -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].
|
23
|
-
file2 = description2[scope].
|
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::
|
18
|
+
class FileScope < Machinery::Array
|
19
19
|
def compare_with(other)
|
20
|
-
|
20
|
+
only_self, only_other, changed, common = super(other)
|
21
21
|
|
22
|
-
only_self
|
23
|
-
|
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
|
-
|
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
|
data/lib/file_validator.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
83
|
-
|
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/
|
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/
|
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/
|
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/
|
39
|
-
# filter.matches?("/unmanaged_files/
|
38
|
+
# filter = Filter.new("/unmanaged_files/name=/opt*")
|
39
|
+
# filter.matches?("/unmanaged_files/name", "/opt/foo")
|
40
40
|
# => true
|
41
|
-
# filter.matches?("/unmanaged_files/
|
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
|
data/lib/filter_option_parser.rb
CHANGED
@@ -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/
|
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].
|
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"].
|
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"].
|
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
|
|
data/lib/machinery_helper.rb
CHANGED
@@ -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
|