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/object.rb CHANGED
@@ -29,10 +29,12 @@ module Machinery
29
29
  value.is_a?(property_class) ? value : property_class.from_json(value)
30
30
  else
31
31
  case value
32
- when ::Array
33
- Machinery::Array.from_json(value)
34
32
  when Hash
35
- Machinery::Object.from_json(value)
33
+ if value.keys.include?("_elements")
34
+ Machinery::Array.from_json(value)
35
+ else
36
+ Machinery::Object.from_json(value)
37
+ end
36
38
  else
37
39
  value
38
40
  end
data/lib/remote_system.rb CHANGED
@@ -16,16 +16,23 @@
16
16
  # you may find current contact information at www.suse.com
17
17
 
18
18
  class RemoteSystem < System
19
- attr_accessor :host
20
- attr_accessor :remote_user
19
+ attr_reader :host, :remote_user, :ssh_port, :ssh_identity_file
21
20
 
22
21
  def type
23
22
  "remote"
24
23
  end
25
24
 
26
- def initialize(host, remote_user = "root")
25
+ def initialize(host, opts = {})
26
+ options = {
27
+ remote_user: "root",
28
+ ssh_port: nil,
29
+ ssh_identity_file: nil
30
+ }.merge(opts)
31
+
27
32
  @host = host
28
- @remote_user = remote_user
33
+ @remote_user = options[:remote_user]
34
+ @ssh_port = options[:ssh_port]
35
+ @ssh_identity_file = options[:ssh_identity_file]
29
36
 
30
37
  connect
31
38
  end
@@ -78,12 +85,13 @@ class RemoteSystem < System
78
85
 
79
86
  sudo = ["sudo", "-n"] if options[:privileged] && remote_user != "root"
80
87
  cmds = [
81
- "ssh", "#{remote_user}@#{host}", "-o", "LogLevel=ERROR", sudo, "LANGUAGE=",
82
- "LC_ALL=#{locale}", *piped_args, options
88
+ *build_command(:ssh), "#{remote_user}@#{host}", "-o", \
89
+ "LogLevel=ERROR", sudo, "LANGUAGE=", "LC_ALL=#{locale}", *piped_args, options
83
90
  ].compact.flatten
91
+
84
92
  cheetah_class.run(*cmds)
85
93
  rescue Cheetah::ExecutionFailed => e
86
- if e.stderr.include?("password is required")
94
+ if e.stderr && e.stderr.include?("password is required")
87
95
  raise Machinery::Errors::InsufficientPrivileges.new(remote_user, host)
88
96
  else
89
97
  raise e
@@ -93,7 +101,8 @@ class RemoteSystem < System
93
101
  # Tries to run the noop-command(:) on the remote system as root (without a password or passphrase)
94
102
  # and raises an Machinery::Errors::SshConnectionFailed exception when it's not successful.
95
103
  def connect
96
- LoggedCheetah.run "ssh", "-q", "-o", "BatchMode=yes", "#{remote_user}@#{host}", ":"
104
+ LoggedCheetah.run(*build_command(:ssh), "-q", "-o", "BatchMode=yes",
105
+ "#{remote_user}@#{host}", ":")
97
106
  rescue Cheetah::ExecutionFailed
98
107
  raise Machinery::Errors::SshConnectionFailed.new(
99
108
  "Could not establish SSH connection to host '#{host}'. Please make sure that " \
@@ -117,7 +126,7 @@ class RemoteSystem < System
117
126
 
118
127
  cmd = [
119
128
  "rsync",
120
- "-e", "ssh",
129
+ "-e", build_command(:ssh).join(" "),
121
130
  "--chmod=go-rwx",
122
131
  "--files-from=-",
123
132
  "--rsync-path=#{rsync_path}",
@@ -153,7 +162,7 @@ class RemoteSystem < System
153
162
  destination = "#{remote_user}@#{host}:#{destination}"
154
163
 
155
164
  cmd = [
156
- "scp",
165
+ *build_command(:scp),
157
166
  source,
158
167
  destination
159
168
  ]
@@ -175,4 +184,28 @@ class RemoteSystem < System
175
184
  "Could not remove file '#{file}' on host '#{host}'.\nError: #{e}"
176
185
  )
177
186
  end
187
+
188
+ private
189
+
190
+ def build_command(name)
191
+ raise Machinery::Errors::MachineryError.new("You must set one of these flags in " \
192
+ "build_command: :ssh or :scp") unless [:ssh, :scp].include?(name)
193
+
194
+ command = [name.to_s]
195
+
196
+ if name == :ssh && @ssh_port
197
+ command.push("-p")
198
+ command.push(@ssh_port.to_s)
199
+ elsif name == :scp && @ssh_port
200
+ command.push("-P")
201
+ command.push(@ssh_port.to_s)
202
+ end
203
+
204
+ if @ssh_identity_file
205
+ command.push("-i")
206
+ command.push(@ssh_identity_file)
207
+ end
208
+
209
+ command
210
+ end
178
211
  end
data/lib/renderer.rb CHANGED
@@ -147,7 +147,7 @@ class Renderer
147
147
  end
148
148
 
149
149
  def render_comparison_only_in(description)
150
- return if !description[scope]
150
+ return if !description[scope] || description[scope].elements.try(:empty?)
151
151
 
152
152
  puts "Only in '#{description.name}':"
153
153
  indent { compare_content_only_in(description) }
@@ -208,7 +208,7 @@ class Renderer
208
208
  print_indented "#{s}"
209
209
  end
210
210
 
211
- def list(name = nil, &block)
211
+ def list(name = nil, options = {}, &block)
212
212
  unless block_given?
213
213
  raise InvalidStructureError.new(
214
214
  "'list' was called without a block"
@@ -217,7 +217,6 @@ class Renderer
217
217
 
218
218
  @stack << :list
219
219
 
220
-
221
220
  if name && !name.empty?
222
221
  print_indented "#{name}:"
223
222
  indent do
@@ -226,7 +225,7 @@ class Renderer
226
225
  else
227
226
  block.call
228
227
  end
229
- @buffer += "\n" unless @buffer.end_with?("\n\n")
228
+ @buffer += "\n" unless @buffer.end_with?("\n\n") || options[:sublist]
230
229
 
231
230
  @stack.pop
232
231
  end
data/lib/rpm_database.rb CHANGED
@@ -15,193 +15,17 @@
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 RpmDatabase
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 changed_files(&block)
37
- return @changed_files if @changed_files
38
- check_requirements
39
-
40
- out = @system.run_script_with_progress("changed_files.sh", &block)
41
- result = out.each_line.map do |line|
42
- line.chomp!
43
- next unless line.match(/^[^ ]+[ ]+. \/.*$/)
44
-
45
- file, changes, type = parse_rpm_changes_line(line)
46
-
47
- package = @system.run_command("rpm", "-qf", file, stdout: :capture).split.first
48
- package_name, package_version = package.scan(/(.*)-([^-]*)-[^-]/).first
49
-
50
- ChangedFile.new(
51
- type,
52
- name: file,
53
- package_name: package_name,
54
- package_version: package_version,
55
- status: "changed",
56
- changes: changes
57
- )
58
- end.compact.uniq
59
-
60
- paths = result.reject { |f| f.changes == Machinery::Array.new(["deleted"]) }.map(&:name)
61
- path_data = get_path_data(paths)
62
- result.each do |pkg|
63
- next unless path_data[pkg.name]
64
-
65
- path_data[pkg.name].each do |key, value|
66
- pkg[key] = value
67
- end
68
- end
69
-
70
- @changed_files = result
71
- end
72
-
73
- def expected_tag?(character, position)
74
- if @rpm_changes[position] == character
75
- true
76
- else
77
- @unknown_tag ||= ![".", "?"].include?(@rpm_changes[position])
78
- false
79
- end
80
- end
81
-
82
- def parse_rpm_changes_line(line)
83
- # rpm provides lines per config file where first 9 characters indicate which
84
- # properties of the file are modified
85
- @rpm_changes, *fields = line.split(" ")
86
- # nine rpm changes are known
87
- @unknown_tag = @rpm_changes.size > 9
88
-
89
- # For config or documentation files there's an additional field which
90
- # contains "c" or "d"
91
- type = fields[0].start_with?("/") ? "" : fields.shift
92
- path = fields.join(" ")
93
-
94
- changes = []
95
- if @rpm_changes == "missing"
96
- changes << "deleted"
97
- elsif @rpm_changes == "........." && path.end_with?(" (replaced)")
98
- changes << "replaced"
99
- path.slice!(/ \(replaced\)$/)
100
- else
101
- changes << "size" if expected_tag?("S", 0)
102
- changes << "mode" if expected_tag?("M", 1)
103
- changes << "md5" if expected_tag?("5", 2)
104
- changes << "device_number" if expected_tag?("D", 3)
105
- changes << "link_path" if expected_tag?("L", 4)
106
- changes << "user" if expected_tag?("U", 5)
107
- changes << "group" if expected_tag?("G", 6)
108
- changes << "time" if expected_tag?("T", 7)
109
- changes << "capabilities" if @rpm_changes.size > 8 && expected_tag?("P", 8)
110
- end
111
-
112
- if @unknown_tag
113
- changes << "other_rpm_changes"
114
- end
115
-
116
- if @rpm_changes.include?("?")
117
- message = "Could not perform all tests on rpm changes for file '#{path}'."
118
- Machinery.logger.warn(message)
119
- Machinery::Ui.warn("Warning: #{message}")
120
- end
121
-
122
- [path, changes, type]
123
- end
124
-
125
- def parse_stat_line(line)
126
- mode, user, group, uid, gid, type, *path_line = line.split(":")
127
- path = path_line.join(":").chomp
128
-
129
- user = uid if user == "UNKNOWN"
130
- group = gid if group == "UNKNOWN"
131
-
132
- type = case type
133
- when "directory"
134
- "dir"
135
- when "symbolic link"
136
- "link"
137
- when /file$/
138
- "file"
139
- else
140
- raise(
141
- "The inspection failed because of the unknown type `#{type}` of file `#{path}`."
142
- )
143
- end
144
-
145
- [path,
146
- {
147
- mode: mode,
148
- user: user,
149
- group: group,
150
- type: type
151
- }
152
- ]
153
- end
154
-
155
- def get_link_target(link)
156
- @system.run_command(
157
- "find", link, "-prune", "-printf", "%l",
158
- stdout: :capture,
159
- privileged: true
160
- ).strip
161
- end
162
-
163
- # get path data for list of files
164
- # cur_files is guaranteed to not exceed max command line length
165
- def get_file_properties(cur_files)
166
- ret = {}
167
- out = @system.run_command(
168
- "stat", "--printf", "%a:%U:%G:%u:%g:%F:%n\\n",
169
- *cur_files,
170
- stdout: :capture,
171
- privileged: true
172
- )
173
- out.each_line do |l|
174
- path, values = parse_stat_line(l)
175
- ret[path] = values
176
- ret[path][:target] = get_link_target(path) if values[:type] == "link"
177
- end
178
- ret
18
+ class RpmDatabase < ManagedFilesDatabase
19
+ def managed_files_list(&block)
20
+ @system.run_script_with_progress("changed_files.sh", &block)
179
21
  end
180
22
 
181
- def get_path_data(paths)
182
- ret = {}
183
- path_index = 0
184
- # arbitrary number for maximum command line length that should always work
185
- max_len = 50000
186
- cur_files = []
187
- cur_len = 0
188
- while path_index < paths.size
189
- if cur_files.empty? || paths[path_index].size + cur_len + 1 < max_len
190
- cur_files << paths[path_index]
191
- cur_len += paths[path_index].size + 1
192
- path_index += 1
193
- else
194
- ret.merge!(get_file_properties(cur_files))
195
- cur_files.clear
196
- cur_len = 0
197
- end
198
- end
199
- ret.merge!(get_file_properties(cur_files)) unless cur_files.empty?
200
- ret
23
+ def package_for_file_path(file)
24
+ package = @system.run_command("rpm", "-qf", file, stdout: :capture).split.first
25
+ package_name, package_version = package.scan(/(.*)-([^-]*)-[^-]/).first
26
+ [package_name, package_version]
201
27
  end
202
28
 
203
- private
204
-
205
29
  def check_requirements
206
30
  @system.check_requirement("rpm", "--version")
207
31
  @system.check_requirement("stat", "--version")
@@ -22,7 +22,7 @@ module ScopeFileAccessArchive
22
22
  FileUtils.cp(File.join(scope_file_store.path, "files.tgz"), destination)
23
23
 
24
24
  target = File.join(destination, "trees")
25
- files.select(&:directory?).each do |system_file|
25
+ self.select(&:directory?).each do |system_file|
26
26
  raise Machinery::Errors::FileUtilsError if !system_file.directory?
27
27
 
28
28
  tarball_target = File.join(target, File.dirname(system_file.name))
@@ -33,8 +33,8 @@ module ScopeFileAccessArchive
33
33
  end
34
34
 
35
35
  def has_file?(name)
36
- return true if files.any? { |file| file.name == name }
37
- if files.any? { |file| file.name == File.join(File.dirname(name), "") }
36
+ return true if any? { |file| file.name == name }
37
+ if any? { |file| file.name == File.join(File.dirname(name), "") }
38
38
  tgz_file = File.join(scope_file_store.path, "trees", "#{File.dirname(name)}.tgz")
39
39
  return Cheetah.run("tar", "ztf", tgz_file, stdout: :capture).split(/\n/).
40
40
  any? { |f| "/#{f}" == name }
@@ -34,6 +34,6 @@ module ScopeFileAccessFlat
34
34
  end
35
35
 
36
36
  def has_file?(name)
37
- return true if files.any? { |file| file.name == name }
37
+ return true if any? { |file| file.name == name }
38
38
  end
39
39
  end
data/lib/server.rb CHANGED
@@ -88,6 +88,11 @@ class Server < Sinatra::Base
88
88
  object.length.to_s + " " + Machinery.pluralize(object.length, singular, plural)
89
89
  end
90
90
 
91
+ def repository_changes
92
+ klass = @diff["repositories"].changed.first.first.class
93
+ changed_elements("repositories", attributes: klass.attributes, key: klass.key)
94
+ end
95
+
91
96
  def changed_elements(scope, opts)
92
97
  optional_attributes = opts[:optional_attributes] || []
93
98
 
@@ -199,7 +204,7 @@ class Server < Sinatra::Base
199
204
  description = SystemDescription.load(params[:id], settings.system_description_store)
200
205
  filename = File.join("/", params["splat"].first)
201
206
 
202
- file = description[params[:scope]].files.find { |f| f.name == filename }
207
+ file = description[params[:scope]].find { |f| f.name == filename }
203
208
 
204
209
  if request.accept.first.to_s == "text/plain" && file.binary?
205
210
  status 406
@@ -298,7 +303,7 @@ class Server < Sinatra::Base
298
303
  diffs_dir = @description.scope_file_store("analyze/config_file_diffs").path
299
304
  if @description.config_files && diffs_dir
300
305
  # Enrich description with the config file diffs
301
- @description.config_files.files.each do |file|
306
+ @description.config_files.each do |file|
302
307
  path = File.join(diffs_dir, file.name + ".diff")
303
308
  file.diff = diff_to_object(File.read(path)) if File.exists?(path)
304
309
  end
data/lib/system.rb CHANGED
@@ -33,9 +33,9 @@ class System
33
33
 
34
34
  attr_writer :locale
35
35
 
36
- def self.for(host, remote_user = "root")
36
+ def self.for(host, opts = {})
37
37
  if host && host != "localhost"
38
- RemoteSystem.new(host, remote_user)
38
+ RemoteSystem.new(host, opts)
39
39
  else
40
40
  LocalSystem.new
41
41
  end
@@ -113,14 +113,61 @@ class System
113
113
  # Machinery::Ui.progress("Found #{count} changed files...")
114
114
  # end
115
115
  def run_script_with_progress(*script, &callback)
116
+ run_with_progress(*script, :script, &callback)
117
+ end
118
+
119
+ def run_command_with_progress(*command, &callback)
120
+ run_with_progress(*command, :command, &callback)
121
+ end
122
+
123
+ def has_command?(command)
124
+ run_command("bash", "-c", "type -P #{command}", stdout: :capture)
125
+ true
126
+ rescue Cheetah::ExecutionFailed
127
+ false
128
+ end
129
+
130
+ def arch
131
+ run_command("uname", "-m", stdout: :capture).chomp
132
+ end
133
+
134
+ def locale
135
+ @locale || "C"
136
+ end
137
+
138
+ def managed_files_database
139
+ if @managed_files_database
140
+ return @managed_files_database
141
+ elsif has_command?("rpm")
142
+ @managed_files_database = RpmDatabase.new(self)
143
+ elsif has_command?("dpkg")
144
+ @managed_files_database = DpkgDatabase.new(self)
145
+ else
146
+ raise Machinery::Errors::MissingRequirement.new(
147
+ "Need binary 'rpm' or 'dpkg' to be available on the inspected system."
148
+ )
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def run_with_progress(*command, type, &callback)
116
155
  output = ""
117
156
  error = ""
118
157
  write_io = StringIO.new(output, "a")
119
158
  error_io = StringIO.new(error, "a")
120
159
  read_io = StringIO.new(output, "r")
121
160
 
161
+ options = command.last.is_a?(Hash) ? command.pop : {}
162
+ options[:stdout] = write_io
163
+ options[:stderr] = error_io
164
+
122
165
  inspect_thread = Thread.new do
123
- run_script(*script, stdout: write_io, stderr: error_io)
166
+ if type == :script
167
+ run_script(*command, options)
168
+ else
169
+ run_command(*command, options)
170
+ end
124
171
  end
125
172
 
126
173
  while inspect_thread.alive?
@@ -135,23 +182,4 @@ class System
135
182
 
136
183
  output
137
184
  end
138
-
139
- def has_command?(command)
140
- run_command("bash", "-c", "type -P #{command}", stdout: :capture)
141
- true
142
- rescue Cheetah::ExecutionFailed
143
- false
144
- end
145
-
146
- def arch
147
- run_command("uname", "-m", stdout: :capture).chomp
148
- end
149
-
150
- def locale
151
- @locale || "C"
152
- end
153
-
154
- def rpm_database
155
- @rpm_database ||= RpmDatabase.new(self)
156
- end
157
185
  end