machinery-tool 1.0.2

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 (157) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +674 -0
  3. data/NEWS +143 -0
  4. data/bin/machinery +29 -0
  5. data/helpers/changed_managed_files.sh +32 -0
  6. data/helpers/filter-packages-for-build.yaml +6 -0
  7. data/html/assets/arrow_down.png +0 -0
  8. data/html/assets/arrow_up.png +0 -0
  9. data/html/assets/bootstrap-popover.js +113 -0
  10. data/html/assets/bootstrap-tooltip.js +457 -0
  11. data/html/assets/bootstrap.min.css +5 -0
  12. data/html/assets/collapse.js +174 -0
  13. data/html/assets/hogan-3.0.2.min.mustache.js +5 -0
  14. data/html/assets/jquery-2.1.1.min.js +4 -0
  15. data/html/assets/logo-changed-managed-files-small.png +0 -0
  16. data/html/assets/logo-changed-managed-files.png +0 -0
  17. data/html/assets/logo-config-files-small.png +0 -0
  18. data/html/assets/logo-config-files.png +0 -0
  19. data/html/assets/logo-groups-small.png +0 -0
  20. data/html/assets/logo-groups.png +0 -0
  21. data/html/assets/logo-os-small.png +0 -0
  22. data/html/assets/logo-os.png +0 -0
  23. data/html/assets/logo-packages-small.png +0 -0
  24. data/html/assets/logo-packages.png +0 -0
  25. data/html/assets/logo-patterns-small.png +0 -0
  26. data/html/assets/logo-patterns.png +0 -0
  27. data/html/assets/logo-repositories-small.png +0 -0
  28. data/html/assets/logo-repositories.png +0 -0
  29. data/html/assets/logo-services-small.png +0 -0
  30. data/html/assets/logo-services.png +0 -0
  31. data/html/assets/logo-unmanaged-files-small.png +0 -0
  32. data/html/assets/logo-unmanaged-files.png +0 -0
  33. data/html/assets/logo-users-small.png +0 -0
  34. data/html/assets/logo-users.png +0 -0
  35. data/html/assets/machinery-base.css +5767 -0
  36. data/html/assets/machinery.css +131 -0
  37. data/html/assets/machinery.js +148 -0
  38. data/html/assets/transition.js +59 -0
  39. data/html/assets/wheels_horizontal.png +0 -0
  40. data/html/index.html.haml +468 -0
  41. data/kiwi_helpers/kiwi_export_readme.md +22 -0
  42. data/kiwi_helpers/merge_users_and_groups.pl.erb +231 -0
  43. data/kiwi_helpers/unmanaged_files_build_excludes +5 -0
  44. data/lib/analyze_config_file_diffs_task.rb +130 -0
  45. data/lib/array.rb +98 -0
  46. data/lib/build_task.rb +124 -0
  47. data/lib/changed_rpm_files_helper.rb +96 -0
  48. data/lib/cli.rb +600 -0
  49. data/lib/compare_task.rb +68 -0
  50. data/lib/config.rb +33 -0
  51. data/lib/config_base.rb +117 -0
  52. data/lib/config_task.rb +56 -0
  53. data/lib/constants.rb +24 -0
  54. data/lib/copy_task.rb +22 -0
  55. data/lib/current_user.rb +23 -0
  56. data/lib/deploy_task.rb +89 -0
  57. data/lib/exceptions.rb +113 -0
  58. data/lib/generate_html_task.rb +22 -0
  59. data/lib/helper.rb +22 -0
  60. data/lib/hint.rb +39 -0
  61. data/lib/html.rb +103 -0
  62. data/lib/inspect_task.rb +93 -0
  63. data/lib/inspector.rb +65 -0
  64. data/lib/kiwi_config.rb +356 -0
  65. data/lib/kiwi_export_task.rb +36 -0
  66. data/lib/list_task.rb +64 -0
  67. data/lib/local_system.rb +127 -0
  68. data/lib/logged_cheetah.rb +25 -0
  69. data/lib/machinery.rb +85 -0
  70. data/lib/machinery_logger.rb +47 -0
  71. data/lib/migration.rb +128 -0
  72. data/lib/mountpoints.rb +72 -0
  73. data/lib/object.rb +138 -0
  74. data/lib/os.rb +78 -0
  75. data/lib/remote_system.rb +114 -0
  76. data/lib/remove_task.rb +43 -0
  77. data/lib/renderer.rb +243 -0
  78. data/lib/renderer_helper.rb +26 -0
  79. data/lib/rpm.rb +52 -0
  80. data/lib/scope_mixin.rb +38 -0
  81. data/lib/show_task.rb +65 -0
  82. data/lib/system.rb +81 -0
  83. data/lib/system_description.rb +228 -0
  84. data/lib/system_description_store.rb +167 -0
  85. data/lib/system_description_validator.rb +216 -0
  86. data/lib/tarball.rb +82 -0
  87. data/lib/ui.rb +74 -0
  88. data/lib/upgrade_format_task.rb +55 -0
  89. data/lib/validate_task.rb +23 -0
  90. data/lib/version.rb +22 -0
  91. data/lib/zypper.rb +70 -0
  92. data/man/generated/machinery.1.gz +0 -0
  93. data/man/generated/machinery.1.html +1056 -0
  94. data/plugins/docs/changed_managed_files.md +2 -0
  95. data/plugins/docs/config_files.md +5 -0
  96. data/plugins/docs/groups.md +2 -0
  97. data/plugins/docs/os.md +2 -0
  98. data/plugins/docs/packages.md +2 -0
  99. data/plugins/docs/patterns.md +5 -0
  100. data/plugins/docs/repositories.md +24 -0
  101. data/plugins/docs/services.md +6 -0
  102. data/plugins/docs/unmanaged_files.md +13 -0
  103. data/plugins/docs/users.md +3 -0
  104. data/plugins/inspect/changed_managed_files_inspector.rb +109 -0
  105. data/plugins/inspect/config_files_inspector.rb +117 -0
  106. data/plugins/inspect/groups_inspector.rb +46 -0
  107. data/plugins/inspect/os_inspector.rb +116 -0
  108. data/plugins/inspect/packages_inspector.rb +46 -0
  109. data/plugins/inspect/patterns_inspector.rb +67 -0
  110. data/plugins/inspect/repositories_inspector.rb +107 -0
  111. data/plugins/inspect/services_inspector.rb +88 -0
  112. data/plugins/inspect/unmanaged_files_inspector.rb +393 -0
  113. data/plugins/inspect/users_inspector.rb +87 -0
  114. data/plugins/model/changed_managed_files_model.rb +29 -0
  115. data/plugins/model/config_files_model.rb +29 -0
  116. data/plugins/model/groups_model.rb +26 -0
  117. data/plugins/model/os_model.rb +20 -0
  118. data/plugins/model/packages_model.rb +26 -0
  119. data/plugins/model/patterns_model.rb +26 -0
  120. data/plugins/model/repositories_model.rb +26 -0
  121. data/plugins/model/services_model.rb +48 -0
  122. data/plugins/model/unmanaged_files_model.rb +29 -0
  123. data/plugins/model/users_model.rb +26 -0
  124. data/plugins/schema/v1/system-description-changed-managed-files.schema.json +83 -0
  125. data/plugins/schema/v1/system-description-config-files.schema.json +83 -0
  126. data/plugins/schema/v1/system-description-groups.schema.json +30 -0
  127. data/plugins/schema/v1/system-description-os.schema.json +21 -0
  128. data/plugins/schema/v1/system-description-packages.schema.json +34 -0
  129. data/plugins/schema/v1/system-description-patterns.schema.json +24 -0
  130. data/plugins/schema/v1/system-description-repositories.schema.json +41 -0
  131. data/plugins/schema/v1/system-description-services.schema.json +30 -0
  132. data/plugins/schema/v1/system-description-unmanaged-files.schema.json +105 -0
  133. data/plugins/schema/v1/system-description-users.schema.json +61 -0
  134. data/plugins/schema/v2/system-description-changed-managed-files.schema.json +92 -0
  135. data/plugins/schema/v2/system-description-config-files.schema.json +92 -0
  136. data/plugins/schema/v2/system-description-groups.schema.json +30 -0
  137. data/plugins/schema/v2/system-description-os.schema.json +21 -0
  138. data/plugins/schema/v2/system-description-packages.schema.json +34 -0
  139. data/plugins/schema/v2/system-description-patterns.schema.json +24 -0
  140. data/plugins/schema/v2/system-description-repositories.schema.json +41 -0
  141. data/plugins/schema/v2/system-description-services.schema.json +30 -0
  142. data/plugins/schema/v2/system-description-unmanaged-files.schema.json +138 -0
  143. data/plugins/schema/v2/system-description-users.schema.json +61 -0
  144. data/plugins/show/changed_managed_files_renderer.rb +46 -0
  145. data/plugins/show/config_files_renderer.rb +62 -0
  146. data/plugins/show/groups_renderer.rb +36 -0
  147. data/plugins/show/os_renderer.rb +31 -0
  148. data/plugins/show/packages_renderer.rb +32 -0
  149. data/plugins/show/patterns_renderer.rb +32 -0
  150. data/plugins/show/repositories_renderer.rb +38 -0
  151. data/plugins/show/services_renderer.rb +32 -0
  152. data/plugins/show/unmanaged_files_renderer.rb +42 -0
  153. data/plugins/show/users_renderer.rb +35 -0
  154. data/schema/migrations/migrate1to2.rb +56 -0
  155. data/schema/v1/system-description-global.schema.json +31 -0
  156. data/schema/v2/system-description-global.schema.json +31 -0
  157. metadata +370 -0
@@ -0,0 +1,228 @@
1
+ # Copyright (c) 2013-2014 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 SystemDescription < Machinery::Object
19
+ CURRENT_FORMAT_VERSION = 2
20
+ EXTRACTABLE_SCOPES = [
21
+ "changed_managed_files",
22
+ "config_files",
23
+ "unmanaged_files"
24
+ ]
25
+
26
+ attr_accessor :name
27
+ attr_accessor :store
28
+ attr_accessor :format_version
29
+
30
+ class << self
31
+ def from_json(name, json, store = nil)
32
+ begin
33
+ json_hash = JSON.parse(json)
34
+ rescue JSON::ParserError => e
35
+ lines = e.message.split("\n")
36
+ error_pos = json.split("\n").length - lines.length + 2
37
+ block_end = lines.index { |l| l =~ / [\}\]],?$/ }
38
+
39
+ # remove needless json error information
40
+ lines[0].gsub!(/^\d+: (.*)$/, "\\1")
41
+ json_error = lines[0..block_end].join("\n")
42
+
43
+ if error_pos == 1
44
+ json_error = "An opening bracket, a comma or quotation is missing " \
45
+ "in one of the global scope definitions or in the meta section. " \
46
+ "Unlike issues with the elements of the scopes, our JSON parser " \
47
+ "isn't able to locate issues like these."
48
+ error_pos = nil
49
+ end
50
+
51
+ error = "The JSON data of the system description '#{name}' " \
52
+ "couldn't be parsed. The following error occured"
53
+ error += " around line #{error_pos}" if error_pos
54
+ error += " in file '#{store.manifest_path(name)}'" if store
55
+ error += ":\n\n#{json_error}"
56
+
57
+ raise Machinery::Errors::SystemDescriptionError.new(error)
58
+ end
59
+
60
+ if compatible_json?(json_hash)
61
+ SystemDescriptionValidator.new(self).validate_json(json_hash)
62
+ end
63
+
64
+ begin
65
+ description = self.new(name, self.create_attrs(json_hash), store)
66
+ rescue NameError
67
+ raise Machinery::Errors::SystemDescriptionIncompatible.new(name)
68
+ end
69
+
70
+ json_format_version = json_hash["meta"]["format_version"] if json_hash["meta"]
71
+ description.format_version = json_format_version
72
+
73
+ description
74
+ end
75
+
76
+ def create_attrs(hash)
77
+ entries = hash.map do |key, value|
78
+ next if key == "meta"
79
+
80
+ class_name = "#{key.split("_").map(&:capitalize).join}Scope"
81
+ value_converted = Object.const_get(class_name).from_json(value)
82
+
83
+ # Set metadata
84
+ if hash["meta"] && hash["meta"][key]
85
+ value_converted.meta = Machinery::Object.from_json(hash["meta"][key])
86
+ end
87
+
88
+ [key, value_converted]
89
+ end.compact
90
+
91
+ Hash[entries]
92
+ end
93
+
94
+ private
95
+
96
+ def compatible_json?(json)
97
+ json.is_a?(Hash) &&
98
+ json["meta"].is_a?(Hash) &&
99
+ json["meta"]["format_version"] == CURRENT_FORMAT_VERSION
100
+ end
101
+ end
102
+
103
+ def initialize(name, hash = {}, store = nil)
104
+ @name = name
105
+ @store = store
106
+ @format_version = CURRENT_FORMAT_VERSION
107
+
108
+ super(hash)
109
+ end
110
+
111
+ def compatible?
112
+ !format_version.nil? &&
113
+ format_version == SystemDescription::CURRENT_FORMAT_VERSION
114
+ end
115
+
116
+ def validate_compatibility
117
+ if !compatible?
118
+ raise Machinery::Errors::SystemDescriptionIncompatible.new(self.name)
119
+ end
120
+ end
121
+
122
+ def to_hash
123
+ meta = {}
124
+ meta["format_version"] = self.format_version if self.format_version
125
+
126
+ attributes.each do |key, value|
127
+ meta[key] = self[key].meta.as_json if self[key].meta
128
+ end
129
+
130
+ hash = as_json
131
+ hash["meta"] = meta unless meta.empty?
132
+ hash
133
+ end
134
+
135
+ def to_json
136
+ JSON.pretty_generate(to_hash)
137
+ end
138
+
139
+ def scopes
140
+ scopes = attributes.map {
141
+ |key, value| key.to_s
142
+ }.sort
143
+ scopes
144
+ end
145
+
146
+ def assert_scopes(*scopes)
147
+ missing = scopes.reject do |e|
148
+ self.send(e) && !self.send(e).empty?
149
+ end
150
+
151
+ unless missing.empty?
152
+ raise Machinery::Errors::SystemDescriptionError.new(
153
+ "The system description misses the following section(s): #{missing.join(", ")}."
154
+ )
155
+ end
156
+ end
157
+
158
+ def short_os_version
159
+ assert_scopes("os")
160
+
161
+ case self.os.name
162
+ when /^SUSE Linux Enterprise Server/
163
+ "sles" + self.os.version[/\d+( SP\d+)*/].gsub(" ", "").downcase
164
+ when /^openSUSE/
165
+ self.os.version[/^\d+.\d+/]
166
+ else
167
+ "unknown"
168
+ end
169
+ end
170
+
171
+ def scope_extracted?(scope)
172
+ self[scope] && self[scope].is_extractable? && self[scope].extracted
173
+ end
174
+
175
+ def os_object
176
+ assert_scopes("os")
177
+
178
+ begin
179
+ Os.for(self.os.name)
180
+ rescue Machinery::Errors::UnknownOs => e
181
+ raise Machinery::Errors::SystemDescriptionError.new(e)
182
+ end
183
+ end
184
+
185
+ def missing_files(scope, file_list)
186
+ file_list.map! { |file| File.join(file_store(scope), file) }
187
+
188
+ file_list.select do |file|
189
+ !File.exists?(file)
190
+ end
191
+ end
192
+
193
+ def additional_files(scope, file_list)
194
+ file_list.map! { |file| File.join(file_store(scope), file) }
195
+ files = list_file_store_content(scope)
196
+
197
+ files - file_list
198
+ end
199
+
200
+ def validate_file_data
201
+ SystemDescriptionValidator.new(self).validate_file_data
202
+ end
203
+
204
+ # Filestore handling
205
+ def initialize_file_store(store_name)
206
+ @store.initialize_file_store(self.name, store_name)
207
+ end
208
+
209
+ def file_store(store_name)
210
+ @store.file_store(self.name, store_name)
211
+ end
212
+
213
+ def remove_file_store(store_name)
214
+ @store.remove_file_store(self.name, store_name)
215
+ end
216
+
217
+ def rename_file_store(old_name, new_name)
218
+ @store.rename_file_store(self.name, old_name, new_name)
219
+ end
220
+
221
+ def list_file_store_content(store_name)
222
+ @store.list_file_store_content(self.name, store_name)
223
+ end
224
+
225
+ def create_file_store_sub_dir(store_name, sub_dir)
226
+ @store.create_file_store_sub_dir(self.name, store_name, sub_dir)
227
+ end
228
+ end
@@ -0,0 +1,167 @@
1
+ # Copyright (c) 2013-2014 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 SystemDescriptionStore
19
+ attr_reader :base_path
20
+
21
+ def default_path
22
+ Machinery::DEFAULT_CONFIG_DIR
23
+ end
24
+
25
+ def initialize(base_path = default_path)
26
+ @base_path = base_path
27
+ create_dir(@base_path)
28
+ end
29
+
30
+ def description_path(name)
31
+ File.join(@base_path, name)
32
+ end
33
+
34
+ def manifest_path(name)
35
+ File.join(description_path(name), "manifest.json")
36
+ end
37
+
38
+ def html_path(name)
39
+ File.join(description_path(name), "index.html")
40
+ end
41
+
42
+ def load_json(name)
43
+ validate_name(name)
44
+ file_name = manifest_path(name)
45
+ unless File.exists?(file_name)
46
+ raise Machinery::Errors::SystemDescriptionNotFound.new(
47
+ "A system description with the name #{name} was not found."
48
+ )
49
+ end
50
+ File.read(file_name)
51
+ end
52
+
53
+ def load(name)
54
+ json = load_json(name)
55
+ description = SystemDescription.from_json(name, json, self)
56
+ description.validate_compatibility
57
+ description.validate_file_data
58
+ description
59
+ end
60
+
61
+ def save(description)
62
+ validate_name(description.name)
63
+ create_dir(description_path(description.name))
64
+ path = manifest_path(description.name)
65
+ created = !File.exists?(path)
66
+ File.write(path, description.to_json)
67
+ File.chmod(0600,path) if created
68
+ end
69
+
70
+ def list
71
+ Dir["#{@base_path}/*"].
72
+ select { |item| File.exists?(manifest_path(File.basename(item)))}.
73
+ map { |item| File.basename(item) }
74
+ end
75
+
76
+ def remove(name)
77
+ unless name.empty?
78
+ validate_name(name)
79
+ FileUtils.rm_rf(description_path(name))
80
+ else
81
+ raise "The system description has no name specified and thus can't be deleted."
82
+ end
83
+ end
84
+
85
+ def copy(from, to)
86
+ validate_name(from)
87
+ validate_name(to)
88
+ if !list.include?(from)
89
+ raise Machinery::Errors::SystemDescriptionNotFound.new(
90
+ "System description \"#{from}\" does not exist."
91
+ )
92
+ end
93
+
94
+ if list.include?(to)
95
+ raise Machinery::Errors::SystemDescriptionError.new(
96
+ "A System description with the name \"#{to}\" does already exist."
97
+ )
98
+ end
99
+
100
+ FileUtils.cp_r(description_path(from), description_path(to))
101
+ end
102
+
103
+ def initialize_file_store(description_name, store_name)
104
+ dir = File.join(description_path(description_name), store_name)
105
+ create_dir(dir, new_dir_mode(description_name))
106
+ end
107
+
108
+ def file_store(description_name, store_name)
109
+ dir = File.join(description_path(description_name), store_name)
110
+ Dir.exists?(dir) ? dir : nil
111
+ end
112
+
113
+ def remove_file_store(description_name, store_name)
114
+ FileUtils.rm_rf(File.join(description_path(description_name), store_name))
115
+ end
116
+
117
+ def rename_file_store(description_name, store_old, store_new)
118
+ FileUtils.mv(
119
+ File.join(description_path(description_name), store_old),
120
+ File.join(description_path(description_name), store_new)
121
+ )
122
+ end
123
+
124
+ def create_file_store_sub_dir(description_name, store_name, sub_dir)
125
+ dir = File.join(description_path(description_name), store_name, sub_dir)
126
+ create_dir(dir, new_dir_mode(description_name))
127
+ end
128
+
129
+ def list_file_store_content(description_name, store_name)
130
+ dir = File.join(description_path(description_name), store_name)
131
+
132
+ files = Dir.glob(File.join(dir, "**/{*,.*}"))
133
+ # filter parent directories because they should not be listed separately
134
+ files.reject { |f| files.index { |e| e =~ /^#{f}\/.+/ } }
135
+ end
136
+
137
+ def new_dir_mode(name)
138
+ mode = 0700
139
+ if Dir.exists?(description_path(name))
140
+ mode = File.stat(description_path(name)).mode & 0777
141
+ end
142
+ mode
143
+ end
144
+
145
+ private
146
+
147
+ def create_dir(dir, mode = 0700)
148
+ unless Dir.exists?(dir)
149
+ FileUtils.mkdir_p(dir, :mode => mode)
150
+ end
151
+ end
152
+
153
+ def validate_name(name)
154
+ if ! /^[\w\.:-]*$/.match(name)
155
+ raise Machinery::Errors::SystemDescriptionError.new(
156
+ "System description name \"#{name}\" is invalid. Only \"a-zA-Z0-9_:.-\" are valid characters."
157
+ )
158
+ end
159
+
160
+ if name.start_with?(".")
161
+ raise Machinery::Errors::SystemDescriptionError.new(
162
+ "System description name \"#{name}\" is invalid. A dot is not allowed as first character."
163
+ )
164
+ end
165
+
166
+ end
167
+ end
@@ -0,0 +1,216 @@
1
+ # Copyright (c) 2013-2014 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 SystemDescriptionValidator
19
+ class << self
20
+ def load_global_schema
21
+ JSON.parse(File.read(File.join(
22
+ Machinery::ROOT,
23
+ "schema",
24
+ "v#{SystemDescription::CURRENT_FORMAT_VERSION}",
25
+ "system-description-global.schema.json"
26
+ )))
27
+ end
28
+
29
+ def load_scope_schemas
30
+ schema_dir = File.join(
31
+ Machinery::ROOT,
32
+ "plugins",
33
+ "schema",
34
+ "v#{SystemDescription::CURRENT_FORMAT_VERSION}",
35
+ )
36
+
37
+ Hash[
38
+ Dir["#{schema_dir}/*.schema.json"].map do |file|
39
+ scope = file.match(/system-description-(.*)\.schema\.json$/)[1].tr("-", "_")
40
+ schema = JSON.parse(File.read(file))
41
+
42
+ [scope, schema]
43
+ end
44
+ ]
45
+ end
46
+ end
47
+
48
+ GLOBAL_SCHEMA = load_global_schema
49
+ SCOPE_SCHEMAS = load_scope_schemas
50
+
51
+ def initialize(description)
52
+ @description = description
53
+ end
54
+
55
+ def validate_json(json)
56
+ errors = JSON::Validator.fully_validate(GLOBAL_SCHEMA, json)
57
+
58
+ scopes = json.keys
59
+ scopes.delete("meta")
60
+
61
+ errors += scopes.flat_map do |scope|
62
+ schema = SCOPE_SCHEMAS[scope]
63
+
64
+ if schema
65
+ issue = JSON::Validator.fully_validate(schema, json[scope]).map do |error|
66
+ "In scope #{scope}: #{error}"
67
+ end
68
+ issue.map do |error|
69
+ cleanup_json_error_message(error, scope)
70
+ end
71
+ else
72
+ []
73
+ end
74
+ end
75
+
76
+ if !errors.empty?
77
+ raise Machinery::Errors::SystemDescriptionError.new(errors.join("\n"))
78
+ end
79
+ end
80
+
81
+ def missing_files_for_plain_scope(scope)
82
+ expected_files = @description[scope].files.reject { |file| file.changes.include?("deleted") }
83
+ missing_files = @description.missing_files(scope, expected_files.map(&:name))
84
+ end
85
+
86
+ def expected_files_for_tared_scope(scope)
87
+ has_files_tarball = @description[scope].files.any? { |f| f.type == "file" || f.type == "link" }
88
+ tree_tarballs = @description[scope].files.
89
+ select { |f| f.type == "dir" }.
90
+ map { |d| File.join("trees", d.name.sub(/\/$/, "") + ".tgz") }
91
+
92
+ expected_files = []
93
+ expected_files << "files.tgz" if has_files_tarball
94
+ expected_files += tree_tarballs
95
+ end
96
+
97
+ def missing_files_for_tared_scope(scope)
98
+ expected_files = expected_files_for_tared_scope(scope)
99
+ missing_files = @description.missing_files(scope, expected_files)
100
+ end
101
+
102
+ def missing_files_for_scope(scope)
103
+ if scope == "unmanaged_files"
104
+ missing_files_for_tared_scope(scope)
105
+ else
106
+ missing_files_for_plain_scope(scope)
107
+ end
108
+ end
109
+
110
+ def additional_files_for_tared_scope(scope)
111
+ expected_files = expected_files_for_tared_scope(scope)
112
+ additional_files = @description.additional_files(scope, expected_files)
113
+ end
114
+
115
+ def additional_files_for_plain_scope(scope)
116
+ expected_files = @description[scope].files.reject { |file| file.changes.include?("deleted") }
117
+ additional_files = @description.additional_files(scope, expected_files.map(&:name))
118
+ end
119
+
120
+ def additional_files_for_scope(scope)
121
+ if scope == "unmanaged_files"
122
+ additional_files_for_tared_scope(scope)
123
+ else
124
+ additional_files_for_plain_scope(scope)
125
+ end
126
+ end
127
+
128
+ def format_file_errors(scope, missing_files, additional_files)
129
+ errors = missing_files.map do |file|
130
+ " * File '" + file + "' doesn't exist"
131
+ end
132
+ errors += additional_files.map do |file|
133
+ " * File '" + file + "' doesn't have meta data"
134
+ end
135
+ "Scope '#{scope}':\n" + errors.join("\n")
136
+ end
137
+
138
+ def validate_file_data
139
+ errors = []
140
+
141
+ ["config_files", "changed_managed_files", "unmanaged_files"].each do |scope|
142
+ if @description.scope_extracted?(scope)
143
+ missing_files = missing_files_for_scope(scope)
144
+ additional_files = additional_files_for_scope(scope)
145
+
146
+ if !missing_files.empty? or !additional_files.empty?
147
+ errors.push(format_file_errors(scope, missing_files, additional_files))
148
+ end
149
+ end
150
+ end
151
+
152
+ if !errors.empty?
153
+ e = Machinery::Errors::SystemDescriptionValidationFailed.new(errors)
154
+ e.header = "Error validating description '#{@description.name}'"
155
+ raise e
156
+ end
157
+ end
158
+
159
+ def cleanup_json_error_message(message, scope)
160
+ message = cleanup_json_path(message, scope)
161
+ message = remove_json_error_uuid(message)
162
+ message
163
+ end
164
+
165
+ private
166
+
167
+ def cleanup_json_path(message, scope)
168
+ old_path = message[/The property '#\/(.*?)'/,1]
169
+
170
+ position = error_position_from_json_path(old_path)
171
+ details = extract_details_from_json_path(old_path, scope)
172
+
173
+ new_path = "The property"
174
+ new_path += " ##{position}" if position > -1
175
+ new_path += " (#{details})" if !details.empty?
176
+
177
+ message.gsub(/The property '#\/.*?'/, new_path)
178
+ end
179
+
180
+ def error_position_from_json_path(path)
181
+ elements = path.split("/")
182
+
183
+ position = -1
184
+ elements.each do |e|
185
+ if Machinery::is_int?(e)
186
+ number = e.to_i
187
+ position = number if number > position
188
+ end
189
+ end
190
+ position
191
+ end
192
+
193
+ def extract_details_from_json_path(path, scope)
194
+ elements = path.split("/")
195
+
196
+ elements.uniq!
197
+
198
+ # filter numbers since the position is calculated elswhere
199
+ elements.reject! { |e| Machinery::is_int?(e) }
200
+
201
+ # The json schema path often contains the word "type" in many messages
202
+ # but this information adds no value for the user since it is not related
203
+ # to our manifest.json
204
+ # So we filter it for all scopes except of repositories since this is the
205
+ # only scope which does have an attribute called "type"
206
+ if scope != "repositories"
207
+ elements.reject! { |e| e == "type" }
208
+ end
209
+
210
+ elements.join("/")
211
+ end
212
+
213
+ def remove_json_error_uuid(message)
214
+ message.gsub(/ in schema .*$/, ".")
215
+ end
216
+ end