machinery-tool 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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