diff options
author | onefang | 2019-06-25 15:08:34 +1000 |
---|---|---|
committer | onefang | 2019-06-25 15:08:34 +1000 |
commit | 22fc09cfd2305850063d2bbdd3af76237caf2e73 (patch) | |
tree | 87765825e2c2425ba00bcd26dfeb14919e39d539 | |
parent | Git ignore the results. (diff) | |
download | apt-panopticon-22fc09cfd2305850063d2bbdd3af76237caf2e73.zip apt-panopticon-22fc09cfd2305850063d2bbdd3af76237caf2e73.tar.gz apt-panopticon-22fc09cfd2305850063d2bbdd3af76237caf2e73.tar.bz2 apt-panopticon-22fc09cfd2305850063d2bbdd3af76237caf2e73.tar.xz |
Add the actual source code, and the basic documentation.
Still need to write the help output.
-rw-r--r-- | README.md | 95 | ||||
-rwxr-xr-x | mirror-checker.lua | 420 |
2 files changed, 515 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..415025c --- /dev/null +++ b/README.md | |||
@@ -0,0 +1,95 @@ | |||
1 | Lua script for checking the health of Devuan Linux package mirrors. | ||
2 | |||
3 | This is currently under development, not everything has been written yet. | ||
4 | |||
5 | mirror-checker-lua is a Lua script used by the Devuan mirror admins | ||
6 | (maybe, if they like it) to check the health of Devuan Linux package | ||
7 | mirrors. Originally there was bash scripts for this job, then Evilham | ||
8 | wrote some Python scripts, now onefang has written it in Lua. We all | ||
9 | have different tastes in languages. lol | ||
10 | |||
11 | The main difference is that this Lua version tries to do everything, and | ||
12 | will be maintained. Currently the shell scripts and Python scripts are | ||
13 | actually being used I think. Evilham asked me to write this, after I | ||
14 | badgered him about his Python scripts. | ||
15 | |||
16 | The source code is at https://sledjhamr.org/cgit/mirror-checker-lua/ | ||
17 | |||
18 | The issue tracker is at https://sledjhamr.org/mantisbt/project_page.php?project_id=13 | ||
19 | |||
20 | |||
21 | Installation. | ||
22 | ------------- | ||
23 | |||
24 | Download the source. You may want to put the actual mirror-checker.lua | ||
25 | in someplace liku /usr/local/bin and make sure it is executable. | ||
26 | |||
27 | It should run on any recent Linux, you'll need to have the following | ||
28 | installed - | ||
29 | |||
30 | Luajit | ||
31 | |||
32 | wget | ||
33 | |||
34 | dig, part of BIND. On Debian based systems it'll be in the dnsutils | ||
35 | package. | ||
36 | |||
37 | LuaSocket, on Debian based systems it'll be in the lua-socket package. | ||
38 | |||
39 | ionice, on Debian based systems it'll be in the util-linux package. | ||
40 | |||
41 | |||
42 | Using it. | ||
43 | --------- | ||
44 | |||
45 | These examples assume you are running it from the source code directory. | ||
46 | A directory will be created called results, it'll be full of log files | ||
47 | and any files that get downloaded. There will also be results/email and | ||
48 | results/web directories, with the notification email and web pages with | ||
49 | the easy to read emails (once I write that bit). | ||
50 | |||
51 | Note that unlike typical commands, you can't run single character options | ||
52 | together, so this is wrong - | ||
53 | |||
54 | ./mirror-checker.lua -vvv | ||
55 | |||
56 | Instead do this - | ||
57 | |||
58 | ./mirror-checker.lua -v -v -v | ||
59 | |||
60 | Just run the script to do all of the tests - | ||
61 | |||
62 | ./mirror-checker.lua | ||
63 | |||
64 | Which will print any errors. If you don't want to see errors - | ||
65 | |||
66 | ./mirror-checker.lua -q | ||
67 | |||
68 | If you want to see warnings to (as usual, the more -v options, the more | ||
69 | details) - | ||
70 | |||
71 | ./mirror-checker.lua -v | ||
72 | |||
73 | Or use the usual options for the help and version number - | ||
74 | |||
75 | ./mirror-checker.lua -h | ||
76 | ./mirror-checker.lua --help | ||
77 | ./mirror-checker.lua --version | ||
78 | |||
79 | To run the tests on a specific mirror, for example pkgmaster.devuan.org - | ||
80 | |||
81 | ./mirror-checker.lua pkgmaster.devuan.org | ||
82 | |||
83 | You can use the --tests option to tune which tests are run, for example | ||
84 | to stop IPv6 tests, coz you don't have IPv6 - | ||
85 | |||
86 | ./mirror-checker.lua --tests=-IPv6 | ||
87 | |||
88 | To do the same, but not run the HTTPS tests either - | ||
89 | |||
90 | ./mirror-checker.lua --tests=-IPv6,-https | ||
91 | |||
92 | To only run the http integrity tests, only on IPv6 - | ||
93 | |||
94 | ./mirror-checker.lua --tests=http,Integrity,IPv6 | ||
95 | |||
diff --git a/mirror-checker.lua b/mirror-checker.lua new file mode 100755 index 0000000..34b2ec8 --- /dev/null +++ b/mirror-checker.lua | |||
@@ -0,0 +1,420 @@ | |||
1 | #!/usr/bin/env luajit | ||
2 | |||
3 | |||
4 | local args = {...} | ||
5 | |||
6 | |||
7 | verbosity = 0 | ||
8 | keep = false | ||
9 | options = | ||
10 | { | ||
11 | referenceSite = | ||
12 | { | ||
13 | typ = "string", | ||
14 | help = "", | ||
15 | value = "pkgmaster.devuan.org", | ||
16 | }, | ||
17 | tests = | ||
18 | { | ||
19 | typ = "table", | ||
20 | help = "", | ||
21 | value = | ||
22 | { | ||
23 | "IPv4", | ||
24 | "IPv6", | ||
25 | -- "ftp", | ||
26 | "http", | ||
27 | "https", | ||
28 | -- "rsync", | ||
29 | "DNS-RR", | ||
30 | "Protocol", | ||
31 | -- "URL-Sanity", | ||
32 | -- "Integrity", | ||
33 | -- "Updated", | ||
34 | }, | ||
35 | }, | ||
36 | } | ||
37 | |||
38 | local defaultURL = {scheme = "http"} | ||
39 | local download = "wget -np -N -r -P results " | ||
40 | local releases = {"jessie", "ascii", "beowulf", "ceres"} | ||
41 | local releaseFiles = | ||
42 | { | ||
43 | -- Release file. | ||
44 | "/Release", | ||
45 | "/InRelease", | ||
46 | "/main/binary-all/Packages.gz", | ||
47 | -- Contents files. | ||
48 | "/main/Contents-all.gz", | ||
49 | "/main/Contents-amd64.gz", | ||
50 | "/main/Contents-arm64.gz", | ||
51 | "-security/main/Contents-all.gz", | ||
52 | "-security/main/Contents-amd64.gz", | ||
53 | "-security/main/Contents-arm64.gz", | ||
54 | } | ||
55 | local referenceDebs = | ||
56 | { | ||
57 | -- Devuan package. NOTE this one likely should not get redirected, but that's more a warning than an error. | ||
58 | "merged/pool/DEVUAN/main/d/desktop-base/desktop-base_2.0.3_all.deb", | ||
59 | -- Debian package. | ||
60 | "merged/pool/DEBIAN/main/d/dash/dash_0.5.8-2.4_amd64.deb", | ||
61 | -- Debian security package. NOTE this one should always be redirected? | ||
62 | "merged/pool/DEBIAN-SECURITY/updates/main/a/apt/apt-transport-https_1.4.9_amd64.deb", | ||
63 | } | ||
64 | local arg = {} | ||
65 | local sendArgs = "" | ||
66 | local log | ||
67 | |||
68 | |||
69 | local socket = require 'socket' | ||
70 | local ftp = require 'socket.ftp' | ||
71 | local http = require 'socket.http' | ||
72 | local url = require 'socket.url' | ||
73 | |||
74 | |||
75 | -- Use this to print a table. | ||
76 | printTable = function (table, space, name) | ||
77 | print(space .. name .. ": ") | ||
78 | print(space .. "{") | ||
79 | printTableSub(table, space .. " ") | ||
80 | print(space .. "}") | ||
81 | end | ||
82 | printTableSub = function (table, space) | ||
83 | for k, v in pairs(table) do | ||
84 | if type(v) == "table" then | ||
85 | printTable(v, space, k) | ||
86 | elseif type(v) == "string" then | ||
87 | print(space .. k .. ': "' .. v .. '";') | ||
88 | elseif type(v) == "function" then | ||
89 | print(space .. "function " .. k .. "();") | ||
90 | elseif type(v) == "userdata" then | ||
91 | print(space .. "userdata " .. k .. ";") | ||
92 | elseif type(v) == "boolean" then | ||
93 | if (v) then | ||
94 | print(space .. "boolean " .. k .. " TRUE ;") | ||
95 | else | ||
96 | print(space .. "boolean " .. k .. " FALSE ;") | ||
97 | end | ||
98 | else | ||
99 | print(space .. k .. ": " .. v .. ";") | ||
100 | end | ||
101 | end | ||
102 | end | ||
103 | |||
104 | local log = function(v, t, s) | ||
105 | if v <= verbosity then | ||
106 | if 3 <= verbosity then t = os.date() .. " " .. t end | ||
107 | print(t .. ": " .. s) | ||
108 | end | ||
109 | if nil ~= log then | ||
110 | log:write(os.date() .. " " .. t .. ": " .. s .. "/n") | ||
111 | log:flush() | ||
112 | end | ||
113 | end | ||
114 | local D = function(s) log(3, "DEBUG ", s) end | ||
115 | local I = function(s) log(2, "INFO ", s) end | ||
116 | local W = function(s) log(1, "WARNING ", s) end | ||
117 | local E = function(s) log(0, "ERROR ", s) end | ||
118 | local C = function(s) log(-1, "CRITICAL", s) end | ||
119 | |||
120 | local testing = function(t) | ||
121 | for i, v in pairs(options.tests.value) do | ||
122 | if t == v then return true end | ||
123 | end | ||
124 | return false | ||
125 | end | ||
126 | |||
127 | local checkExes = function (exe) | ||
128 | local count = io.popen('ps x | grep "' .. exe .. '" | grep -v " grep " | wc -l'):read("*l") | ||
129 | D(count .. " " .. exe .. " commands left.") | ||
130 | return tonumber(count) | ||
131 | end | ||
132 | |||
133 | |||
134 | local IP = {} | ||
135 | local gatherIPs = function (host) | ||
136 | if nil == IP[host] then | ||
137 | local IPs | ||
138 | local dig = io.popen('dig +keepopen +noall +nottlid +answer ' .. host .. ' A ' .. host .. ' AAAA ' .. host .. ' CNAME ' .. host .. ' SRV | sort | uniq') | ||
139 | repeat | ||
140 | IPs = dig:read("*l") | ||
141 | if nil ~= IPs then | ||
142 | for k, t, v in IPs:gmatch("([%w_%-%.]*)%.%s*IN%s*(%a*)%s*(.*)") do | ||
143 | if "." == v:sub(-1, -1) then v = v:sub(1, -2) end | ||
144 | if nil == IP[k] then IP[k] = {} end | ||
145 | IP[k][v] = t | ||
146 | end | ||
147 | end | ||
148 | until nil == IPs | ||
149 | end | ||
150 | end | ||
151 | |||
152 | -- Returns FTP directory listing | ||
153 | local nlst = function (u) | ||
154 | local t = {} | ||
155 | local p = url.parse(u) | ||
156 | p.command = "nlst" | ||
157 | p.sink = ltn12.sink.table(t) | ||
158 | local r, e = ftp.get(p) | ||
159 | return r and table.concat(t), e | ||
160 | end | ||
161 | |||
162 | checkURL = function (host, URL, r) | ||
163 | if nil == r then r = 0 end | ||
164 | local check = "Checking file" | ||
165 | if 0 < r then | ||
166 | check = "Redirecting to" | ||
167 | -- checkIP(host) | ||
168 | end | ||
169 | if 10 < r then | ||
170 | E("too many redirects! " .. check .. " " .. host .. " -> " .. URL) | ||
171 | return | ||
172 | end | ||
173 | local PU = url.parse(URL, defaultURL) | ||
174 | D(" " .. PU.scheme .. " :// " .. check .. " " .. host .. " -> " .. URL) | ||
175 | if not testing(PU.scheme) then D("not testing " .. PU.scheme .. " " .. host .. " -> " .. URL); return end | ||
176 | local hd = {Host = host} | ||
177 | local p, c, h, s = http.request{method = "HEAD", redirect = false, url = URL, headers = hd} | ||
178 | if nil == p then E(c .. "! " .. check .. " " .. host .. " -> " .. URL) else | ||
179 | l = h.location | ||
180 | if nil ~= l then | ||
181 | local pu = url.parse(l, defaultURL) | ||
182 | if l == URL then | ||
183 | E("redirect loop! " .. check .. " " .. host .. " -> " .. URL) | ||
184 | else | ||
185 | if nil == pu.host then | ||
186 | W("no location host! " .. check .. " " .. host .. " -> " .. URL .. " -> " .. l) | ||
187 | checkURL(host, PU.scheme .. "://" .. PU.host .. l, r + 1) | ||
188 | else | ||
189 | if testing("Protocol") and pu.scheme ~= PU.scheme then | ||
190 | W("protocol changed during redirect! " .. check .. " " .. host .. " -> " .. URL .. " -> " .. l) | ||
191 | end | ||
192 | checkURL(pu.host, l, r + 1) | ||
193 | end | ||
194 | end | ||
195 | end | ||
196 | end | ||
197 | end | ||
198 | |||
199 | local checkPaths = function (host, ip, path) | ||
200 | I(" Checking IP " .. host .. " -> " .. ip .. " " .. path) | ||
201 | for i, s in pairs(referenceDebs) do | ||
202 | if testing("http") then checkURL(host, "http://" .. ip .. path .. s) end | ||
203 | if testing("https") then checkURL(host, "https://" .. ip .. path .. s) end | ||
204 | end | ||
205 | |||
206 | for i, s in pairs(releases) do | ||
207 | for j, k in pairs(releaseFiles) do | ||
208 | if testing("http") then checkURL(host, "http://" .. ip .. path .. "merged/dists/" .. s .. k) end | ||
209 | if testing("https") then checkURL(host, "https://" .. ip .. path .. "merged/dists/" .. s .. k) end | ||
210 | end | ||
211 | end | ||
212 | end | ||
213 | |||
214 | local execute = function (s) | ||
215 | D("executing " .. s) | ||
216 | os.execute(s) | ||
217 | end | ||
218 | |||
219 | forkIP = function (orig, host) | ||
220 | if nil == host then host = orig end | ||
221 | local po = url.parse("http://" .. orig, defaultURL) | ||
222 | local ph = url.parse("http://" .. host, defaultURL) | ||
223 | gatherIPs(ph.host) | ||
224 | for k, v in pairs(IP[ph.host]) do | ||
225 | if v == "A" then | ||
226 | if testing("IPv4") then execute("sleep 1; ionice -c3 ./mirror-checker.lua " .. sendArgs .. " " .. host .. " " .. k .. " &") end | ||
227 | elseif v == "AAAA" then | ||
228 | if testing("IPv6") then execute("sleep 1; ionice -c3 ./mirror-checker.lua " .. sendArgs .. " " .. host .. " [" .. k .. "] &") end | ||
229 | elseif v == "CNAME" then | ||
230 | forkIP(orig, k) | ||
231 | end | ||
232 | end | ||
233 | end | ||
234 | |||
235 | checkIP = function (orig, host, path, ip) | ||
236 | if nil ~= ip then | ||
237 | checkPaths(orig, ip, path) | ||
238 | else | ||
239 | D("checkIP " .. orig .. " " .. host) | ||
240 | gatherIPs(host) | ||
241 | for k, v in pairs(IP[host]) do | ||
242 | if v == "A" then | ||
243 | if testing("IPv4") then checkPaths(orig, k, path) end | ||
244 | elseif v == "AAAA" then | ||
245 | if testing("IPv6") then checkPaths(orig, "[" .. k .. "]", path) end | ||
246 | elseif v == "CNAME" then | ||
247 | checkIP(orig, k, path) | ||
248 | end | ||
249 | end | ||
250 | end | ||
251 | end | ||
252 | |||
253 | local checkHost = function (host, path, ip) | ||
254 | if nil == path then path = "/" else | ||
255 | if "/" == path:sub(-1, -1) then | ||
256 | W("slash at end of BaseURL in mirror_list.txt! " .. host .. " " .. path) | ||
257 | else | ||
258 | path = path .. "/" | ||
259 | end | ||
260 | end | ||
261 | checkIP(host, host, path, ip) | ||
262 | end | ||
263 | |||
264 | local downloads = function (host, URL, IP) | ||
265 | if nil == URL then URL = "/" end | ||
266 | if nil == IP then IP = "" else IP = "-" .. IP end | ||
267 | local log = " --rejected-log=results/wget-%s_REJECTS-" .. host .. IP .. ".log -a results/wget-%s-" .. host .. IP ..".log " | ||
268 | I("starting file download commands for " .. host .. " " .. URL) | ||
269 | local cm = "ionice -c3 " .. download .. log:format("debs", "debs") | ||
270 | for i, s in pairs(referenceDebs) do | ||
271 | cm = cm .. " https://" .. host .. URL .. "/" .. s | ||
272 | end | ||
273 | for i, s in pairs(releases) do | ||
274 | execute(cm .. " &") | ||
275 | cm = "ionice -c3 " .. download .. log:format(s, s) | ||
276 | for j, k in pairs(releaseFiles) do | ||
277 | cm = cm .. " https://" .. host .. URL .. "/merged/dists/" .. s .. k | ||
278 | end | ||
279 | end | ||
280 | execute(cm .. " &") | ||
281 | end | ||
282 | |||
283 | local getMirrors = function () | ||
284 | local mirrors = {} | ||
285 | local host = "" | ||
286 | local m = {} | ||
287 | local URL = "https://" .. options.referenceSite.value .. "/mirror_list.txt" | ||
288 | I("getting mirrors.") | ||
289 | local p, c, h = http.request(URL) | ||
290 | if nil == p then E(c .. " fetching " .. URL) else | ||
291 | for l in p:gmatch("\n*([^\n]+)\n*") do | ||
292 | local t, d = l:match("(%a*):%s*(.*)") | ||
293 | if "FQDN" == t then | ||
294 | if "" ~= host then | ||
295 | mirrors[host] = m | ||
296 | m = {} | ||
297 | end | ||
298 | host = d | ||
299 | end | ||
300 | m[t] = d | ||
301 | end | ||
302 | if "" ~= host then | ||
303 | mirrors[host] = m | ||
304 | end | ||
305 | end | ||
306 | return mirrors | ||
307 | end | ||
308 | |||
309 | |||
310 | if 0 ~= #args then | ||
311 | local option = "" | ||
312 | for i, a in pairs(args) do | ||
313 | if ("--help" == a) or ("-h" == a) then | ||
314 | print("I should write some docs, huh?") | ||
315 | elseif "--version" == a then | ||
316 | print("mirror-checker-lua version 0.1 alpha") | ||
317 | elseif "-v" == a then | ||
318 | verbosity = verbosity + 1 | ||
319 | sendArgs = sendArgs .. a .. " " | ||
320 | elseif "-q" == a then | ||
321 | verbosity = -1 | ||
322 | sendArgs = sendArgs .. a .. " " | ||
323 | elseif "-k" == a then | ||
324 | keep = true | ||
325 | elseif "--" == a:sub(1, 2) then | ||
326 | local s, e = a:find("=") | ||
327 | if nil == s then e = -1 end | ||
328 | option = a:sub(3, e - 1) | ||
329 | local o = options[option] | ||
330 | if nil == o then | ||
331 | print("Unknown option --" .. option) | ||
332 | option = "" | ||
333 | else | ||
334 | option = a | ||
335 | sendArgs = sendArgs .. a .. " " | ||
336 | local s, e = a:find("=") | ||
337 | if nil == s then e = 0 end | ||
338 | option = a:sub(3, e - 1) | ||
339 | if "table" == options[option].typ then | ||
340 | local result = {} | ||
341 | for t in (a:sub(e + 1) .. ","):gmatch("([+%-]?%w*),") do | ||
342 | local f = t:sub(1, 1) | ||
343 | local n = t:sub(2, -1) | ||
344 | if ("+" ~= f) and ("-" ~= f) then | ||
345 | table.insert(result, t) | ||
346 | end | ||
347 | end | ||
348 | if 0 ~= #result then | ||
349 | options[option].value = result | ||
350 | else | ||
351 | for t in (a:sub(e + 1) .. ","):gmatch("([+%-]?%w*),") do | ||
352 | local f = t:sub(1, 1) | ||
353 | local n = t:sub(2, -1) | ||
354 | if "+" == f then | ||
355 | table.insert(options[option].value, n) | ||
356 | elseif "-" == f then | ||
357 | local r = {} | ||
358 | for i, k in pairs(options[option].value) do | ||
359 | if k ~= n then table.insert(r, k) end | ||
360 | end | ||
361 | options[option].value = r | ||
362 | end | ||
363 | end | ||
364 | end | ||
365 | else | ||
366 | options[option].value = a | ||
367 | end | ||
368 | option = "" | ||
369 | end | ||
370 | elseif "-" == a:sub(1, 1) then | ||
371 | print("Unknown option " .. a) | ||
372 | else | ||
373 | table.insert(arg, a) | ||
374 | end | ||
375 | end | ||
376 | end | ||
377 | |||
378 | --printTable(options.tests.value, "", "tests") | ||
379 | |||
380 | execute("mkdir -p results") | ||
381 | |||
382 | if 0 < #arg then | ||
383 | if nil ~= arg[2] | ||
384 | log = io.open ("mirror-checker-lua_" .. arg[1] .. "_" .. arg[2] .. ".log", "a+") | ||
385 | else | ||
386 | log = io.open ("mirror-checker-lua_" .. arg[1] .. ".log", "a+" [, mode]) | ||
387 | end | ||
388 | local pu = url.parse("http://" .. arg[1], defaultURL) | ||
389 | I("Starting tests for " ..pu.host .. " with these tests - " .. table.concat(options.tests.value, ", ")) | ||
390 | if nil ~= arg[2] then I(" Using IP " .. arg[2]) end | ||
391 | if testing("Integrity") or testing("Updated") then | ||
392 | if not keep then execute("rm -fr results/" .. pu.host) end | ||
393 | downloads(pu.host, pu.path, arg[2]) | ||
394 | checkExes("mirror-checker.lua " .. sendArgs) | ||
395 | checkExes(download) | ||
396 | end | ||
397 | checkHost(pu.host, pu.path, arg[2]) | ||
398 | else | ||
399 | if not keep then os.execute("rm -f results/*.log") end | ||
400 | log = io.open ("mirror-checker-lua.log", "a+") | ||
401 | I("Starting tests " .. table.concat(options.tests.value, ", ")) | ||
402 | execute("mkdir -p results") | ||
403 | local mirrors = getMirrors() | ||
404 | mirrors[options.referenceSite.value] = nil | ||
405 | -- checkHost(options.referenceSite.value) | ||
406 | forkIP(options.referenceSite.value) | ||
407 | -- checkHost("deb.devuan.org") | ||
408 | forkIP("deb.devuan.org") | ||
409 | for k, m in pairs(mirrors) do | ||
410 | local pu = url.parse("http://" .. m.BaseURL, defaultURL) | ||
411 | -- checkHost(pu.host) | ||
412 | forkIP(m.BaseURL) | ||
413 | checkExes("mirror-checker.lua " .. sendArgs) | ||
414 | if testing("Integrity") or testing("Updated") then checkExes(download) end | ||
415 | end | ||
416 | while 1 <= checkExes("mirror-checker.lua " .. sendArgs) do os.execute("sleep 30") end | ||
417 | if testing("Integrity") or testing("Updated") then | ||
418 | while 1 < checkExes(download) do os.execute("sleep 30") end | ||
419 | end | ||
420 | end | ||