/
consistency_check.rb
247 lines (223 loc) · 7.49 KB
/
consistency_check.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
require 'api_exception'
require 'xmlhash'
class ConsistencyCheckJob < ApplicationJob
def fix
perform(true)
end
def init
User.current ||= User.get_default_admin
@errors = ""
end
def perform(fix = nil)
init
@errors = project_existence_consistency_check(fix)
Project.find_each(batch_size: 100) do |project|
unless Project.valid_name? project.name
@errors << "Invalid project name #{project.name}\n"
if fix
Suse::Backend.without_global_write_through do
# just remove it, the backend won't accept it anyway
project.destroy
end
end
next
end
@errors << package_existence_consistency_check(project, fix)
@errors << project_meta_check(project, fix)
end
unless @errors.blank?
@errors = "FIXING the following errors:\n" << @errors if fix
Rails.logger.error("Detected problems during consistency check")
Rails.logger.error(@errors)
AdminMailer.error(@errors).deliver_now
end
nil
end
# for manual fixing by admin via rails command
def fix_project
init
check_project(true)
end
def check_project(fix = nil)
init
if ENV['project'].blank?
puts "Please specify the project with 'project=MyProject' on CLI"
return
end
begin
project = Project.get_by_name(ENV['project'])
@errors << project_meta_check(project, fix)
rescue Project::UnknownObjectError
# specified but does not exist in api. does it also not exist in backend?
@errors << import_project_from_backend(ENV['project'])
project = Project.get_by_name(ENV['project'])
end
@errors << package_existence_consistency_check(project, fix)
puts @errors unless @errors.blank?
end
def project_meta_check(project, fix = nil)
errors = ""
# WARNING: this is using the memcache content. should maybe dropped before
api_meta = project.to_axml
begin
backend_meta = Suse::Backend.get("/source/#{project.name}/_meta").body
rescue ActiveXML::Transport::NotFoundError
# project disappeared ... may happen in running system
return ""
end
backend_hash = Xmlhash.parse(backend_meta)
api_hash = Xmlhash.parse(api_meta)
# ignore description and title
backend_hash['title'] = api_hash['title'] = nil
backend_hash['description'] = api_hash['description'] = nil
diff = hash_diff(api_hash, backend_hash)
unless diff.empty?
errors << "Project meta is different in backend for #{project.name}\n#{diff}\n"
if fix
# Assume that api is right
project.store({login: "Admin", comment: "out-of-sync fix"})
end
end
errors
end
def project_existence_consistency_check(fix = nil)
errors = ""
# compare projects
project_list_api = Project.all.pluck(:name).sort
begin
project_list_backend = dir_to_array(Xmlhash.parse(Suse::Backend.get("/source").body))
rescue ActiveXML::Transport::NotFoundError
# project disappeared ... may happen in running system
return ""
end
diff = project_list_api - project_list_backend
unless diff.empty?
errors << "Additional projects in api:\n #{diff}\n"
if fix
# just delete ... if it exists in backend it can be undeleted
diff.each do |project|
project = Project.find_by_name project
project.destroy if project
end
end
end
diff = project_list_backend - project_list_api
unless diff.empty?
errors << "Additional projects in backend:\n #{diff}\n"
if fix
diff.each do |project|
errors << import_project_from_backend(project)
end
end
end
errors
end
def import_project_from_backend(project)
Suse::Backend.without_global_write_through do
meta = Suse::Backend.get("/source/#{project}/_meta").body
project = Project.new(name: project)
project.update_from_xml(Xmlhash.parse(meta))
project.save!
end
return ""
rescue ActiveRecord::RecordInvalid
Suse::Backend.delete("/source/#{project}")
return "DELETED #{project} on backend due to invalid data\n"
rescue ActiveXML::Transport::NotFoundError
return "specified #{project} does not exist on backend\n"
end
def package_existence_consistency_check(project, fix = nil)
errors = ""
begin
project.reload
rescue ActiveRecord::RecordNotFound
# project disappeared ... may happen in running system
return ""
end
# valid package names?
Suse::Backend.without_global_write_through do
package_list_api = project.packages.pluck(:name)
package_list_api.each do |name|
unless Package.valid_name? name
errors << "Invalid package name #{name} in project #{project.name}\n"
if fix
# just remove it, the backend won't accept it anyway
project.packages.find_by(name: name).destroy
next
end
end
end
end
# compare all packages
package_list_api = project.packages.pluck(:name)
plb = dir_to_array(Xmlhash.parse(Suse::Backend.get("/source/#{project.name}").body))
# filter multibuild source container
package_list_backend = plb.map{ |e| e.start_with?('_patchinfo:', '_product:') ? e : e.gsub(/:.*$/, '') }
diff = package_list_api - package_list_backend
unless diff.empty?
errors << "Additional package in api project #{project.name}:\n #{diff}\n"
if fix
# delete database object, can be undeleted
diff.each do |package|
pkg = project.packages.where(name: package).first
pkg.destroy if pkg
end
end
end
diff = package_list_backend - package_list_api
unless diff.empty?
errors << "Additional package in backend project #{project.name}:\n #{diff}\n"
if fix
Suse::Backend.without_global_write_through do
# restore from backend
diff.each do |package|
begin
meta = Suse::Backend.get("/source/#{project.name}/#{package}/_meta").body
pkg = project.packages.new(name: package)
pkg.update_from_xml(Xmlhash.parse(meta), true) # ignore locked project
pkg.save!
rescue ActiveRecord::RecordInvalid,
ActiveXML::Transport::NotFoundError
Suse::Backend.delete("/source/#{project.name}/#{package}")
errors << "DELETED in backend due to invalid data #{project.name}/#{package}\n"
end
end
end
end
end
errors
end
def dir_to_array(xmlhash)
array = []
xmlhash.elements('entry') do |e|
array << e['name']
end
array.sort
end
def hash_diff(a, b)
# ignore the order inside of the hash
(a.keys.sort | b.keys.sort).each_with_object({}) do |diff, k|
a_ = a[k]
b_ = b[k]
# we need to ignore the ordering in some cases
# old xml generator wrote them in a different order
# but in other cases the order of elements matters
if k == "person" && a_.kind_of?(Array)
a_ = a_.map{ |i| "#{i['userid']}/#{i['role']}" }.sort
b_ = b_.map{ |i| "#{i['userid']}/#{i['role']}" }.sort
end
if k == "group" && a_.kind_of?(Array)
a_ = a_.map{ |i| "#{i['groupid']}/#{i['role']}" }.sort
b_ = b_.map{ |i| "#{i['groupid']}/#{i['role']}" }.sort
end
if a_ != b_
if a[k].class == Hash && b[k].class == Hash
diff[k] = hash_diff(a[k], b[k])
else
diff[k] = [a[k], b[k]]
end
end
diff
end
end
end