machinery-tool 1.9.1 → 1.10.0

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