machinery-tool 1.4.0 → 1.5.0

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/NEWS +7 -0
  3. data/bin/machinery +3 -1
  4. data/helpers/default_filters.json +24 -0
  5. data/helpers/yum_repositories.py +8 -2
  6. data/lib/autoyast.rb +8 -3
  7. data/lib/changed_rpm_files_helper.rb +34 -7
  8. data/lib/cli.rb +149 -101
  9. data/lib/element_filter.rb +45 -0
  10. data/lib/exceptions.rb +3 -2
  11. data/lib/filter.rb +121 -0
  12. data/lib/inspect_task.rb +25 -4
  13. data/lib/kiwi_config.rb +11 -0
  14. data/lib/list_task.rb +53 -26
  15. data/lib/local_system.rb +1 -1
  16. data/lib/machinery.rb +2 -0
  17. data/lib/manifest.rb +3 -2
  18. data/lib/migration.rb +2 -2
  19. data/lib/system_description.rb +33 -4
  20. data/lib/tarball.rb +5 -5
  21. data/lib/version.rb +1 -1
  22. data/lib/zypper.rb +12 -17
  23. data/man/generated/machinery.1.gz +0 -0
  24. data/man/generated/machinery.1.html +21 -4
  25. data/plugins/docs/changed_managed_files.md +3 -1
  26. data/plugins/docs/config_files.md +3 -2
  27. data/plugins/inspect/changed_managed_files_inspector.rb +8 -2
  28. data/plugins/inspect/config_files_inspector.rb +1 -1
  29. data/plugins/inspect/groups_inspector.rb +3 -1
  30. data/plugins/inspect/os_inspector.rb +2 -2
  31. data/plugins/inspect/packages_inspector.rb +1 -1
  32. data/plugins/inspect/patterns_inspector.rb +1 -1
  33. data/plugins/inspect/repositories_inspector.rb +1 -1
  34. data/plugins/inspect/services_inspector.rb +1 -1
  35. data/plugins/inspect/unmanaged_files_inspector.rb +17 -42
  36. data/plugins/inspect/users_inspector.rb +1 -1
  37. data/plugins/schema/v3/system-description-changed-managed-files.schema.json +1 -1
  38. data/plugins/schema/v3/system-description-config-files.schema.json +1 -1
  39. data/plugins/schema/v3/system-description-repositories.schema.json +1 -1
  40. data/schema/v3/system-description-global.schema.json +12 -0
  41. metadata +5 -2
@@ -0,0 +1,45 @@
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 ElementFilter
19
+ attr_accessor :path, :matchers
20
+
21
+ def initialize(path, matchers = nil)
22
+ @path = path
23
+ @matchers = []
24
+
25
+ raise("Wrong type") if ![NilClass, String, Array].include?(matchers.class)
26
+
27
+ add_matchers(matchers) if matchers
28
+ end
29
+
30
+ def add_matchers(matchers)
31
+ @matchers += Array(matchers)
32
+ end
33
+
34
+ def matches?(value)
35
+ @matchers.each do |matcher|
36
+ if matcher.end_with?("*")
37
+ return true if value.start_with?(matcher[0..-2])
38
+ else
39
+ return true if value == matcher
40
+ end
41
+ end
42
+
43
+ false
44
+ end
45
+ end
@@ -38,10 +38,11 @@ module Machinery
38
38
  class SystemDescriptionNotFound < SystemDescriptionError; end
39
39
 
40
40
  class SystemDescriptionIncompatible < SystemDescriptionError
41
- attr_reader :name
41
+ attr_reader :name, :format_version
42
42
 
43
- def initialize(name)
43
+ def initialize(name, format_version)
44
44
  @name = name
45
+ @format_version = format_version
45
46
  end
46
47
 
47
48
  def to_s
@@ -0,0 +1,121 @@
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
+ # The Filter class is used to hold the information about the filter conditions
19
+ # that should be applied during certain Machinery commands.
20
+ #
21
+ # Filters are usually created by passing a filter definition string to the
22
+ # constructor, e.g.
23
+ #
24
+ # filter = Filter.new("/unmanaged_files/files/name=/opt")
25
+ #
26
+ # Existing filters can be extended by amending the definition:
27
+ #
28
+ # filter.add_element_filter_from_definition("/unmanaged_files/files/name=/srv")
29
+ #
30
+ # or by adding ElementFilters directly:
31
+ #
32
+ # element_filter = ElementFilter.new("/unmanaged_files/files/name", ["/opt", "/srv"])
33
+ # filter.add_element_filter(element_filter)
34
+ #
35
+ #
36
+ # The actual filtering can be done by passing values to Filter#matches?
37
+ #
38
+ # filter = Filter.new("/unmanaged_files/files/name=/opt*")
39
+ # filter.matches?("/unmanaged_files/files/name", "/opt/foo")
40
+ # => true
41
+ # filter.matches?("/unmanaged_files/files/name", "/srv/bar")
42
+ # => false
43
+ #
44
+ # More details about how the filter work can be found at
45
+ # https://github.com/SUSE/machinery/blob/master/docs/Filtering-Design.md
46
+ class Filter
47
+ attr_accessor :element_filters
48
+
49
+ def self.parse_filter_definitions(filter_definitions)
50
+ element_filters = {}
51
+ Array(filter_definitions).each do |definition|
52
+ path, matcher_definition = definition.split("=", 2)
53
+
54
+ element_filters[path] ||= ElementFilter.new(path)
55
+ if matcher_definition.index(",")
56
+ matchers = matcher_definition.split(/(?<!\\),/)
57
+ matchers.map! { |matcher| matcher.gsub("\\,", ",") } # Unescape escaped commas
58
+
59
+ element_filters[path].add_matchers([matchers])
60
+ else
61
+ element_filters[path].add_matchers(matcher_definition)
62
+ end
63
+ end
64
+
65
+ element_filters
66
+ end
67
+
68
+ def self.from_default_definition(command)
69
+ filter = Filter.new
70
+
71
+ default_filters_file = File.join(Machinery::ROOT, "helpers/default_filters.json")
72
+ if File.exists?(default_filters_file)
73
+ default_filters = JSON.parse(File.read(default_filters_file))
74
+ if default_filters[command]
75
+ default_filters[command].each do |definition|
76
+ filter.add_element_filter_from_definition(definition)
77
+ end
78
+ end
79
+ end
80
+
81
+ filter
82
+ end
83
+
84
+ def initialize(definitions = [])
85
+ @element_filters = Filter.parse_filter_definitions(definitions)
86
+ end
87
+
88
+ def add_element_filter_from_definition(filter_definition)
89
+ new_element_filters = Filter.parse_filter_definitions(filter_definition)
90
+
91
+ new_element_filters.each do |path, element_filter|
92
+ @element_filters[path] ||= ElementFilter.new(path)
93
+ @element_filters[path].add_matchers(element_filter.matchers)
94
+ end
95
+ end
96
+
97
+ def add_element_filter(element_filter)
98
+ path = element_filter.path
99
+ @element_filters[path] ||= ElementFilter.new(path)
100
+ @element_filters[path].add_matchers(element_filter.matchers)
101
+ end
102
+
103
+ def to_array
104
+ @element_filters.flat_map do |path, element_filter|
105
+ element_filter.matchers.map do |matcher|
106
+ "#{path}=#{Array(matcher).join(",")}"
107
+ end
108
+ end
109
+ end
110
+
111
+ def matches?(path, value)
112
+ filter = element_filter_for(path)
113
+ return false if !filter
114
+
115
+ filter.matches?(value)
116
+ end
117
+
118
+ def element_filter_for(path)
119
+ element_filters[path]
120
+ end
121
+ end
@@ -16,11 +16,12 @@
16
16
  # you may find current contact information at www.suse.com
17
17
 
18
18
  class InspectTask
19
- def inspect_system(store, host, name, current_user, scopes, options = {})
19
+ def inspect_system(store, host, name, current_user, scopes, filter, options = {})
20
20
  system = System.for(host)
21
21
  check_root(system, current_user)
22
22
 
23
- description, failed_inspections = build_description(store, name, system, scopes, options)
23
+ description, failed_inspections = build_description(store, name, system,
24
+ scopes, filter, options)
24
25
 
25
26
  if !description.attributes.empty?
26
27
  print_description(description, scopes) if options[:show]
@@ -57,7 +58,17 @@ class InspectTask
57
58
  end
58
59
  end
59
60
 
60
- def build_description(store, name, system, scopes, options)
61
+ def adapt_filter_in_metadata(filter_in_metadata, scope, filter)
62
+ filter_in_metadata.element_filters.
63
+ reject! { |path, _filter| path.start_with?("/#{scope}") }
64
+ filter.element_filters.
65
+ select { |path, _filter| path.start_with?("/#{scope}") }.
66
+ each do |_path, element_filter|
67
+ filter_in_metadata.add_element_filter(element_filter)
68
+ end
69
+ end
70
+
71
+ def build_description(store, name, system, scopes, filter, options)
61
72
  begin
62
73
  description = SystemDescription.load(name, store)
63
74
  rescue Machinery::Errors::SystemDescriptionNotFound
@@ -72,17 +83,27 @@ class InspectTask
72
83
 
73
84
  failed_inspections = {}
74
85
 
86
+ if description.filters["inspect"]
87
+ filter_in_metadata = description.filters["inspect"]
88
+ else
89
+ filter_in_metadata = Filter.new
90
+ end
91
+
75
92
  scopes.map { |s| Inspector.for(s) }.each do |inspector|
76
93
  Machinery::Ui.puts "Inspecting #{Machinery::Ui.internal_scope_list_to_string(inspector.scope)}..."
77
94
  begin
78
- summary = inspector.inspect(system, description, options)
95
+ summary = inspector.inspect(system, description, filter, options)
79
96
  rescue Machinery::Errors::MachineryError => e
80
97
  Machinery::Ui.puts " -> Inspection failed!"
81
98
  failed_inspections[inspector.scope] = e
82
99
  next
83
100
  end
84
101
  description[inspector.scope].set_metadata(timestring, host)
102
+
103
+ adapt_filter_in_metadata(filter_in_metadata, inspector.scope, filter)
104
+
85
105
  if !description.attributes.empty?
106
+ description.set_filter("inspect", filter_in_metadata)
86
107
  description.save
87
108
  end
88
109
  Machinery::Ui.puts " -> " + summary
@@ -221,6 +221,7 @@ EOF
221
221
 
222
222
  def apply_repositories(xml)
223
223
  if @system_description.repositories
224
+ usable_repositories = false
224
225
  @system_description.repositories.each do |repo|
225
226
  # workaround kiwi issue by replacing spaces
226
227
  # the final image is not affected because the repositories are added by the config.sh
@@ -231,6 +232,7 @@ EOF
231
232
  end
232
233
  # only use accessible repositories as source for kiwi build
233
234
  if repo.enabled && !repo.type.nil? && !repo.external_medium?
235
+ usable_repositories = true
234
236
  xml.repository(parameters) do
235
237
  xml.source(path: repo.url)
236
238
  end
@@ -244,6 +246,15 @@ EOF
244
246
  @sh << "zypper -n mr --priority=#{repo.priority} '#{repo.name}'\n"
245
247
  end
246
248
  end
249
+ if !usable_repositories
250
+ raise(
251
+ Machinery::Errors::MissingRequirement.new(
252
+ "The system description doesn't contain any enabled or network reachable repository." \
253
+ " Please make sure that there is at least one accessible repository with all the" \
254
+ " required packages."
255
+ )
256
+ )
257
+ end
247
258
  end
248
259
  end
249
260
 
@@ -20,45 +20,72 @@ class ListTask
20
20
  descriptions = store.list.sort
21
21
 
22
22
  descriptions.each do |name|
23
- name = File.basename(name)
24
23
  begin
25
24
  description = SystemDescription.load(name, store, skip_validation: true)
25
+ rescue Machinery::Errors::SystemDescriptionIncompatible => e
26
+ if !e.format_version
27
+ show_error("#{name}: incompatible format version. Can not be upgraded.\n", options)
28
+ elsif e.format_version < SystemDescription::CURRENT_FORMAT_VERSION
29
+ show_error("#{name}: format version #{e.format_version}, " \
30
+ "needs to be upgraded.\n", options)
31
+ else
32
+ show_error("#{name}: format version #{e.format_version}. " \
33
+ "Please upgrade Machinery to the latest version.\n", options)
34
+ end
35
+ next
36
+ rescue Machinery::Errors::SystemDescriptionValidationFailed
37
+ show_error("#{name}: This description is broken. Use " \
38
+ "`#{$0} validate #{name}` to see the error message.\n", options)
39
+ next
26
40
  rescue Machinery::Errors::SystemDescriptionError
27
- Machinery::Ui.puts " #{name}:\n"
28
- Machinery::Ui.puts " This description has an incompatible data format or is broken.\n" \
29
- " Use `#{$0} validate #{name}` to see the error message.\n\n"
41
+ show_error("#{name}: This description is broken.\n", options)
30
42
  next
31
43
  end
32
- scopes = []
33
44
 
34
- description.scopes.each do |scope|
35
- entry = Machinery::Ui.internal_scope_list_to_string(scope)
36
- if SystemDescription::EXTRACTABLE_SCOPES.include?(scope)
37
- if description.scope_extracted?(scope)
38
- entry += " (extracted)"
39
- else
40
- entry += " (not extracted)"
45
+ if options[:short]
46
+ Machinery::Ui.puts name
47
+ else
48
+ scopes = []
49
+
50
+ description.scopes.each do |scope|
51
+ entry = Machinery::Ui.internal_scope_list_to_string(scope)
52
+ if SystemDescription::EXTRACTABLE_SCOPES.include?(scope)
53
+ if description.scope_extracted?(scope)
54
+ entry += " (extracted)"
55
+ else
56
+ entry += " (not extracted)"
57
+ end
41
58
  end
42
- end
43
59
 
44
- if options["verbose"]
45
- meta = description[scope].meta
46
- if meta
47
- time = Time.parse(meta.modified).getlocal
48
- date = time.strftime "%Y-%m-%d %H:%M:%S"
49
- hostname = meta.hostname
50
- else
51
- date = "unknown"
52
- hostname = "Unknown hostname"
60
+ if options[:verbose]
61
+ meta = description[scope].meta
62
+ if meta
63
+ time = Time.parse(meta.modified).getlocal
64
+ date = time.strftime "%Y-%m-%d %H:%M:%S"
65
+ hostname = meta.hostname
66
+ else
67
+ date = "unknown"
68
+ hostname = "Unknown hostname"
69
+ end
70
+ entry += "\n Host: [#{hostname}]"
71
+ entry += "\n Date: (#{date})"
53
72
  end
54
- entry += "\n Host: [#{hostname}]"
55
- entry += "\n Date: (#{date})"
73
+
74
+ scopes << entry
56
75
  end
57
76
 
58
- scopes << entry
77
+ Machinery::Ui.puts " #{name}:\n * " + scopes .join("\n * ") + "\n\n"
59
78
  end
79
+ end
80
+ end
81
+
82
+ private
60
83
 
61
- Machinery::Ui.puts " #{name}:\n * " + scopes .join("\n * ") + "\n\n"
84
+ def show_error(error_message, options)
85
+ if options[:short]
86
+ Machinery::Ui.puts(error_message)
87
+ else
88
+ Machinery::Ui.puts(" " + error_message + "\n")
62
89
  end
63
90
  end
64
91
  end
@@ -24,7 +24,7 @@ class LocalSystem < System
24
24
  description = SystemDescription.new("localhost",
25
25
  SystemDescriptionMemoryStore.new)
26
26
  inspector = OsInspector.new
27
- inspector.inspect(System.for("localhost"), description)
27
+ inspector.inspect(System.for("localhost"), description, nil)
28
28
  @@os = description.os
29
29
  end
30
30
  @@os
@@ -87,6 +87,8 @@ require_relative "scope_file_store"
87
87
  require_relative "json_validator"
88
88
  require_relative "json_validation_error_cleaner"
89
89
  require_relative "file_validator"
90
+ require_relative "element_filter"
91
+ require_relative "filter"
90
92
 
91
93
  Dir[File.join(Machinery::ROOT, "plugins", "**", "*.rb")].each { |f| require(f) }
92
94
 
@@ -55,7 +55,7 @@ class Manifest
55
55
 
56
56
  errors = JsonValidator.new(@hash).validate
57
57
  if !errors.empty?
58
- raise Machinery::Errors::SystemDescriptionError.new(errors.join("\n"))
58
+ raise Machinery::Errors::SystemDescriptionValidationFailed.new(errors)
59
59
  end
60
60
  end
61
61
 
@@ -74,7 +74,6 @@ class Manifest
74
74
 
75
75
  # remove needless json error information
76
76
  lines[0].gsub!(/^\d+: (.*)$/, "\\1")
77
- json_error = lines[0..block_end].join("\n")
78
77
 
79
78
  if error_pos == 1
80
79
  json_error = "An opening bracket, a comma or quotation is missing " \
@@ -84,6 +83,8 @@ class Manifest
84
83
  error_pos = nil
85
84
  end
86
85
 
86
+ json_error ||= lines[0..block_end].join("\n")
87
+
87
88
  error = "The JSON data of the system description '#{name}' " \
88
89
  "couldn't be parsed. The following error occured"
89
90
  error += " around line #{error_pos}" if error_pos
@@ -75,13 +75,13 @@ class Migration
75
75
  Machinery::Ui.warn("Warning: System Description validation errors:")
76
76
  Machinery::Ui.warn(errors)
77
77
  else
78
- raise Machinery::Errors::SystemDescriptionError.new(errors.join("\n"))
78
+ raise Machinery::Errors::SystemDescriptionValidationFailed.new(errors)
79
79
  end
80
80
  end
81
81
 
82
82
  current_version = hash["meta"]["format_version"]
83
83
  if !current_version
84
- raise Machinery::Errors::SystemDescriptionError.new(
84
+ raise Machinery::Errors::SystemDescriptionIncompatible.new(
85
85
  "The system description '#{description_name}' was generated by an old " \
86
86
  "version of machinery that is not supported by the upgrade mechanism."
87
87
  )
@@ -36,6 +36,7 @@ class SystemDescription < Machinery::Object
36
36
  attr_accessor :name
37
37
  attr_accessor :store
38
38
  attr_accessor :format_version
39
+ attr_accessor :filters
39
40
 
40
41
  class << self
41
42
  # Load the system description with the given name
@@ -89,14 +90,27 @@ class SystemDescription < Machinery::Object
89
90
 
90
91
  def from_hash(name, store, hash)
91
92
  begin
93
+ json_format_version = hash["meta"]["format_version"] if hash["meta"]
92
94
  description = SystemDescription.new(name, store, create_scopes(hash))
93
- rescue NameError
94
- raise Machinery::Errors::SystemDescriptionIncompatible.new(name)
95
+ rescue NameError, TypeError
96
+ if json_format_version && json_format_version != SystemDescription::CURRENT_FORMAT_VERSION
97
+ raise Machinery::Errors::SystemDescriptionIncompatible.new(name, json_format_version)
98
+ else
99
+ raise Machinery::Errors::SystemDescriptionError.new
100
+ end
95
101
  end
96
102
 
97
- json_format_version = hash["meta"]["format_version"] if hash["meta"]
98
103
  description.format_version = json_format_version
99
104
 
105
+ if hash["meta"] && hash["meta"]["filters"]
106
+ hash["meta"]["filters"].each do |command, filter_definitions|
107
+ description.filters[command] = Filter.new
108
+ filter_definitions.each do |definition|
109
+ description.filters[command].add_element_filter_from_definition(definition)
110
+ end
111
+ end
112
+ end
113
+
100
114
  description
101
115
  end
102
116
 
@@ -125,6 +139,7 @@ class SystemDescription < Machinery::Object
125
139
  @name = name
126
140
  @store = store
127
141
  @format_version = CURRENT_FORMAT_VERSION
142
+ @filters = {}
128
143
 
129
144
  super(hash)
130
145
  end
@@ -136,7 +151,7 @@ class SystemDescription < Machinery::Object
136
151
 
137
152
  def validate_format_compatibility
138
153
  if !compatible?
139
- raise Machinery::Errors::SystemDescriptionIncompatible.new(self.name)
154
+ raise Machinery::Errors::SystemDescriptionIncompatible.new(name, format_version)
140
155
  end
141
156
  end
142
157
 
@@ -161,6 +176,10 @@ class SystemDescription < Machinery::Object
161
176
  attributes.keys.each do |key|
162
177
  meta[key] = self[key].meta.as_json if self[key].meta
163
178
  end
179
+ @filters.each do |command, filter|
180
+ meta["filters"] ||= {}
181
+ meta["filters"][command] = filter.to_array
182
+ end
164
183
 
165
184
  hash = as_json
166
185
  hash["meta"] = meta unless meta.empty?
@@ -180,6 +199,16 @@ class SystemDescription < Machinery::Object
180
199
  File.chmod(0600, path) if created
181
200
  end
182
201
 
202
+ def set_filter(command, filter)
203
+ if !["inspect"].include?(command)
204
+ raise Machinery::Errors::MachineryError.new(
205
+ "Storing the filter for command '#{command}' is not supported."
206
+ )
207
+ end
208
+
209
+ @filters[command] = filter
210
+ end
211
+
183
212
  def scopes
184
213
  attributes.keys.map(&:to_s).sort
185
214
  end