machinery-tool 1.9.1 → 1.10.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/NEWS +7 -0
  3. data/html/assets/compare/machinery-compare.js +43 -0
  4. data/html/assets/compare/machinery.js +17 -0
  5. data/html/assets/machinery.css +13 -0
  6. data/html/assets/modal.js +280 -0
  7. data/html/assets/show/machinery-show.js +16 -9
  8. data/html/assets/show/machinery.js +61 -25
  9. data/html/comparison.html.haml +194 -116
  10. data/html/index.html.haml +46 -12
  11. data/lib/array.rb +2 -1
  12. data/lib/cli.rb +44 -23
  13. data/lib/compare_task.rb +5 -29
  14. data/lib/comparison.rb +69 -0
  15. data/lib/config.rb +4 -0
  16. data/lib/export_task.rb +7 -0
  17. data/lib/file_scope.rb +2 -2
  18. data/lib/html.rb +165 -88
  19. data/lib/machinery.rb +6 -2
  20. data/lib/{generate_html_task.rb → man_task.rb} +8 -6
  21. data/lib/object.rb +1 -1
  22. data/lib/renderer.rb +70 -41
  23. data/lib/{scope_file_access.rb → scope_file_access_archive.rb} +23 -20
  24. data/lib/scope_file_access_flat.rb +36 -0
  25. data/lib/serve_html_task.rb +33 -0
  26. data/lib/show_task.rb +19 -5
  27. data/lib/system_file.rb +25 -0
  28. data/lib/version.rb +1 -1
  29. data/man/generated/machinery.1.gz +0 -0
  30. data/man/generated/machinery.1.html +60 -34
  31. data/plugins/changed_managed_files/changed_managed_files_renderer.rb +5 -5
  32. data/plugins/config_files/config_files_renderer.rb +7 -6
  33. data/plugins/groups/groups_renderer.rb +3 -3
  34. data/plugins/os/os_renderer.rb +5 -5
  35. data/plugins/packages/packages_model.rb +25 -0
  36. data/plugins/packages/packages_renderer.rb +36 -5
  37. data/plugins/patterns/patterns_renderer.rb +3 -3
  38. data/plugins/repositories/repositories_model.rb +1 -1
  39. data/plugins/repositories/repositories_renderer.rb +4 -4
  40. data/plugins/services/services_model.rb +2 -2
  41. data/plugins/services/services_renderer.rb +3 -3
  42. data/plugins/unmanaged_files/unmanaged_files_model.rb +2 -1
  43. data/plugins/unmanaged_files/unmanaged_files_renderer.rb +5 -5
  44. data/plugins/users/users_renderer.rb +3 -3
  45. metadata +36 -4
data/html/index.html.haml CHANGED
@@ -2,7 +2,7 @@
2
2
  %html{"ng-app" => "machinery-show"}
3
3
  %head
4
4
  %title
5
- = description.name + " - Machinery System Description"
5
+ = description_name + " - Machinery System Description"
6
6
  %meta{:charset => 'utf-8'}/
7
7
  %link{:href => "assets/machinery-base.css", :rel => "stylesheet", :type => "text/css"}/
8
8
  %link{:href => "assets/machinery.css", :rel => "stylesheet", :type => "text/css"}/
@@ -12,6 +12,7 @@
12
12
  %script{:src => "assets/jquery-2.1.1.min.js"}
13
13
  %script{:src => "assets/transition.js"}
14
14
  %script{:src => "assets/collapse.js"}
15
+ %script{:src => "assets/modal.js"}
15
16
 
16
17
  %script#inspection_details_template{:type => "text/ng-template"}
17
18
  #inspection_details.hidden
@@ -248,13 +249,13 @@
248
249
  %tbody
249
250
  %tr{"ng-repeat" => "service in description.services.services | filter:query"}
250
251
  %td {{service.name}}
251
- %td{:class=>"{{services.init_system}}_{{state}}"}
252
+ %td{:class=>"{{description.services.init_system}}_{{service.state}}"}
252
253
  {{service.state}}
253
254
 
254
255
  %script#scope_config_files.partial{:type => "text/ng-template"}
255
256
  %div{"ng-show" => "description.config_files.files"}
256
257
  %a.scope_anchor{:id => "config_files"}
257
- #config_files_container.scope
258
+ #config_files_container.scope{"data-scope" => "config_files"}
258
259
  %scope-header{ |
259
260
  :summary => "'#{scope_help('config_files')}'", |
260
261
  :logo => "'assets/logo-config-files.png'", |
@@ -289,7 +290,11 @@
289
290
  %tbody
290
291
  %tr{"ng-repeat" => "file in description.config_files.files | filter:query"}
291
292
  %td
292
- {{file.name}}
293
+ %span{"ng-switch" => true, "on" => "file.downloadable"}
294
+ %a.file-download{"ng-switch-when" => "true", "href" => "#"}
295
+ {{file.name}}
296
+ %span{"ng-switch-default" => true}
297
+ {{file.name}}
293
298
  %a.diff-toggle{"ng-show" => "file.diff", "data-config-file" => "{{file.name}}", "data-toggle" => "popover"}
294
299
  Show diff
295
300
  %td {{file.package_name}}
@@ -304,7 +309,7 @@
304
309
  %script#scope_changed_managed_files.partial{:type => "text/ng-template"}
305
310
  %div{"ng-show" => "description.changed_managed_files.files"}
306
311
  %a.scope_anchor{:id => "changed_managed_files"}
307
- #changed_managed_files_container.scope
312
+ #changed_managed_files_container.scope{"data-scope" => "changed_managed_files"}
308
313
  %scope-header{ |
309
314
  :summary => "'#{scope_help('changed_managed_files')}'", |
310
315
  :logo => "'assets/logo-changed-managed-files.png'", |
@@ -329,7 +334,11 @@
329
334
  %th Group
330
335
  %tbody
331
336
  %tr{"ng-repeat" => "file in description.changed_managed_files.files | filter:query"}
332
- %td {{file.name}}
337
+ %td{"ng-switch" => true, "on" => "file.downloadable"}
338
+ %a.file-download{"ng-switch-when" => "true", "href" => "#"}
339
+ {{file.name}}
340
+ %span{"ng-switch-default" => true}
341
+ {{file.name}}
333
342
  %td {{file.package_name}}
334
343
  %td {{file.package_version}}
335
344
  %td
@@ -342,7 +351,7 @@
342
351
  %script#scope_unmanaged_files.partial{:type => "text/ng-template"}
343
352
  %div{"ng-show" => "description.unmanaged_files.files"}
344
353
  %a.scope_anchor{:id => "unmanaged_files"}
345
- #unmanaged_files_container.scope
354
+ #unmanaged_files_container.scope{"data-scope" => "unmanaged_files"}
346
355
  %scope-header{ |
347
356
  :summary => "'#{scope_help('unmanaged_files')}'", |
348
357
  :logo => "'assets/logo-unmanaged-files.png'", |
@@ -362,22 +371,47 @@
362
371
  %th Type
363
372
  %tbody
364
373
  %tr{"ng-repeat" => "file in description.unmanaged_files.files | filter:query"}
365
- %td {{file.name}}
374
+ %td{"ng-switch" => true, "on" => "file.downloadable"}
375
+ %a.file-download{"ng-switch-when" => "true", "href" => "#"}
376
+ {{file.name}}
377
+ %span{"ng-switch-default" => true}
378
+ {{file.name}}
366
379
  %td {{file.type}}
367
380
 
368
- %script{:src => "assets/description.js"}
369
381
  %script{:src => "assets/show/machinery.js"}
370
382
  %script{:src => "assets/bootstrap-tooltip.js"}
371
383
  %script{:src => "assets/bootstrap-popover.js"}
372
384
 
373
- %body
385
+ %body{"data-description" => description_name}
386
+ #file-modal.modal.fade
387
+ .modal-dialog.modal-lg
388
+ .modal-content
389
+ .modal-header
390
+ %button.close{"type" => "button", "data-dismiss" => "modal"}
391
+ ×
392
+ %h4#file-modal-title
393
+ .modal-body
394
+ %textarea#file-modal-file-content{"readonly" => "true"}
395
+ #file-modal-error{"style" => "display: none"}
396
+ .modal-footer
397
+ %a#file-modal-download-link.btn.btn-success{"target" => "_blank"}
398
+ Download
399
+ %button.btn.btn-primary{"type" => "button", "data-dismiss" => "modal"}
400
+ Close
401
+
402
+ #file_popover{:style => "display: none"}
403
+ .header
404
+ %h3.name
405
+ .body
406
+ %textarea.content
407
+
374
408
  .container-fluid
375
409
  #nav-bar
376
410
  .row
377
411
  .col-xs-1
378
412
  .col-xs-10
379
413
  %h1
380
- System '#{description.name}'
414
+ System '#{description_name}'
381
415
 
382
416
  %a.inspection_details{:href => "#", "data-toggle" => "popover"} (inspection details)
383
417
  .row
@@ -386,7 +420,7 @@
386
420
  Expand all
387
421
  %a#collapse-all{:href => "#"}
388
422
  Collapse all
389
- .col-xs-10
423
+ .col-xs-9
390
424
  %small.pull-right.pad-top
391
425
  created by
392
426
  %a{:href => "http://machinery-project.org", :target => "_blank"}
data/lib/array.rb CHANGED
@@ -113,8 +113,9 @@ module Machinery
113
113
  [
114
114
  self - other,
115
115
  other - self,
116
+ [],
116
117
  self & other
117
- ].map { |e| !e.empty? ? e : nil }
118
+ ].map { |e| e.empty? ? nil : e }
118
119
  end
119
120
 
120
121
  def method_missing(name, *args, &block)
data/lib/cli.rb CHANGED
@@ -86,6 +86,9 @@ class Cli
86
86
  Machinery::Ui.kill_pager
87
87
 
88
88
  case e
89
+ when GLI::MissingRequiredArgumentsException
90
+ Machinery::Ui.error("Option --" + e.message)
91
+ exit 1
89
92
  when GLI::UnknownCommandArgument, GLI::UnknownGlobalArgument,
90
93
  GLI::UnknownCommand, GLI::BadCommandLine, OptionParser::MissingArgument
91
94
  Machinery::Ui.error e.to_s + "\n\n"
@@ -359,7 +362,6 @@ class Cli
359
362
  end
360
363
 
361
364
 
362
-
363
365
  desc "Deploy image to OpenStack cloud"
364
366
  long_desc <<-LONGDESC
365
367
  Deploy system description as image to OpenStack cloud.
@@ -421,8 +423,6 @@ class Cli
421
423
  end
422
424
  end
423
425
 
424
-
425
-
426
426
  desc "Export system description as AutoYaST profile"
427
427
  long_desc <<-LONGDESC
428
428
  Export system description as AutoYaST profile
@@ -450,8 +450,6 @@ class Cli
450
450
  end
451
451
  end
452
452
 
453
-
454
-
455
453
  desc "Inspect running system"
456
454
  long_desc <<-LONGDESC
457
455
  Inspect running system and generate system descripton from inspected data.
@@ -566,6 +564,18 @@ class Cli
566
564
  end
567
565
  end
568
566
 
567
+ desc "Shows man page"
568
+ long_desc <<-LONGDESC
569
+ Shows the man page of the machinery tool.
570
+
571
+ LONGDESC
572
+ command :man do |c|
573
+ c.action do
574
+ task = ManTask.new
575
+ task.man
576
+ end
577
+ end
578
+
569
579
 
570
580
 
571
581
  desc "Remove system descriptions"
@@ -605,6 +615,10 @@ class Cli
605
615
  desc: "Show specified scopes", arg_name: "SCOPE_LIST"
606
616
  c.flag ["exclude-scope", :e], type: String, required: false,
607
617
  desc: "Exclude specified scopes", arg_name: "SCOPE_LIST"
618
+ c.flag [:port, :p], type: Integer, required: false, default_value: @config.http_server_port,
619
+ desc: "Listen on port PORT", arg_name: "PORT"
620
+ c.flag [:ip, :i], type: String, required: false, default_value: "127.0.0.1",
621
+ desc: "Listen on ip address IP", arg_name: "IP"
608
622
  c.switch "pager", required: false, default_value: true,
609
623
  desc: "Pipe output into a pager"
610
624
  c.switch "show-diffs", required: false, negatable: false,
@@ -642,8 +656,10 @@ class Cli
642
656
 
643
657
  task = ShowTask.new
644
658
  opts = {
645
- show_diffs: options["show-diffs"],
646
- show_html: options["html"]
659
+ show_diffs: options["show-diffs"],
660
+ show_html: options["html"],
661
+ ip: options["ip"],
662
+ port: options["port"]
647
663
  }
648
664
  task.show(description, scope_list, filter, opts)
649
665
  end
@@ -691,21 +707,6 @@ class Cli
691
707
  end
692
708
  end
693
709
 
694
- desc "Generate an HTML view for a system description"
695
- long_desc <<-LONGDESC
696
- Generates an HTML view for a system description.
697
- LONGDESC
698
- arg "NAME"
699
- command "generate-html" do |c|
700
- c.action do |global_options,options,args|
701
- name = shift_arg(args, "NAME")
702
-
703
- description = SystemDescription.load(name, system_description_store)
704
- task = GenerateHtmlTask.new
705
- task.generate(description)
706
- end
707
- end
708
-
709
710
  desc "Show or change machinery's configuration"
710
711
  long_desc <<-LONGDESC
711
712
  Show or change machinery's configuration.
@@ -731,12 +732,32 @@ class Cli
731
732
  task = ConfigTask.new
732
733
  task.config(key, value)
733
734
 
734
- if key == "hints" && !@config.hints
735
+ if key == "hints" && (value == "false" || value == "off")
735
736
  Machinery::Ui.puts "Hints can be switched on again by '#{$0} config hints=on'."
736
737
  end
737
738
  end
738
739
  end
739
740
 
741
+ desc "Start a webserver serving an HTML view of a system description"
742
+ long_desc <<-LONGDESC
743
+ Starts a web server which serves an HTML view for the given system description.
744
+ LONGDESC
745
+ arg "NAME"
746
+ command "serve" do |c|
747
+ c.flag [:port, :p], type: Integer, required: false, default_value: 7585,
748
+ desc: "Listen on port PORT", arg_name: "PORT"
749
+ c.flag [:ip, :i], type: String, required: false, default_value: "127.0.0.1",
750
+ desc: "Listen on ip IP", arg_name: "IP"
751
+
752
+ c.action do |_global_options, options, args|
753
+ name = shift_arg(args, "NAME")
754
+
755
+ description = SystemDescription.load(name, system_description_store)
756
+ task = ServeHtmlTask.new
757
+ task.serve(description, options[:ip], options[:port])
758
+ end
759
+ end
760
+
740
761
  def self.system_description_store
741
762
  if ENV.has_key?("MACHINERY_DIR")
742
763
  SystemDescriptionStore.new(ENV["MACHINERY_DIR"])
data/lib/compare_task.rb CHANGED
@@ -36,8 +36,8 @@ class CompareTask
36
36
 
37
37
  scopes.each do |scope|
38
38
  if description1[scope] && description2[scope]
39
- comparison = description1[scope].compare_with(description2[scope])
40
- diff[scope] = comparison.map { |scope| scope.as_json if scope }
39
+ comparison = Comparison.compare_scope(description1, description2, scope)
40
+ diff[scope] = comparison.as_json
41
41
  end
42
42
  end
43
43
 
@@ -54,37 +54,13 @@ class CompareTask
54
54
  identical = true
55
55
  identical_scopes = []
56
56
  common_scopes = false
57
- store = description1.store
58
57
  scopes.each do |scope|
59
58
  if description1[scope] && description2[scope]
60
- comparison = description1[scope].compare_with(description2[scope])
59
+ comparison = Comparison.compare_scope(description1, description2, scope)
61
60
 
62
- partial_description1 = SystemDescription.new(
63
- description1.name,
64
- store,
65
- scope => comparison[0]
66
- )
67
-
68
- partial_description2 = SystemDescription.new(
69
- description2.name,
70
- store,
71
- scope => comparison[1]
72
- )
73
-
74
- partial_description_common = SystemDescription.new(
75
- "common",
76
- store,
77
- scope => comparison[2]
78
- )
79
-
80
- output += Renderer.for(scope).render_comparison(
81
- partial_description1,
82
- partial_description2,
83
- partial_description_common,
84
- options
85
- )
61
+ output += Renderer.for(scope).render_comparison(comparison, options)
86
62
 
87
- if partial_description1[scope] || partial_description2[scope]
63
+ if comparison.only_in1 || comparison.only_in2 || comparison.changed
88
64
  identical = false
89
65
  else
90
66
  identical_scopes << scope
data/lib/comparison.rb ADDED
@@ -0,0 +1,69 @@
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 Comparison
19
+ attr_accessor :name1, :name2, :only_in1, :only_in2, :changed, :common, :store, :scope
20
+
21
+ def self.compare_scope(description1, description2, scope)
22
+ result = new
23
+ result.store = description1.store
24
+ result.scope = scope
25
+ result.name1 = description1.name
26
+ result.name2 = description2.name
27
+
28
+ if !description1[scope]
29
+ result.only_in2 = description2[scope]
30
+ elsif !description2[scope]
31
+ result.only_in1 = description1[scope]
32
+ else
33
+ result.only_in1, result.only_in2, result.changed, result.common =
34
+ description1[scope].compare_with(description2[scope])
35
+ end
36
+
37
+ result
38
+ end
39
+
40
+ def as_description(which)
41
+ case which
42
+ when :one
43
+ name = name1
44
+ data = only_in1
45
+ when :two
46
+ name = name2
47
+ data = only_in2
48
+ when :common
49
+ name = "common"
50
+ data = common
51
+ else
52
+ raise "'which' has to be :one, :two or :common"
53
+ end
54
+
55
+ SystemDescription.new(name, store, scope => data)
56
+ end
57
+
58
+ def as_json
59
+ json = {}
60
+ json["only_in1"] = only_in1.as_json if only_in1
61
+ json["only_in2"] = only_in2.as_json if only_in2
62
+ if changed
63
+ json["changed"] = changed.map { |elements| [elements.first.as_json, elements.last.as_json] }
64
+ end
65
+ json["common"] = common.as_json if common
66
+
67
+ json
68
+ end
69
+ end
data/lib/config.rb CHANGED
@@ -42,6 +42,10 @@ module Machinery
42
42
  default: true,
43
43
  description: "Check whether the current platform is supported by Machinery"
44
44
  )
45
+ entry("http_server_port",
46
+ default: 7585,
47
+ description: "TCP port used by the HTTP server for the HTML view"
48
+ )
45
49
  end
46
50
  end
47
51
  end
data/lib/export_task.rb CHANGED
@@ -24,6 +24,13 @@ class ExportTask
24
24
  @exporter.system_description.assert_scopes("os")
25
25
  @exporter.system_description.validate_export_compatibility
26
26
 
27
+ ["unmanaged_files", "changed_managed_files", "config_files"].each do |scope|
28
+ if @exporter.system_description[scope] &&
29
+ !@exporter.system_description.scope_extracted?(scope)
30
+ raise Machinery::Errors::MissingExtractedFiles.new(@exporter.system_description, [scope])
31
+ end
32
+ end
33
+
27
34
  output_dir = File.join(output_dir, @exporter.export_name)
28
35
  if File.exists?(output_dir)
29
36
  if options[:force]
data/lib/file_scope.rb CHANGED
@@ -29,7 +29,7 @@ class FileScope < Machinery::Object
29
29
  only_self = nil if only_self.empty?
30
30
  only_other = nil if only_other.empty?
31
31
  shared = nil if shared.empty?
32
- [only_self, only_other, shared]
32
+ [only_self, only_other, nil, shared]
33
33
  end
34
34
 
35
35
  private
@@ -57,7 +57,7 @@ class FileScope < Machinery::Object
57
57
  end
58
58
 
59
59
  def compare_files(other, only_self, only_other, shared)
60
- own_files, other_files, shared_files = files.compare_with(other.files)
60
+ own_files, other_files, _changed, shared_files = files.compare_with(other.files)
61
61
 
62
62
  only_self.files = own_files if own_files
63
63
  only_other.files = other_files if other_files
data/lib/html.rb CHANGED
@@ -16,39 +16,177 @@
16
16
  # you may find current contact information at www.suse.com
17
17
 
18
18
  class Html
19
- def self.escape_javascript(object)
20
- object.gsub(/([\\'"])/, "\\\\\\1")
19
+ module Helpers
20
+ def scope_help(scope)
21
+ text = File.read(File.join(Machinery::ROOT, "plugins", "#{scope}/#{scope}.md"))
22
+ Kramdown::Document.new(text).to_html
23
+ end
24
+
25
+ def diff_to_object(diff)
26
+ diff = Machinery.scrub(diff)
27
+ lines = diff.lines[2..-1]
28
+ diff_object = {
29
+ file: diff[/--- a(.*)/, 1],
30
+ additions: lines.select { |l| l.start_with?("+") }.length,
31
+ deletions: lines.select { |l| l.start_with?("-") }.length
32
+ }
33
+
34
+ original_line_number = 0
35
+ new_line_number = 0
36
+ diff_object[:lines] = lines.map do |line|
37
+ line = ERB::Util.html_escape(line.chomp).
38
+ gsub("\\", "&#92;").
39
+ gsub("\t", "&nbsp;" * 8)
40
+ case line
41
+ when /^@.*/
42
+ entry = {
43
+ type: "header",
44
+ content: line
45
+ }
46
+ original_line_number = line[/-(\d+)/, 1].to_i
47
+ new_line_number = line[/\+(\d+)/, 1].to_i
48
+ when /^ .*/, ""
49
+ entry = {
50
+ type: "common",
51
+ new_line_number: new_line_number,
52
+ original_line_number: original_line_number,
53
+ content: line[1..-1]
54
+ }
55
+ new_line_number += 1
56
+ original_line_number += 1
57
+ when /^\+.*/
58
+ entry = {
59
+ type: "addition",
60
+ new_line_number: new_line_number,
61
+ content: line[1..-1]
62
+ }
63
+ new_line_number += 1
64
+ when /^\-.*/
65
+ entry = {
66
+ type: "deletion",
67
+ original_line_number: original_line_number,
68
+ content: line[1..-1]
69
+ }
70
+ original_line_number += 1
71
+ end
72
+
73
+ entry
74
+ end
75
+
76
+ diff_object
77
+ end
21
78
  end
22
79
 
23
- def self.generate(description)
24
- template = Haml::Engine.new(
25
- File.read(File.join(Machinery::ROOT, "html", "index.html.haml"))
26
- )
27
- target = description.store.description_path(description.name)
28
-
29
- diffs_dir = description.scope_file_store("analyze/config_file_diffs").path
30
- if description.config_files && diffs_dir
31
- # Enrich description with the config file diffs
32
- description.config_files.files.each do |file|
33
- path = File.join(diffs_dir, file.name + ".diff")
34
- file.diff = diff_to_object(File.read(path)) if File.exists?(path)
80
+ # this is required for the #generate_comparison method which renders a HAML template manually with
81
+ # the local binding, so it expects the helper methods to be available in Html. It can be removed
82
+ # once the comparison was move to the webserver approach as well
83
+ extend Helpers
84
+
85
+ # Creates a new thread running a sinatra webserver which serves the local system descriptions
86
+ # The Thread object is returned so that the caller can `.join` it until it's finished.
87
+ def self.run_server(system_description_store, opts)
88
+ Thread.new do
89
+ require "sinatra/base"
90
+ require "mimemagic"
91
+
92
+ server = Sinatra.new do
93
+ set :port, opts[:port] || 7585
94
+ set :bind, opts[:ip] || "localhost"
95
+ set :public_folder, File.join(Machinery::ROOT, "html")
96
+
97
+ helpers Helpers
98
+
99
+ get "/descriptions/:id.js" do
100
+ description = SystemDescription.load(params[:id], system_description_store)
101
+ diffs_dir = description.scope_file_store("analyze/config_file_diffs").path
102
+ if description.config_files && diffs_dir
103
+ # Enrich description with the config file diffs
104
+ description.config_files.files.each do |file|
105
+ path = File.join(diffs_dir, file.name + ".diff")
106
+ file.diff = diff_to_object(File.read(path)) if File.exists?(path)
107
+ end
108
+ end
109
+
110
+ # Enrich file information with downloadable flag
111
+ ["config_files", "changed_managed_files", "unmanaged_files"].each do |scope|
112
+ description[scope].files.each do |file|
113
+ file.downloadable = file.on_disk?
114
+ end
115
+ end
116
+
117
+ description.to_hash.to_json
118
+ end
119
+
120
+ get "/descriptions/:id/files/:scope/*" do
121
+ description = SystemDescription.load(params[:id], system_description_store)
122
+ filename = File.join("/", params["splat"].first)
123
+
124
+ file = description[params[:scope]].files.find { |f| f.name == filename }
125
+
126
+ if request.accept.first.to_s == "text/plain" && file.binary?
127
+ status 406
128
+ return "binary file"
129
+ end
130
+
131
+ content = file.content
132
+ type = MimeMagic.by_path(filename) || MimeMagic.by_magic(content) || "text/plain"
133
+
134
+ content_type type
135
+ attachment File.basename(filename)
136
+
137
+ content
138
+ end
139
+
140
+ get "/:id" do
141
+ haml File.read(File.join(Machinery::ROOT, "html/index.html.haml")),
142
+ locals: { description_name: params[:id] }
143
+ end
144
+ end
145
+
146
+ if opts[:ip] != "localhost" && opts[:ip] != "127.0.0.1"
147
+ Machinery::Ui.puts <<EOF
148
+ Warning:
149
+ You specified an IP address other than '127.0.0.1', your server may be reachable from the network.
150
+ This could lead to confidential data like passwords or private keys being readable by others.
151
+ EOF
152
+ end
153
+
154
+ begin
155
+ setup_output_redirection
156
+ server.run!
157
+ rescue => e
158
+ # Re-raise exception in main thread
159
+ Thread.main.raise e
160
+ ensure
161
+ remove_output_redirection
35
162
  end
36
163
  end
164
+ end
37
165
 
38
- FileUtils.cp_r(File.join(Machinery::ROOT, "html", "assets"), target)
39
- File.write(File.join(target, "index.html"), template.render(binding))
166
+ def self.when_server_ready(ip, port, &block)
167
+ 20.times do
168
+ begin
169
+ TCPSocket.new(ip, port).close
170
+ block.call
171
+ return
172
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
173
+ sleep 0.1
174
+ end
175
+ end
176
+ raise Machinery::Errors::MachineryError, "The web server did not come up in time."
177
+ end
40
178
 
41
- # Generate JSON and escape the 's and "s in order to not break the JSON
42
- # string in javascript.
43
- json = escape_javascript(description.to_hash.to_json)
44
- File.write(File.join(target, "assets/description.js"),<<-EOT
45
- function getDescription() {
46
- return JSON.parse('#{json}'
47
- )
48
- }
49
- EOT
50
- )
51
- target
179
+ def self.setup_output_redirection
180
+ @orig_stdout = STDOUT.clone
181
+ @orig_stderr = STDERR.clone
182
+ server_log = File.join(Machinery::DEFAULT_CONFIG_DIR, "webserver.log")
183
+ STDOUT.reopen server_log, "w"
184
+ STDERR.reopen server_log, "w"
185
+ end
186
+
187
+ def self.remove_output_redirection
188
+ STDOUT.reopen @orig_stdout
189
+ STDERR.reopen @orig_stderr
52
190
  end
53
191
 
54
192
  def self.generate_comparison(diff, target)
@@ -68,65 +206,4 @@ class Html
68
206
  EOT
69
207
  )
70
208
  end
71
-
72
- # Template helpers
73
-
74
- def self.scope_help(scope)
75
- text = File.read(File.join(Machinery::ROOT, "plugins", "#{scope}/#{scope}.md"))
76
- Kramdown::Document.new(text).to_html
77
- end
78
-
79
- def self.diff_to_object(diff)
80
- diff = Machinery.scrub(diff)
81
- lines = diff.lines[2..-1]
82
- diff_object = {
83
- file: diff[/--- a(.*)/, 1],
84
- additions: lines.select { |l| l.start_with?("+") }.length,
85
- deletions: lines.select { |l| l.start_with?("-") }.length
86
- }
87
-
88
- original_line_number = 0
89
- new_line_number = 0
90
- diff_object[:lines] = lines.map do |line|
91
- line = ERB::Util.html_escape(line.chomp).
92
- gsub("\\", "&#92;").
93
- gsub("\t", "&nbsp;"*8)
94
- case line
95
- when /^@.*/
96
- entry = {
97
- type: "header",
98
- content: line
99
- }
100
- original_line_number = line[/-(\d+)/, 1].to_i
101
- new_line_number = line[/\+(\d+)/, 1].to_i
102
- when /^ .*/, ""
103
- entry = {
104
- type: "common",
105
- new_line_number: new_line_number,
106
- original_line_number: original_line_number,
107
- content: line[1..-1]
108
- }
109
- new_line_number += 1
110
- original_line_number += 1
111
- when /^\+.*/
112
- entry = {
113
- type: "addition",
114
- new_line_number: new_line_number,
115
- content: line[1..-1]
116
- }
117
- new_line_number += 1
118
- when /^\-.*/
119
- entry = {
120
- type: "deletion",
121
- original_line_number: original_line_number,
122
- content: line[1..-1]
123
- }
124
- original_line_number += 1
125
- end
126
-
127
- entry
128
- end
129
-
130
- diff_object
131
- end
132
209
  end