machinery-tool 1.0.2

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 (157) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +674 -0
  3. data/NEWS +143 -0
  4. data/bin/machinery +29 -0
  5. data/helpers/changed_managed_files.sh +32 -0
  6. data/helpers/filter-packages-for-build.yaml +6 -0
  7. data/html/assets/arrow_down.png +0 -0
  8. data/html/assets/arrow_up.png +0 -0
  9. data/html/assets/bootstrap-popover.js +113 -0
  10. data/html/assets/bootstrap-tooltip.js +457 -0
  11. data/html/assets/bootstrap.min.css +5 -0
  12. data/html/assets/collapse.js +174 -0
  13. data/html/assets/hogan-3.0.2.min.mustache.js +5 -0
  14. data/html/assets/jquery-2.1.1.min.js +4 -0
  15. data/html/assets/logo-changed-managed-files-small.png +0 -0
  16. data/html/assets/logo-changed-managed-files.png +0 -0
  17. data/html/assets/logo-config-files-small.png +0 -0
  18. data/html/assets/logo-config-files.png +0 -0
  19. data/html/assets/logo-groups-small.png +0 -0
  20. data/html/assets/logo-groups.png +0 -0
  21. data/html/assets/logo-os-small.png +0 -0
  22. data/html/assets/logo-os.png +0 -0
  23. data/html/assets/logo-packages-small.png +0 -0
  24. data/html/assets/logo-packages.png +0 -0
  25. data/html/assets/logo-patterns-small.png +0 -0
  26. data/html/assets/logo-patterns.png +0 -0
  27. data/html/assets/logo-repositories-small.png +0 -0
  28. data/html/assets/logo-repositories.png +0 -0
  29. data/html/assets/logo-services-small.png +0 -0
  30. data/html/assets/logo-services.png +0 -0
  31. data/html/assets/logo-unmanaged-files-small.png +0 -0
  32. data/html/assets/logo-unmanaged-files.png +0 -0
  33. data/html/assets/logo-users-small.png +0 -0
  34. data/html/assets/logo-users.png +0 -0
  35. data/html/assets/machinery-base.css +5767 -0
  36. data/html/assets/machinery.css +131 -0
  37. data/html/assets/machinery.js +148 -0
  38. data/html/assets/transition.js +59 -0
  39. data/html/assets/wheels_horizontal.png +0 -0
  40. data/html/index.html.haml +468 -0
  41. data/kiwi_helpers/kiwi_export_readme.md +22 -0
  42. data/kiwi_helpers/merge_users_and_groups.pl.erb +231 -0
  43. data/kiwi_helpers/unmanaged_files_build_excludes +5 -0
  44. data/lib/analyze_config_file_diffs_task.rb +130 -0
  45. data/lib/array.rb +98 -0
  46. data/lib/build_task.rb +124 -0
  47. data/lib/changed_rpm_files_helper.rb +96 -0
  48. data/lib/cli.rb +600 -0
  49. data/lib/compare_task.rb +68 -0
  50. data/lib/config.rb +33 -0
  51. data/lib/config_base.rb +117 -0
  52. data/lib/config_task.rb +56 -0
  53. data/lib/constants.rb +24 -0
  54. data/lib/copy_task.rb +22 -0
  55. data/lib/current_user.rb +23 -0
  56. data/lib/deploy_task.rb +89 -0
  57. data/lib/exceptions.rb +113 -0
  58. data/lib/generate_html_task.rb +22 -0
  59. data/lib/helper.rb +22 -0
  60. data/lib/hint.rb +39 -0
  61. data/lib/html.rb +103 -0
  62. data/lib/inspect_task.rb +93 -0
  63. data/lib/inspector.rb +65 -0
  64. data/lib/kiwi_config.rb +356 -0
  65. data/lib/kiwi_export_task.rb +36 -0
  66. data/lib/list_task.rb +64 -0
  67. data/lib/local_system.rb +127 -0
  68. data/lib/logged_cheetah.rb +25 -0
  69. data/lib/machinery.rb +85 -0
  70. data/lib/machinery_logger.rb +47 -0
  71. data/lib/migration.rb +128 -0
  72. data/lib/mountpoints.rb +72 -0
  73. data/lib/object.rb +138 -0
  74. data/lib/os.rb +78 -0
  75. data/lib/remote_system.rb +114 -0
  76. data/lib/remove_task.rb +43 -0
  77. data/lib/renderer.rb +243 -0
  78. data/lib/renderer_helper.rb +26 -0
  79. data/lib/rpm.rb +52 -0
  80. data/lib/scope_mixin.rb +38 -0
  81. data/lib/show_task.rb +65 -0
  82. data/lib/system.rb +81 -0
  83. data/lib/system_description.rb +228 -0
  84. data/lib/system_description_store.rb +167 -0
  85. data/lib/system_description_validator.rb +216 -0
  86. data/lib/tarball.rb +82 -0
  87. data/lib/ui.rb +74 -0
  88. data/lib/upgrade_format_task.rb +55 -0
  89. data/lib/validate_task.rb +23 -0
  90. data/lib/version.rb +22 -0
  91. data/lib/zypper.rb +70 -0
  92. data/man/generated/machinery.1.gz +0 -0
  93. data/man/generated/machinery.1.html +1056 -0
  94. data/plugins/docs/changed_managed_files.md +2 -0
  95. data/plugins/docs/config_files.md +5 -0
  96. data/plugins/docs/groups.md +2 -0
  97. data/plugins/docs/os.md +2 -0
  98. data/plugins/docs/packages.md +2 -0
  99. data/plugins/docs/patterns.md +5 -0
  100. data/plugins/docs/repositories.md +24 -0
  101. data/plugins/docs/services.md +6 -0
  102. data/plugins/docs/unmanaged_files.md +13 -0
  103. data/plugins/docs/users.md +3 -0
  104. data/plugins/inspect/changed_managed_files_inspector.rb +109 -0
  105. data/plugins/inspect/config_files_inspector.rb +117 -0
  106. data/plugins/inspect/groups_inspector.rb +46 -0
  107. data/plugins/inspect/os_inspector.rb +116 -0
  108. data/plugins/inspect/packages_inspector.rb +46 -0
  109. data/plugins/inspect/patterns_inspector.rb +67 -0
  110. data/plugins/inspect/repositories_inspector.rb +107 -0
  111. data/plugins/inspect/services_inspector.rb +88 -0
  112. data/plugins/inspect/unmanaged_files_inspector.rb +393 -0
  113. data/plugins/inspect/users_inspector.rb +87 -0
  114. data/plugins/model/changed_managed_files_model.rb +29 -0
  115. data/plugins/model/config_files_model.rb +29 -0
  116. data/plugins/model/groups_model.rb +26 -0
  117. data/plugins/model/os_model.rb +20 -0
  118. data/plugins/model/packages_model.rb +26 -0
  119. data/plugins/model/patterns_model.rb +26 -0
  120. data/plugins/model/repositories_model.rb +26 -0
  121. data/plugins/model/services_model.rb +48 -0
  122. data/plugins/model/unmanaged_files_model.rb +29 -0
  123. data/plugins/model/users_model.rb +26 -0
  124. data/plugins/schema/v1/system-description-changed-managed-files.schema.json +83 -0
  125. data/plugins/schema/v1/system-description-config-files.schema.json +83 -0
  126. data/plugins/schema/v1/system-description-groups.schema.json +30 -0
  127. data/plugins/schema/v1/system-description-os.schema.json +21 -0
  128. data/plugins/schema/v1/system-description-packages.schema.json +34 -0
  129. data/plugins/schema/v1/system-description-patterns.schema.json +24 -0
  130. data/plugins/schema/v1/system-description-repositories.schema.json +41 -0
  131. data/plugins/schema/v1/system-description-services.schema.json +30 -0
  132. data/plugins/schema/v1/system-description-unmanaged-files.schema.json +105 -0
  133. data/plugins/schema/v1/system-description-users.schema.json +61 -0
  134. data/plugins/schema/v2/system-description-changed-managed-files.schema.json +92 -0
  135. data/plugins/schema/v2/system-description-config-files.schema.json +92 -0
  136. data/plugins/schema/v2/system-description-groups.schema.json +30 -0
  137. data/plugins/schema/v2/system-description-os.schema.json +21 -0
  138. data/plugins/schema/v2/system-description-packages.schema.json +34 -0
  139. data/plugins/schema/v2/system-description-patterns.schema.json +24 -0
  140. data/plugins/schema/v2/system-description-repositories.schema.json +41 -0
  141. data/plugins/schema/v2/system-description-services.schema.json +30 -0
  142. data/plugins/schema/v2/system-description-unmanaged-files.schema.json +138 -0
  143. data/plugins/schema/v2/system-description-users.schema.json +61 -0
  144. data/plugins/show/changed_managed_files_renderer.rb +46 -0
  145. data/plugins/show/config_files_renderer.rb +62 -0
  146. data/plugins/show/groups_renderer.rb +36 -0
  147. data/plugins/show/os_renderer.rb +31 -0
  148. data/plugins/show/packages_renderer.rb +32 -0
  149. data/plugins/show/patterns_renderer.rb +32 -0
  150. data/plugins/show/repositories_renderer.rb +38 -0
  151. data/plugins/show/services_renderer.rb +32 -0
  152. data/plugins/show/unmanaged_files_renderer.rb +42 -0
  153. data/plugins/show/users_renderer.rb +35 -0
  154. data/schema/migrations/migrate1to2.rb +56 -0
  155. data/schema/v1/system-description-global.schema.json +31 -0
  156. data/schema/v2/system-description-global.schema.json +31 -0
  157. metadata +370 -0
@@ -0,0 +1,46 @@
1
+ # Copyright (c) 2013-2014 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 PackagesInspector < Inspector
19
+ def inspect(system, description, options = {})
20
+ system.check_requirement("rpm", "--version")
21
+
22
+ packages = Array.new
23
+ rpm_data = system.run_command(
24
+ "rpm","-qa","--qf",
25
+ "%{NAME}|%{VERSION}|%{RELEASE}|%{ARCH}|%{VENDOR}|%{SIGMD5}$",
26
+ :stdout=>:capture
27
+ )
28
+ # gpg-pubkeys are no real packages but listed by rpm in the regular
29
+ # package list
30
+ rpm_data.scan(/(.*?)\|(.*?)\|(.*?)\|(.*?)\|(.*?)\|(.*?)\$/).reject do |name, *attrs|
31
+ name =~ /^gpg-pubkey$/
32
+ end.each do |name, version, release, arch, vendor, checksum|
33
+ packages << Package.new(
34
+ :name => name,
35
+ :version => version,
36
+ :release => release,
37
+ :arch => arch,
38
+ :vendor => vendor,
39
+ :checksum => checksum
40
+ )
41
+ end
42
+
43
+ description.packages = PackagesScope.new(packages.sort_by(&:name))
44
+ "Found #{packages.count} packages."
45
+ end
46
+ end
@@ -0,0 +1,67 @@
1
+ # Copyright (c) 2013-2014 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 PatternsInspector < Inspector
19
+ def inspect(system, description, options = {})
20
+ system.check_requirement("zypper", "--version")
21
+
22
+ xml = system.run_command("zypper", "-xq", "patterns", "-i", :stdout => :capture)
23
+ pattern_list = Nokogiri::XML(xml).xpath("/stream/pattern-list/pattern")
24
+
25
+ if pattern_list.count == 0
26
+ description.patterns = PatternsScope.new()
27
+ return "Found 0 patterns."
28
+ end
29
+
30
+ # The zypper patterns output looks like this:
31
+ #
32
+ # <?xml version='1.0'?>
33
+ # <stream>
34
+ # <pattern-list>
35
+ # <pattern name="base" version="13.1" release="13.6.1" epoch="0" arch="i586" vendor="openSUSE" summary="Base System" repo="repo-oss" installed="1" uservisible="1">
36
+ # <description>This is the base runtime system. It contains only a minimal multiuser booting system. For running on real hardware, you need to add additional packages and pattern to make this pattern useful on its own.</description>
37
+ # </pattern>
38
+ # <pattern name="base" version="13.1" release="13.6.1" epoch="0" arch="x86_64" vendor="openSUSE" summary="Base System" repo="repo-oss" installed="1" uservisible="1">
39
+ # <description>This is the base runtime system. It contains only a minimal multiuser booting system. For running on real hardware, you need to add additional packages and pattern to make this pattern useful on its own.</description>
40
+ # </pattern>
41
+ # </pattern-list>
42
+ # </stream>
43
+ #
44
+ #
45
+ # and we want to return an array of pattern objects like this:
46
+ #
47
+ # [
48
+ # {
49
+ # name: "base",
50
+ # version: "13.1-13.6.1",
51
+ # }
52
+ # ]
53
+ #
54
+ # Patterns listed for different architectures should be combined.
55
+
56
+ patterns = pattern_list.map do |pattern|
57
+ Pattern.new(
58
+ name: pattern["name"],
59
+ version: pattern["version"],
60
+ release: pattern["release"]
61
+ )
62
+ end.uniq.sort_by(&:name)
63
+
64
+ description.patterns = PatternsScope.new(patterns)
65
+ "Found #{patterns.count} patterns."
66
+ end
67
+ end
@@ -0,0 +1,107 @@
1
+ # Copyright (c) 2013-2014 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
+ require "nokogiri"
19
+
20
+ class RepositoriesInspector < Inspector
21
+ def inspect(system, description, options = {})
22
+ system.check_requirement("zypper", "--version")
23
+
24
+ xml = system.run_command(
25
+ "zypper", "--non-interactive", "--xmlout", "repos", "--details", :stdout => :capture
26
+ )
27
+ details = system.run_command(
28
+ "zypper", "--non-interactive", "repos", "--details", :stdout => :capture
29
+ ).split("\n").select { |l| l =~ /\A# +\| |\A *\d+ \| / }.
30
+ map { |l| l.split("|").map(&:strip) }
31
+
32
+ if !details.empty?
33
+ # parse and remove header
34
+ idx_prio = details.first.index("Priority")
35
+ idx_alias = details.first.index("Alias")
36
+ details.shift
37
+
38
+ prio = {}
39
+ details.each_with_index do |entry,idx|
40
+ prio[entry[idx_alias]] = entry[idx_prio].to_i
41
+ end
42
+
43
+ credentials = {}
44
+ credential_dir = "/etc/zypp/credentials.d/"
45
+ credential_files = system.run_command(
46
+ "bash", "-c",
47
+ "test -d '#{credential_dir}' && ls -1 '#{credential_dir}' || echo ''",
48
+ :stdout => :capture
49
+ )
50
+ credential_files.split("\n").each do |f|
51
+ content = system.run_command(
52
+ "cat", "/etc/zypp/credentials.d/#{f}", :stdout => :capture
53
+ )
54
+ content.match(/username=(\w*)\npassword=(\w*)/)
55
+ credentials[f] = {
56
+ username: $1,
57
+ password: $2
58
+ }
59
+ end
60
+
61
+ reps = Nokogiri::XML(xml).xpath("/stream/repo-list/repo")
62
+ summary = "Found #{reps.count} repositories."
63
+ result = reps.map do |rep|
64
+ pri_value = rep["priority"] ? rep["priority"].to_i : prio[rep["alias"]]
65
+
66
+ # NCC
67
+ rep.at_xpath("./url").text.match(/\?credentials=(\w*)/)
68
+ cred_value = $1
69
+ if cred_value && credentials[cred_value]
70
+ username = credentials[cred_value][:username]
71
+ password = credentials[cred_value][:password]
72
+ end
73
+
74
+ # SCC
75
+ rep.at_xpath("./url").text.match(/(https:\/\/updates.suse.com\/SUSE\/)/)
76
+ scc_url = $1
77
+ cred_value = "SCCcredentials"
78
+ if scc_url && credentials[cred_value]
79
+ username = credentials[cred_value][:username]
80
+ password = credentials[cred_value][:password]
81
+ end
82
+
83
+ repository = Repository.new(
84
+ alias: rep["alias"],
85
+ name: rep["name"],
86
+ type: rep["type"],
87
+ url: rep.at_xpath("./url").text,
88
+ enabled: rep["enabled"] == "1",
89
+ autorefresh: rep["autorefresh"] == "1",
90
+ gpgcheck: rep["gpgcheck"] == "1",
91
+ priority: pri_value
92
+ )
93
+ if username && password
94
+ repository[:username] = username
95
+ repository[:password] = password
96
+ end
97
+ repository
98
+ end.sort_by(&:name)
99
+ else
100
+ result = []
101
+ summary = "Found 0 repositories."
102
+ end
103
+
104
+ description.repositories = RepositoriesScope.new(result)
105
+ summary
106
+ end
107
+ end
@@ -0,0 +1,88 @@
1
+ # Copyright (c) 2013-2014 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 ServicesInspector < Inspector
19
+ def inspect(system, description, options = {})
20
+ if has_systemd(system)
21
+ result = ServicesScope.new(
22
+ init_system: "systemd",
23
+ services: inspect_systemd_services(system)
24
+ )
25
+ else
26
+ result = ServicesScope.new(
27
+ init_system: "sysvinit",
28
+ services: inspect_sysvinit_services(system)
29
+ )
30
+ end
31
+
32
+ description.services = result
33
+ @summary
34
+ end
35
+
36
+ private
37
+
38
+ def has_systemd(system)
39
+ system.run_command("systemctl", "--version")
40
+ true
41
+ rescue Cheetah::ExecutionFailed
42
+ false
43
+ end
44
+
45
+ def inspect_systemd_services(system)
46
+ output = system.run_command(
47
+ "systemctl",
48
+ "list-unit-files",
49
+ "--type=service,socket",
50
+ :stdout => :capture
51
+ )
52
+
53
+ # The first line contains a table header. The last two lines contain a
54
+ # separator and a summary (e.g. "197 unit files listed"). We also filter
55
+ # templates.
56
+ lines = output.lines[1..-3].reject { |l| l =~ /@/ }
57
+
58
+ @summary = "Found #{lines.size} services."
59
+
60
+ services = lines.map do |line|
61
+ name, state = line.split(/\s+/)
62
+
63
+ Service.new(name: name, state: state)
64
+ end
65
+
66
+ ServiceList.new(services.sort_by(&:name))
67
+ end
68
+
69
+ def inspect_sysvinit_services(system)
70
+ system.check_requirement("chkconfig", "--help")
71
+
72
+ output = system.run_command(
73
+ "chkconfig",
74
+ "--allservices",
75
+ :stdout => :capture
76
+ )
77
+
78
+ @summary = "Found #{output.lines.size} services."
79
+
80
+ services = output.lines.map do |line|
81
+ name, state = line.split(/\s+/)
82
+
83
+ Service.new(name: name, state: state)
84
+ end
85
+
86
+ ServiceList.new(services.sort_by(&:name))
87
+ end
88
+ end
@@ -0,0 +1,393 @@
1
+ # Copyright (c) 2013-2014 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 UnmanagedFilesInspector < Inspector
19
+ # checks if all required binaries are present
20
+ def check_requirements(system, check_tar)
21
+ system.check_requirement("rpm", "--version")
22
+ system.check_requirement("sed", "--version")
23
+ system.check_requirement("cat", "--version")
24
+ system.check_requirement("find", "--version")
25
+ system.check_requirement("tar", "--version") if check_tar
26
+ system.check_requirement("gzip", "--version") if check_tar
27
+ end
28
+
29
+ # extract pathes from rpm database into ruby hashes
30
+ def extract_rpm_database(system)
31
+ out = system.run_command(
32
+ ["rpm", "-qlav"],
33
+ ["sed", "s/^\\(.\\)[^/]* /\\1 /"],
34
+ :stdout => :capture
35
+ )
36
+ files = {}
37
+ dirh = {}
38
+ links = {}
39
+ # result of above command is lines with type as first character, then a path
40
+ # handled types are: "-" for normal files, "d" for directories, "l" for links
41
+ # links have " -> " with link content after path
42
+ out.each_line do |l|
43
+ type = l[0]
44
+ entry = l[2..-2]
45
+ if type == "-"
46
+ files[entry] = ""
47
+ elsif type == "d"
48
+ dirh[entry] = true
49
+ elsif type == "l"
50
+ pair = entry.split(" -> ",2)
51
+ files[pair.first] = pair[1]
52
+ end
53
+ end
54
+ files.each do |f,e|
55
+ dir, sep, file = f.rpartition("/")
56
+
57
+ # make sure that dirs leading to a managed file are treated as if they were
58
+ # in rpm database, otherwise we cannot exclude whole unmanaged trees
59
+ while( !dirh.has_key?(dir) && dir.size > 1 )
60
+ dirh[dir] = false
61
+ dir=dir[0..dir.rindex("/") - 1]
62
+ end
63
+
64
+ # put links to a managed directory also into directory hash
65
+ if !e.empty? && dirh.has_key?(e)
66
+ dirh[f] = false
67
+ end
68
+ end
69
+ Machinery.logger.debug "extract_rpm_database files:#{files.size} dirs:#{dirh.size}"
70
+ [files, dirh]
71
+ end
72
+
73
+ def check_consistency(files, dirh)
74
+ p "files=#{files.size} dirs=#{dirh.size}"
75
+ p "dirs in rpmdb=#{dirh.select{|k,e| e}.size} added:#{dirh.select{|k,e| !e}.size}"
76
+ list = files.select { |f| !f.start_with?("/") }
77
+ p "should not happen non-abs file:#{list}" unless list.empty?
78
+ list = dirh.select { |f| !f.start_with?("/") }
79
+ p "should not happen non-abs dirs:#{list}" unless list.empty?
80
+ end
81
+
82
+ def extract_unmanaged_files(system, description, files, trees, excluded, store_name)
83
+ description.remove_file_store(store_name)
84
+ description.initialize_file_store(store_name)
85
+ store_path = description.file_store(store_name)
86
+
87
+ archive_path = File.join(store_path, "files.tgz")
88
+ system.create_archive(files.join("\0"), archive_path, excluded)
89
+
90
+ trees.each do |tree|
91
+ tree_name = File.basename(tree)
92
+ parent_dir = File.dirname(tree)
93
+ sub_dir = File.join("trees", parent_dir)
94
+
95
+ description.create_file_store_sub_dir(store_name, sub_dir)
96
+ archive_path = File.join(store_path, sub_dir, "#{tree_name}.tgz")
97
+ system.create_archive(tree, archive_path, excluded)
98
+ end
99
+ end
100
+
101
+ # extract metadata from extracted tar archives and put data into Object
102
+ def extract_tar_metadata(osl, destdir)
103
+ if Dir.exists?(destdir)
104
+ tarballs = [File.join(destdir, "files.tgz")]
105
+ osl.select{ |os| os.type == "dir" }.map(&:name).each do |d|
106
+ base = File.dirname(d)
107
+ tarballs << File.join(destdir, "trees", base, "#{File.basename(d)}.tgz")
108
+ end
109
+
110
+ tarballs.each do | archive|
111
+ files = Tarball.new(archive).list
112
+
113
+ files.each do |file|
114
+ os = osl.find do |o|
115
+ o.name == "/#{file[:path]}#{file[:type] == :dir ? "/" : ""}"
116
+ end
117
+
118
+ os.user = file[:user]
119
+ os.group = file[:group]
120
+ if file[:type] != :link
121
+ os.size = file[:size]
122
+ os.mode = file[:mode]
123
+ end
124
+
125
+ # unmanaged dirs are trees and only have one entry in the manifest
126
+ if os.type == "dir"
127
+ os.size = files.map { |d| d[:size] }.reduce(:+)
128
+ os.files = files.size
129
+ break
130
+ end
131
+ end
132
+ end
133
+ end
134
+ osl
135
+ end
136
+
137
+ # find paths below dir until a certain depth is reached
138
+ def get_find_data( system, dir, depth )
139
+ dep = depth - 1
140
+ files = {}
141
+ dirs = {}
142
+ excluded_files = []
143
+
144
+ # compute command line
145
+ cmd = "find #{dir.shellescape} -xdev -maxdepth 1 -maxdepth #{depth} "
146
+ cmd += '-printf "%y\0%P\0%l\0"'
147
+
148
+ # Cheetah seems to be unable to handle binary zeroes "\0" in parameters
149
+ # misuse stdin for command
150
+ #
151
+ # Filenames can contain invalid UTF-8 characters, so we treat the data as
152
+ # binary information first while splitting the raw output and then convert
153
+ # the separate strings to UTF-8, replacing invalid characters with the
154
+ # "REPLACEMENT CHARACTER" (U+FFFD). That way we have both the raw data
155
+ # (which is needed in order to be able to access the files) and the cleaned
156
+ # string which can be safely used.
157
+ out = system.run_command(
158
+ "/bin/bash",
159
+ {
160
+ :stdin => cmd,
161
+ :stdout => :capture,
162
+ :disable_logging => true
163
+ }
164
+ ).force_encoding("binary")
165
+
166
+
167
+ # find creates three field per path
168
+ out.split("\0", -1).each_slice(3) do |type, raw_path, raw_link|
169
+ next unless raw_path && !raw_path.empty?
170
+
171
+ path = scrub(raw_path)
172
+ link = scrub(raw_link)
173
+
174
+ if [path, link].any? { |f| f.include?("\uFFFD") }
175
+ broken_names = []
176
+ if path.include?("\uFFFD")
177
+ broken_names << "filename '#{path}'"
178
+ excluded_files << raw_path
179
+ end
180
+ if link.include?("\uFFFD")
181
+ broken_names << "link target '#{link}'"
182
+ excluded_files << raw_link
183
+ end
184
+
185
+ warning = broken_names.join(" and ")
186
+ warning += " contain#{"s" if broken_names.length == 1}"
187
+ warning += " invalid UTF-8 characters. Skipping."
188
+ warning[0] = warning[0].upcase
189
+
190
+ Machinery.logger.warn(warning)
191
+ Machinery::Ui.warn(warning)
192
+ next
193
+ end
194
+
195
+ files[path] = link if type == "l"
196
+ files[path] = "" if type == "f"
197
+
198
+ # dirs at maxdepth could be non-leafs all othere are leafs
199
+ dirs[path] = path.count("/") == dep if type == "d"
200
+ end
201
+ Machinery.logger.debug "get_find_data dir:#{dir} depth:#{depth} file:#{files.size} dirs:#{dirs.size} excluded:#{excluded_files}"
202
+ [files, dirs, excluded_files]
203
+ end
204
+
205
+ def max_depth
206
+ 6
207
+ end
208
+
209
+ def start_depth
210
+ 3
211
+ end
212
+
213
+ def inspect(system, description, options = nil)
214
+ do_extract = options && options[:extract_unmanaged_files]
215
+ check_requirements(system, do_extract)
216
+
217
+ tmp_file_store = "unmanaged_files.tmp"
218
+ final_file_store = "unmanaged_files"
219
+
220
+
221
+ ignore_list = [
222
+ "tmp",
223
+ "var/tmp",
224
+ "lost+found",
225
+ "var/run",
226
+ "var/lib/rpm",
227
+ ".snapshots",
228
+ description.store.base_path.sub(/^\//, ""),
229
+ "proc"
230
+ ]
231
+
232
+ # Information about users and groups are extracted by the according inspector
233
+ ignore_list += [
234
+ "etc/passwd",
235
+ "etc/shadow",
236
+ "etc/group"
237
+ ]
238
+
239
+ # Information about services is extracted by the ServicesInspector, so
240
+ # we ignore the links representing the same information when inspecting
241
+ # unmanaged files.
242
+ ignore_list += [
243
+ "etc/init.d/boot.d",
244
+ "etc/init.d/rc0.d",
245
+ "etc/init.d/rc1.d",
246
+ "etc/init.d/rc2.d",
247
+ "etc/init.d/rc3.d",
248
+ "etc/init.d/rc4.d",
249
+ "etc/init.d/rc5.d",
250
+ "etc/init.d/rc6.d",
251
+ "etc/init.d/rcS.d"
252
+ ]
253
+
254
+ rpm_files, rpm_dirs = extract_rpm_database(system)
255
+
256
+ mount_points = MountPoints.new(system)
257
+ mounts = mount_points.local
258
+ unmanaged_files = []
259
+ unmanaged_trees = []
260
+ excluded_files = []
261
+ unmanaged_links = {}
262
+ remote_dirs = mount_points.remote
263
+ ignore_list.each do |ignore|
264
+ remote_dirs.delete_if { |e| e.start_with?(File.join("/", ignore, "/")) }
265
+ end
266
+ dirs_todo = [ "/" ]
267
+ start = start_depth
268
+ max = max_depth
269
+ find_count = 0
270
+ sub_tree_containing_remote_fs = []
271
+ excluded_files += remote_dirs
272
+
273
+ if !remote_dirs.empty?
274
+ warning = "The content of the following remote directories is ignored: #{remote_dirs.uniq.join(", ")}."
275
+ Machinery.logger.warn(warning)
276
+ Machinery::Ui.warn(warning)
277
+ end
278
+
279
+ while !dirs_todo.empty?
280
+ find_dir = dirs_todo.first
281
+
282
+ # determine files and directories below find_dir until a certain depth
283
+ depth = mounts.include?(find_dir) ? start : max
284
+ files, dirs, excluded = get_find_data( system, find_dir, depth )
285
+ excluded_files += excluded
286
+ find_count += 1
287
+ find_dir += "/" if find_dir.size > 1
288
+ if !mounts.empty?
289
+ # force all mount points to be non-leave directories (find is called with -xdev)
290
+ mounts.each do |mp|
291
+ dirs[mp] = true if dirs.has_key?(mp)
292
+ end
293
+ mounts.reject!{ |mp| dirs.has_key?(mp) }
294
+ end
295
+ if find_dir == "/"
296
+ ignore_list.each do |d|
297
+ td_with_slash = d + "/"
298
+ dirs.reject! {|p| p == d || p.start_with?(td_with_slash) }
299
+ files.reject! {|p| p == d || p.start_with?(td_with_slash) }
300
+ end
301
+ end
302
+ managed, unmanaged = dirs.keys.partition{ |d| rpm_dirs.has_key?(find_dir + d) }
303
+
304
+ # unmanaged dirs lead to removal of files and dirs below that dir
305
+ while !unmanaged.empty?
306
+ dir = unmanaged.shift
307
+
308
+ # save into list of unmanaged trees
309
+ if !remote_dirs.include?(find_dir + dir)
310
+ unmanaged_trees << find_dir + dir
311
+ end
312
+ dir = File.join(dir, "/")
313
+
314
+ # find sub trees containing remote file systems
315
+ remote_dirs.each do |remote_dir|
316
+ if unmanaged.include?(remote_dir[1..-1])
317
+ sub_tree_containing_remote_fs << remote_dir
318
+ unmanaged = unmanaged.drop_while{ |d| d.start_with?(remote_dir[1...-1]) }
319
+ end
320
+ end
321
+ # remove all possible further references starting with this subdir
322
+ unmanaged = unmanaged.drop_while{ |d| d.start_with?(dir) }
323
+ files.reject!{ |d| d.start_with?(dir) }
324
+ end
325
+
326
+ # remove all (currently known) leaf directories
327
+ managed.select!{ |d| dirs[d] }
328
+
329
+ # update list for still to handle directories
330
+ dirs_todo.shift
331
+ managed.map!{ |d| find_dir + d }
332
+ dirs_todo.push(*managed)
333
+
334
+ # unmanaged files are simply stored
335
+ managed, unmanaged = files.keys.partition{ |d| rpm_files.has_key?(find_dir + d) }
336
+ links = unmanaged.reject { |d| files[d].empty? }
337
+ unmanaged.map!{ |d| find_dir + d }
338
+ unmanaged_files.push(*unmanaged)
339
+ links.each { |d| unmanaged_links[find_dir + d] = "" }
340
+ end
341
+ Machinery.logger.debug "inspect unmanaged files find calls:#{find_count} files:#{unmanaged_files.size} trees:#{unmanaged_trees.size}"
342
+ begin
343
+ if do_extract
344
+ extract_unmanaged_files(system, description, unmanaged_files, unmanaged_trees, excluded_files, tmp_file_store)
345
+ else
346
+ description.remove_file_store(final_file_store)
347
+ end
348
+ osl = unmanaged_files.map do |p|
349
+ type = unmanaged_links.has_key?(p) ? "link" : "file"
350
+ UnmanagedFile.new(name: p, type: type)
351
+ end
352
+ osl += unmanaged_trees.map { |p| UnmanagedFile.new( name: p + "/", type: "dir") }
353
+ if do_extract
354
+ osl = extract_tar_metadata(osl, description.file_store(tmp_file_store))
355
+ description.remove_file_store(final_file_store)
356
+ description.rename_file_store(
357
+ tmp_file_store, final_file_store
358
+ )
359
+ end
360
+ rescue SignalException => e
361
+ # Handle SIGHUP(1), SIGINT(2) and SIGTERM(15) gracefully
362
+ if [1, 2, 15].include?(e.signo)
363
+ Machinery::Ui.warn "Interrupted by user. The partly extracted unmanaged-files are available" \
364
+ " under '#{description.file_store(tmp_file_store)}'"
365
+ end
366
+ raise
367
+ end
368
+ remote_dirs.each do |remote_dir|
369
+ osl << UnmanagedFile.new( name: remote_dir + "/", type: "remote_dir")
370
+ end
371
+
372
+ summary = "#{do_extract ? "Extracted" : "Found"} #{osl.size} unmanaged files and trees."
373
+ description["unmanaged_files"] = UnmanagedFilesScope.new(
374
+ extracted: !!do_extract,
375
+ files: UnmanagedFileList.new(osl.sort_by(&:name))
376
+ )
377
+ summary
378
+ end
379
+
380
+ private
381
+
382
+ # Implementation of String#scrub for Ruby < 2.1. Assumes the string is in
383
+ # UTF-8.
384
+ def scrub(s)
385
+ # We have a string in UTF-8 with possible invalid byte sequences. It turns
386
+ # out that String#encode can remove these sequences when given appropriate
387
+ # options, but just converting into UTF-8 would be a no-op. So let's convert
388
+ # into UTF-16 (which has the same character set as UTF-8) and back.
389
+ #
390
+ # See also: http://stackoverflow.com/a/21315619
391
+ s.dup.force_encoding("UTF-8").encode("UTF-16", invalid: :replace).encode("UTF-8")
392
+ end
393
+ end