machinery-tool 1.5.0 → 1.6.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/NEWS +18 -0
  3. data/html/assets/machinery.css +20 -1
  4. data/html/assets/machinery.js +53 -26
  5. data/html/index.html.haml +69 -46
  6. data/lib/analyze_config_file_diffs_task.rb +1 -1
  7. data/lib/array.rb +36 -14
  8. data/lib/cli.rb +45 -50
  9. data/lib/compare_task.rb +1 -1
  10. data/lib/config.rb +8 -2
  11. data/lib/config_base.rb +14 -1
  12. data/lib/element_filter.rb +48 -11
  13. data/lib/exceptions.rb +5 -0
  14. data/lib/filter.rb +63 -14
  15. data/lib/filter_option_parser.rb +83 -0
  16. data/lib/generate_html_task.rb +3 -1
  17. data/lib/hint.rb +36 -18
  18. data/lib/html.rb +1 -0
  19. data/lib/inspect_task.rb +12 -21
  20. data/lib/inspector.rb +13 -6
  21. data/lib/kiwi_config.rb +17 -14
  22. data/lib/list_task.rb +5 -1
  23. data/lib/local_system.rb +3 -4
  24. data/lib/logged_cheetah.rb +3 -1
  25. data/lib/machinery.rb +1 -0
  26. data/lib/object.rb +24 -19
  27. data/lib/scope_file_store.rb +3 -1
  28. data/lib/show_task.rb +5 -7
  29. data/lib/system_description.rb +11 -12
  30. data/lib/system_description_store.rb +1 -1
  31. data/lib/ui.rb +44 -36
  32. data/lib/upgrade_format_task.rb +4 -1
  33. data/lib/version.rb +1 -1
  34. data/man/generated/machinery.1.gz +0 -0
  35. data/man/generated/machinery.1.html +7 -2
  36. data/plugins/inspect/changed_managed_files_inspector.rb +13 -6
  37. data/plugins/inspect/config_files_inspector.rb +27 -20
  38. data/plugins/inspect/groups_inspector.rb +12 -4
  39. data/plugins/inspect/os_inspector.rb +29 -22
  40. data/plugins/inspect/packages_inspector.rb +13 -5
  41. data/plugins/inspect/patterns_inspector.rb +24 -10
  42. data/plugins/inspect/repositories_inspector.rb +19 -15
  43. data/plugins/inspect/services_inspector.rb +28 -22
  44. data/plugins/inspect/unmanaged_files_inspector.rb +42 -33
  45. data/plugins/inspect/users_inspector.rb +13 -5
  46. data/plugins/model/os_model.rb +1 -1
  47. metadata +3 -2
data/lib/array.rb CHANGED
@@ -22,30 +22,36 @@ module Machinery
22
22
  @element_class = options[:class]
23
23
  end
24
24
 
25
- def from_json(json)
26
- elements = json.map do |element|
27
- if @element_class
28
- @element_class.from_json(element)
25
+ def convert_element(element)
26
+ if @element_class
27
+ element.is_a?(@element_class) ? element : @element_class.from_json(element)
28
+ else
29
+ case element
30
+ when ::Array
31
+ Machinery::Array.from_json(element)
32
+ when Hash
33
+ Machinery::Object.from_json(element)
29
34
  else
30
- case element
31
- when ::Array
32
- Machinery::Array.from_json(element)
33
- when Hash
34
- Machinery::Object.from_json(element)
35
- else
36
- element
37
- end
35
+ element
38
36
  end
39
37
  end
38
+ end
40
39
 
41
- new(elements)
40
+ def convert_raw_array(array)
41
+ array.map do |element|
42
+ convert_element(element)
43
+ end
44
+ end
45
+
46
+ def from_json(json_object)
47
+ new(json_object)
42
48
  end
43
49
  end
44
50
 
45
51
  attr_reader :elements
46
52
 
47
53
  def initialize(elements = [])
48
- @elements = elements
54
+ @elements = self.class.convert_raw_array(elements)
49
55
  end
50
56
 
51
57
  def ==(other)
@@ -69,6 +75,22 @@ module Machinery
69
75
  self.class.new(@elements & other.elements)
70
76
  end
71
77
 
78
+ def push(element)
79
+ @elements.push(self.class.convert_element(element))
80
+ end
81
+
82
+ def <<(element)
83
+ @elements << self.class.convert_element(element)
84
+ end
85
+
86
+ def +(array)
87
+ self.class.new(@elements + self.class.new(array).elements)
88
+ end
89
+
90
+ def insert(index, *elements)
91
+ @elements.insert(index, *self.class.new(elements).elements)
92
+ end
93
+
72
94
  def as_json
73
95
  @elements.map do |element|
74
96
  if element.is_a?(Machinery::Array) || element.is_a?(Machinery::Object)
data/lib/cli.rb CHANGED
@@ -22,9 +22,13 @@ class Cli
22
22
  preserve_argv(true)
23
23
  @version = Machinery::VERSION + " (system description format version " +
24
24
  "#{SystemDescription::CURRENT_FORMAT_VERSION})"
25
+ @config = Machinery::Config.new
25
26
  switch :version, negatable: false, desc: "Show version"
26
27
  switch :debug, negatable: false, desc: "Enable debug mode"
27
28
  switch [:help, :h], negatable: false, desc: "Show help"
29
+ if @config.experimental_features
30
+ flag :exclude, negatable: false, desc: "Exclude elements matching the filter criteria"
31
+ end
28
32
 
29
33
  sort_help :manually
30
34
  pre do |global_options,command,options,args|
@@ -39,14 +43,15 @@ class Cli
39
43
  if command.is_a?(GLI::Commands::Help) && !global_options[:version]
40
44
 
41
45
  Machinery::Ui.puts "\nMachinery can show hints which guide through a typical workflow."
42
- if Machinery::Config.new.hints
46
+ if @config.hints
43
47
  Machinery::Ui.puts "These hints can be switched off by '#{$0} config hints off'."
44
48
  else
45
49
  Machinery::Ui.puts "These hints can be switched on by '#{$0} config hints on'."
46
50
  end
47
51
 
48
- Hint.get_started
52
+ Hint.print(:get_started)
49
53
  end
54
+ Machinery::Ui.close_pager
50
55
  end
51
56
 
52
57
  GLI::Commands::Help.skips_post = false
@@ -68,9 +73,12 @@ class Cli
68
73
  when SignalException
69
74
  Machinery.logger.info "Machinery was aborted with signal #{e.signo}."
70
75
  exit 1
76
+ when Errno::ENOSPC
77
+ Machinery::Ui.error("Error: " + e.message)
78
+ exit 1
71
79
  else
72
80
  Machinery::Ui.error "Machinery experienced an unexpected error. Please file a " \
73
- "bug report at https://github.com/SUSE/machinery/issues/new.\n"
81
+ "bug report at: https://github.com/SUSE/machinery/issues/new\n"
74
82
  if e.is_a?(Cheetah::ExecutionFailed)
75
83
  result = ""
76
84
  result << "#{e.message}\n"
@@ -208,7 +216,7 @@ class Cli
208
216
  when "config-file-diffs"
209
217
  task = AnalyzeConfigFileDiffsTask.new
210
218
  task.analyze(description)
211
- Hint.show_analyze_data(name: name)
219
+ Hint.print(:show_analyze_data, name: name)
212
220
  else
213
221
  raise Machinery::Errors::InvalidCommandLine.new(
214
222
  "The operation '#{options[:operation]}' is not supported. " \
@@ -270,6 +278,8 @@ class Cli
270
278
  desc: "Pipe output into a pager"
271
279
 
272
280
  c.action do |global_options,options,args|
281
+ Machinery::Ui.use_pager = options[:pager]
282
+
273
283
  name1 = shift_arg(args, "NAME1")
274
284
  name2 = shift_arg(args, "NAME2")
275
285
  store = system_description_store
@@ -279,8 +289,7 @@ class Cli
279
289
 
280
290
  task = CompareTask.new
281
291
  opts = {
282
- show_all: options["show-all"],
283
- no_pager: !options["pager"]
292
+ show_all: options["show-all"]
284
293
  }
285
294
  task.compare(description1, description2, scope_list, opts)
286
295
  end
@@ -428,6 +437,8 @@ class Cli
428
437
  desc: "Extract changed managed files from inspected system"
429
438
  c.switch :show, required: false, negatable: false,
430
439
  desc: "Print inspection result"
440
+ c.switch :verbose, required: false, negatable: false,
441
+ desc: "Display the filters which are used during inspection"
431
442
 
432
443
  c.action do |global_options,options,args|
433
444
  host = shift_arg(args, "HOSTNAME")
@@ -455,12 +466,12 @@ class Cli
455
466
  inspect_options[:extract_unmanaged_files] = true
456
467
  end
457
468
 
458
- if options["skip-files"] && !(scope_list & ["config_files", "changed_managed_files"]).empty?
459
- Machinery::Ui.warn("Warning: The --skip-files option is currently only supported for the " \
460
- "\"unmanaged-files\" scope")
461
- end
469
+ filter = FilterOptionParser.parse("inspect", options, global_options)
462
470
 
463
- filter = prepare_filter("inspect", options)
471
+ if options["verbose"] && !filter.empty?
472
+ Machinery::Ui.puts "\nThe following filters are applied during inspection:"
473
+ Machinery::Ui.puts filter.to_array.join("\n") + "\n\n"
474
+ end
464
475
 
465
476
  inspector_task.inspect_system(
466
477
  system_description_store,
@@ -472,10 +483,10 @@ class Cli
472
483
  inspect_options
473
484
  )
474
485
 
475
- Hint.show_data(name: name)
486
+ Hint.print(:show_data, name: name)
476
487
 
477
488
  if !options["extract-files"] || Inspector.all_scopes.count != scope_list.count
478
- Hint.do_complete_inspection(name: name, host: host)
489
+ Hint.print(:do_complete_inspection, name: name, host: host)
479
490
  end
480
491
  end
481
492
  end
@@ -545,8 +556,12 @@ class Cli
545
556
  desc: "Show diffs of configuration files changes."
546
557
  c.switch "html", required: false, negatable: false,
547
558
  desc: "Open system description in HTML format in your web browser."
559
+ c.switch "verbose", required: false, negatable: false,
560
+ desc: "Show the filters that were applied before showing the description."
548
561
 
549
562
  c.action do |global_options,options,args|
563
+ Machinery::Ui.use_pager = options["pager"]
564
+
550
565
  name = shift_arg(args, "NAME")
551
566
  if name == "localhost" && !CurrentUser.new.is_root?
552
567
  Machinery::Ui.puts "You need root rights to access the system description of your locally inspected system."
@@ -554,14 +569,28 @@ class Cli
554
569
  description = SystemDescription.load(name, system_description_store)
555
570
  scope_list = process_scope_option(options[:scope], options["exclude-scope"])
556
571
 
572
+ filter = FilterOptionParser.parse("show", options, global_options)
573
+
574
+ inspected_filters = description.filter_definitions("inspect")
575
+
576
+ if options["verbose"]
577
+ if !inspected_filters.empty?
578
+ Machinery::Ui.puts "\nThe following filters were applied during inspection:"
579
+ Machinery::Ui.puts inspected_filters.join("\n") + "\n\n"
580
+ end
581
+
582
+ if !filter.empty?
583
+ Machinery::Ui.puts "\nThe following filters were applied before showing the description:"
584
+ Machinery::Ui.puts filter.to_array.join("\n") + "\n\n"
585
+ end
586
+ end
557
587
 
558
588
  task = ShowTask.new
559
589
  opts = {
560
- no_pager: !options["pager"],
561
590
  show_diffs: options["show-diffs"],
562
591
  show_html: options["html"]
563
592
  }
564
- task.show(description, scope_list, opts)
593
+ task.show(description, scope_list, filter, opts)
565
594
  end
566
595
  end
567
596
 
@@ -639,7 +668,7 @@ class Cli
639
668
  task = ConfigTask.new
640
669
  task.config(key, value)
641
670
 
642
- if key == "hints" && !Machinery::Config.new.hints
671
+ if key == "hints" && !@config.hints
643
672
  Machinery::Ui.puts "Hints can be switched on again by '#{$0} config hints on'."
644
673
  end
645
674
  end
@@ -652,38 +681,4 @@ class Cli
652
681
  SystemDescriptionStore.new
653
682
  end
654
683
  end
655
-
656
-
657
- def self.prepare_filter(command, options)
658
- filter = Filter.from_default_definition(command)
659
-
660
- skip_files = options.delete("skip-files")
661
- if skip_files
662
- files = skip_files.split(/(?<!\\),/) # Do not split on escaped commas
663
- files = files.flat_map do |file|
664
- if file.start_with?("@")
665
- filename = File.expand_path(file[1..-1])
666
-
667
- if !File.exists?(filename)
668
- raise Machinery::Errors::MachineryError.new(
669
- "The filter file '#{filename}' does not exist."
670
- )
671
- end
672
- File.read(filename).lines.map(&:strip)
673
- else
674
- file
675
- end
676
- end
677
-
678
- files.reject!(&:empty?) # Ignore empty filters
679
- files.map! { |file| file.gsub("\\@", "@") } # Unescape escaped @s
680
- files.map! { |file| file.chomp("/") } # List directories without the trailing /, in order to
681
- # not confuse the unmanaged files inspector
682
- files.each do |file|
683
- filter.add_element_filter_from_definition("/unmanaged_files/files/name=#{file}")
684
- end
685
- end
686
-
687
- filter
688
- end
689
684
  end
data/lib/compare_task.rb CHANGED
@@ -19,7 +19,7 @@ class CompareTask
19
19
  def compare(description1, description2, scopes, options = {})
20
20
  output = render_comparison(description1, description2, scopes, options)
21
21
 
22
- Machinery::Ui.print_output(output, :no_pager => options[:no_pager])
22
+ Machinery::Ui.puts output
23
23
  end
24
24
 
25
25
  def render_comparison(description1, description2, scopes, options = {})
data/lib/config.rb CHANGED
@@ -25,8 +25,14 @@ module Machinery
25
25
  default_config_file(Machinery::DEFAULT_CONFIG_FILE)
26
26
 
27
27
  entry("hints",
28
- default: true,
29
- description: "Show hints about usage of Machinery in the context of the commands ran by the user"
28
+ default: true,
29
+ description: "Show hints about usage of Machinery in the context of the commands ran by" \
30
+ " the user"
31
+ )
32
+ entry("experimental-features",
33
+ default: false,
34
+ description: "Enable experimental features. See " \
35
+ "https://github.com/SUSE/machinery/wiki/Experimental-Features for more details."
30
36
  )
31
37
  end
32
38
  end
data/lib/config_base.rb CHANGED
@@ -32,6 +32,8 @@ class ConfigBase
32
32
  end
33
33
 
34
34
  def entry(key, parameters = {})
35
+ key = normalize_key(key)
36
+
35
37
  @entries[key] = { value: parameters[:default], description: parameters[:description] }
36
38
  create_method(key.to_sym) { get(key) }
37
39
  create_method("#{key}=".to_sym) { |value| set(key, value) }
@@ -42,15 +44,18 @@ class ConfigBase
42
44
  end
43
45
 
44
46
  def each(&block)
45
- @entries.each(&block)
47
+ @entries.map { |key, value| [unnormalize_key(key), value] }.each(&block)
46
48
  end
47
49
 
48
50
  def get(key)
51
+ key = normalize_key(key)
52
+
49
53
  ensure_config_exists(key)
50
54
  @entries[key][:value]
51
55
  end
52
56
 
53
57
  def set(key, value, options = {auto_save: true} )
58
+ key = normalize_key(key)
54
59
  ensure_config_exists(key)
55
60
 
56
61
  # Check if data type is correct. true and false are not of the same type which makes the check complex
@@ -114,4 +119,12 @@ class ConfigBase
114
119
  def create_method(name, &block)
115
120
  self.class.send(:define_method, name, &block)
116
121
  end
122
+
123
+ def normalize_key(key)
124
+ key.gsub("-", "_")
125
+ end
126
+
127
+ def unnormalize_key(key)
128
+ key.gsub("_", "-")
129
+ end
117
130
  end
@@ -18,28 +18,65 @@
18
18
  class ElementFilter
19
19
  attr_accessor :path, :matchers
20
20
 
21
- def initialize(path, matchers = nil)
21
+ def initialize(path, operator = nil, matchers = nil)
22
22
  @path = path
23
- @matchers = []
23
+ @matchers = {}
24
24
 
25
- raise("Wrong type") if ![NilClass, String, Array].include?(matchers.class)
25
+ if ![NilClass, String, Array].include?(matchers.class)
26
+ raise Machinery::Errors::InvalidFilter.new("Wrong filter type")
27
+ end
26
28
 
27
- add_matchers(matchers) if matchers
29
+ add_matchers(operator, matchers) if operator && matchers
28
30
  end
29
31
 
30
- def add_matchers(matchers)
31
- @matchers += Array(matchers)
32
+ def add_matchers(operator, matchers)
33
+ if ![Filter::OPERATOR_EQUALS, Filter::OPERATOR_EQUALS_NOT].include?(operator)
34
+ raise Machinery::Errors::InvalidFilter.new("Wrong filter operator '#{operator}'")
35
+ end
36
+
37
+ @matchers[operator] ||= []
38
+ @matchers[operator] += Array(matchers)
32
39
  end
33
40
 
34
41
  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
42
+ @matchers.each do |operator, matchers|
43
+ matchers.each do |matcher|
44
+ values_equal = case value
45
+ when Machinery::Array
46
+ value_array = value.elements
47
+
48
+ (value_array - Array(matcher)).empty? && (Array(matcher) - value_array).empty?
49
+ when String
50
+ if matcher.is_a?(Array)
51
+ exception = Machinery::Errors::ElementFilterTypeMismatch.new
52
+ exception.failed_matcher =
53
+ "#{path}#{operator}#{matcher.join(",")}"
54
+ raise exception
55
+ end
56
+
57
+ if matcher.end_with?("*")
58
+ value.start_with?(matcher[0..-2])
59
+ else
60
+ value == matcher
61
+ end
62
+ end
63
+ if operator == Filter::OPERATOR_EQUALS
64
+ return true if values_equal
65
+ elsif operator == Filter::OPERATOR_EQUALS_NOT
66
+ return true if !values_equal
67
+ end
40
68
  end
41
69
  end
42
70
 
43
71
  false
44
72
  end
73
+
74
+ def filters_scope?(scope)
75
+ (path =~ /\/#{scope}(\/|$)/) ? true : false
76
+ end
77
+
78
+ def ==(other)
79
+ path == other.path &&
80
+ matchers == other.matchers
81
+ end
45
82
  end
data/lib/exceptions.rb CHANGED
@@ -101,6 +101,11 @@ module Machinery
101
101
 
102
102
  class MigrationError < MachineryError; end
103
103
 
104
+ class InvalidFilter < MachineryError; end
105
+ class ElementFilterTypeMismatch < MachineryError
106
+ attr_accessor :failed_matcher
107
+ end
108
+
104
109
  class BuildFailed < MachineryError; end
105
110
  class DeployFailed < MachineryError; end
106
111
  class InspectionFailed < MachineryError; end
data/lib/filter.rb CHANGED
@@ -46,19 +46,22 @@
46
46
  class Filter
47
47
  attr_accessor :element_filters
48
48
 
49
+ OPERATOR_EQUALS = "="
50
+ OPERATOR_EQUALS_NOT = "!="
51
+
49
52
  def self.parse_filter_definitions(filter_definitions)
50
53
  element_filters = {}
51
54
  Array(filter_definitions).each do |definition|
52
- path, matcher_definition = definition.split("=", 2)
55
+ path, operator, matcher_definition = definition.scan(/([a-zA-Z_\/]+)(.*=)(.*)/)[0]
53
56
 
54
57
  element_filters[path] ||= ElementFilter.new(path)
55
58
  if matcher_definition.index(",")
56
59
  matchers = matcher_definition.split(/(?<!\\),/)
57
60
  matchers.map! { |matcher| matcher.gsub("\\,", ",") } # Unescape escaped commas
58
61
 
59
- element_filters[path].add_matchers([matchers])
62
+ element_filters[path].add_matchers(operator, [matchers])
60
63
  else
61
- element_filters[path].add_matchers(matcher_definition)
64
+ element_filters[path].add_matchers(operator, matcher_definition)
62
65
  end
63
66
  end
64
67
 
@@ -86,24 +89,44 @@ class Filter
86
89
  end
87
90
 
88
91
  def add_element_filter_from_definition(filter_definition)
89
- new_element_filters = Filter.parse_filter_definitions(filter_definition)
92
+ add_element_filters(Filter.parse_filter_definitions(filter_definition).values)
93
+ end
90
94
 
91
- new_element_filters.each do |path, element_filter|
95
+ def add_element_filters(element_filters)
96
+ Array(element_filters).each do |element_filter|
97
+ path = element_filter.path
92
98
  @element_filters[path] ||= ElementFilter.new(path)
93
- @element_filters[path].add_matchers(element_filter.matchers)
99
+
100
+ element_filter.matchers.each do |operator, matchers|
101
+ @element_filters[path].add_matchers(operator, matchers)
102
+ end
103
+ end
104
+ end
105
+
106
+ def element_filter_for(path)
107
+ element_filters[path]
108
+ end
109
+
110
+ def element_filters_for_scope(scope)
111
+ @element_filters.values.select do |element_filter|
112
+ element_filter.filters_scope?(scope)
94
113
  end
95
114
  end
96
115
 
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)
116
+ def set_element_filters_for_scope(scope, element_filters)
117
+ @element_filters.reject! do |_path, element_filter|
118
+ element_filter.filters_scope?(scope)
119
+ end
120
+
121
+ add_element_filters(element_filters)
101
122
  end
102
123
 
103
124
  def to_array
104
125
  @element_filters.flat_map do |path, element_filter|
105
- element_filter.matchers.map do |matcher|
106
- "#{path}=#{Array(matcher).join(",")}"
126
+ element_filter.matchers.flat_map do |operator, matchers|
127
+ matchers.map do |matcher|
128
+ "#{path}#{operator}#{Array(matcher).join(",")}"
129
+ end
107
130
  end
108
131
  end
109
132
  end
@@ -115,7 +138,33 @@ class Filter
115
138
  filter.matches?(value)
116
139
  end
117
140
 
118
- def element_filter_for(path)
119
- element_filters[path]
141
+ def apply!(system_description)
142
+ element_filters.each do |path, element_filter|
143
+ steps = path.split("/").reject(&:empty?)
144
+ target = steps.pop
145
+
146
+ pointer = system_description
147
+ container = nil
148
+ steps.each do |step|
149
+ break if !pointer
150
+ pointer = pointer[step]
151
+ container ||= pointer if pointer.is_a?(Machinery::Array)
152
+ end
153
+
154
+ next if !pointer
155
+
156
+ begin
157
+ pointer.delete_if do |element|
158
+ element_filter.matches?(element[target])
159
+ end
160
+ rescue Machinery::Errors::ElementFilterTypeMismatch => e
161
+ Machinery::Ui.warn("Warning: Filter '#{e.failed_matcher}' tries to match an array, " \
162
+ "but the according element is not an array.")
163
+ end
164
+ end
165
+ end
166
+
167
+ def empty?
168
+ element_filters.empty?
120
169
  end
121
170
  end