summaryrefslogtreecommitdiff
path: root/src/main/lua/paisa-fleet/RmArtifactBaseDir.lua
blob: 949d1fe986f9d51968177099a3e5d241031304bb (plain)
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381

local SL = require("scriptlee")
local newHttpClient = SL.newHttpClient
local newShellcmd = SL.newShellcmd
local newSqlite = SL.newSqlite
local objectSeal = SL.objectSeal
local parseJSON = SL.parseJSON
local sleep = SL.posix.sleep
local startOrExecute = SL.reactor.startOrExecute
SL = nil
local log = io.stdout


function printHelp()
    io.write("\n"
        .."  WARN: This is experimental.\n"
        .."  \n"
        .."  Options:\n"
        .."      --backendHost <inaddr>  (eg \"localhost\")\n"
        .."      --backendPort <int>     (eg 80)\n"
        .."      --backendPath <str>     (eg \"/houston\")\n"
        .."      --sshPort <int>         (eg 22)\n"
        .."      --sshUser <str>         (eg \"eddieuser\")\n"
        .."      --state <path>          (eg \"path/to/state\")\n"
        .."      \n"
        .."      --exportLatestStatus\n"
        .."  \n")
end


function parseArgs( app )
    app.backendPort = 80
    app.statePath = ":memory:"
    local iA = 0
    ::nextArg::
    iA = iA + 1
    local arg = _ENV.arg[iA]
    if not arg then
        goto verifyResult
    elseif arg == "--help" then
        app.isHelp = true return 0
    elseif arg == "--backendHost" then
        iA = iA + 1; arg = _ENV.arg[iA]
        if not arg then log:write("EINVAL: --backendHost needs value\n")return end
        app.backendHost = arg
    elseif arg == "--backendPort" then
        iA = iA + 1; arg = _ENV.arg[iA]
        if not arg then log:write("EINVAL: --backendPort needs value\n")return end
        app.backendHost = arg
    elseif arg == "--backendPath" then
        iA = iA + 1; arg = _ENV.arg[iA]
        if not arg then log:write("EINVAL: --backendPath needs value\n")return end
        app.backendPath = arg
    elseif arg == "--sshPort" then
        iA = iA + 1; arg = _ENV.arg[iA]
        if not arg then log:write("EINVAL: --sshPort needs value\n")return end
        app.sshPort = arg
    elseif arg == "--sshUser" then
        iA = iA + 1; arg = _ENV.arg[iA]
        if not arg then log:write("EINVAL: --sshUser needs value\n")return end
        app.sshUser = arg
    elseif arg == "--state" then
        iA = iA + 1; arg = _ENV.arg[iA]
        if not arg then log:write("EINVAL: --state needs value\n")return end
        app.statePath = arg
    elseif arg == "--exportLatestStatus" then
        app.exportLatestStatus = true
    else
        log:write("EINVAL: ".. arg .."\n")return
    end
    goto nextArg
    ::verifyResult::
    if app.exportLatestStatus then
        if not app.statePath then log:write("EINVAL: --state missing\n")return end
    else
        if not app.backendHost then log:write("EINVAL: --backendHost missing\n")return end
        if not app.backendPath then log:write("EINVAL: --backendPath missing\n")return end
        if app.backendPath:find("^C:.") then log:write("[WARN ] MSYS_NO_PATHCONV=1 likely missing? ".. app.backendPath.."\n") end
    end
    return 0
end


function removeCompletedEddies( app )
    local db = getStateDb(app)
    local rs = db:prepare("SELECT eddieName FROM Eddie"
        .."  JOIN EddieLog ON Eddie.id = eddieId"
        .."  WHERE status = \"OK\";"):execute()
    local eddieNamesToRemoveSet = {}
    while rs:next() do
        assert(rs:type(1) == "TEXT", rs:type(1))
        assert(rs:name(1) == "eddieName", rs:name(1))
        local eddieName = rs:value(1)
        eddieNamesToRemoveSet[eddieName] = true
    end
    local oldEddies = app.eddies
    app.eddies = {}
    local numKeep, numDrop = 0, 0
    for _, eddie in pairs(oldEddies) do
        if not eddieNamesToRemoveSet[eddie.eddieName] then
            --log:write("[DEBUG] Keep '".. eddie.eddieName .."'\n")
            numKeep = numKeep + 1
            table.insert(app.eddies, eddie)
        else
            numDrop = numDrop + 1
            --log:write("[DEBUG] Drop '".. eddie.eddieName .."': Already done\n")
        end
    end
    log:write("[DEBUG] todo: ".. numKeep ..", done: ".. numDrop .."\n")
end


function setEddieStatus( app, statusStr, eddieName, stderrStr, stdoutStr )
    assert(type(app) == "table")
    assert(type(eddieName) == "string")
    assert(statusStr == "OK" or statusStr == "ERROR")
    log:write("[DEBUG] setEddieStatus(".. eddieName ..", ".. statusStr ..")\n")
    local db = getStateDb(app)
    local stmt = db:prepare("INSERT INTO Eddie(eddieName)VALUES($eddieName);")
    stmt:bind("$eddieName", eddieName)
    local ok, emsg = xpcall(function()
        stmt:execute()
    end, debug.traceback)
    if not ok and not emsg:find("UNIQUE constraint failed: Eddie.eddieName") then
        error(emsg)
    end
    local stmt = db:prepare("INSERT INTO EddieLog('when',eddieId,status,stderr,stdout)"
        .."VALUES($when, (SELECT rowid FROM Eddie WHERE eddieName = $eddieName), $status, $stderr, $stdout)")
    stmt:reset()
    stmt:bind("$when", os.date("!%Y-%m-%dT%H:%M:%S+00:00"))
    stmt:bind("$eddieName", eddieName)
    stmt:bind("$status", statusStr)
    stmt:bind("$stderr", stderrStr)
    stmt:bind("$stdout", stdoutStr)
    stmt:execute()
end


function getStateDb( app )
    if not app.stateDb then
        app.stateDb = newSqlite{ database = app.statePath }
        app.stateDb:prepare("CREATE TABLE IF NOT EXISTS Eddie(\n"
            .."  id INTEGER PRIMARY KEY,\n"
            .."  eddieName TEXT UNIQUE NOT NULL)\n"
            ..";"):execute()
        app.stateDb:prepare("CREATE TABLE IF NOT EXISTS EddieLog(\n"
            .."  id INTEGER PRIMARY KEY,\n"
            .."  'when' TEXT NOT NULL,\n"
            .."  eddieId INT NOT NULL,\n"
            .."  status TEXT, -- OneOf OK, ERROR\n"
            .."  stderr TEXT NOT NULL,\n"
            .."  stdout TEXT NOT NULL)\n"
            ..";\n"):execute()
    end
    return app.stateDb
end


function loadEddies( app )
    local httpClient = newHttpClient{}
    local req = objectSeal{
        base = false,
        method = "GET",
        path = app.backendPath .."/data/preflux/inventory",
        rspCode = false,
        rspBody = false,
        isDone = false,
    }
    req.base = httpClient:request{
        cls = req,
        host = app.backendHost, port = app.backendPort,
        method = req.method, url = req.path,
        onRspHdr = function( rspHdr, req )
            req.rspCode = rspHdr.status
            if rspHdr.status ~= 200 then
                log:write(".-----------------------------------------\n")
                log:write("| ".. req.method .." ".. req.path .."\n")
                log:write("| Host: ".. app.backendHost ..":".. app.backendPort .."\n")
                log:write("+-----------------------------------------\n")
                log:write("|  ".. rspHdr.proto .." ".. rspHdr.status .." ".. rspHdr.phrase .."\n")
                for i,h in ipairs(rspHdr.headers) do
                    log:write("|  ".. h[1] ..": ".. h[2] .."\n")
                end
                log:write("|  \n")
            end
        end,
        onRspChunk = function( buf, req )
            if req.rspCode ~= 200 then log:write("|  ".. buf:gsub("\n", "\n|  ")) return end
            if buf then
                if not req.rspBody then req.rspBody = buf
                else req.rspBody = req.rspBody .. buf end
            end
        end,
        onRspEnd = function( req )
            if req.rspCode ~= 200 then log:write("\n'-----------------------------------------\n") end
            req.isDone = true
        end,
    }
    req.base:closeSnk()
    assert(req.isDone)
    if req.rspCode ~= 200 then log:write("ERROR: Couldn't load eddies\n")return end
    local prefluxInventory = parseJSON(req.rspBody)
    local eddies = {}
    for eddieName, detail in pairs(prefluxInventory.hosts) do
        table.insert(eddies, objectSeal{
            eddieName = eddieName,
            lastSeen = detail.lastSeen:value(),
        })
    end
    app.eddies = eddies
end


function makeWhateverWithEddies( app )
    local ssh = "C:/Users/fankhauseand/.opt/gitPortable-2.27.0-x64/usr/bin/ssh.exe"
    local cmdLinePre = ssh .." -oConnectTimeout=3 -oRemoteCommand=none"
    if app.sshPort then cmdLinePre = cmdLinePre .." -p".. app.sshPort end
    if app.sshUser then cmdLinePre = cmdLinePre .." \"-oUser=".. app.sshUser .."\"" end
    for k,eddie in pairs(app.eddies) do
        local eddieName = eddie.eddieName
        local isEddie = eddieName:find("^eddie%d%d%d%d%d$")
        local isTeddie = eddieName:find("^teddie%d%d$")
        local isVted = eddieName:find("^vted%d%d$")
        local isAws = eddieName:find("^10.117.%d+.%d+$")
        local isDevMachine = eddieName:find("^w00[a-z0-9][a-z0-9][a-z0-9]$")
        if isAws or isDevMachine or isVted then
            log:write("[DEBUG] Skip \"".. eddieName .."\"\n")
            goto nextEddie
        end
        assert(isEddie or isTeddie, eddieName or"nil")
        local okMarker = "OK_".. math.random(10000000, 99999999) .."wCAkgQQA2AJAzAIA"
        local cmdLine = cmdLinePre .." ".. eddieName
            .." -- \"true"
            ..    " && if test \"".. eddieName .."\" != \"$(hostname|sed 's,.pnet.ch$,,'); then true\""
            ..        " && echo WrongHost expected=".. eddieName .." actual=$(hostname|sed 's,.pnet.ch$,,') && false"
            ..        " ;fi"
            ..    " && echo hostname=$(hostname|sed 's,.pnet.ch,,')"
            ..    " && echo stage=${PAISA_ENV:?}"
            ..    " && echo Scan /data/instances/default/??ARTIFACT_BASE_DIR?"
            --[[report only]]
            --..    " && test -e /data/instances/default/??ARTIFACT_BASE_DIR? && ls -Ahl /data/instances/default/??ARTIFACT_BASE_DIR?"
            --[[Find un-/affected eddies]]
            ..    " && if test -e /data/instances/default/??ARTIFACT_BASE_DIR?; then true"
            ..        " ;else true"
            ..        " && echo ".. okMarker
            ..        " ;fi"
            --[[DELETE them]]
            --..    " && if test -e /data/instances/default/??ARTIFACT_BASE_DIR?; then true"
            --..        " && find /data/instances/default/??ARTIFACT_BASE_DIR? -type d -mtime +420 -print -delete"
            --..        " ;fi"
            --..    " && echo ".. okMarker ..""
            --[[]]
            ..    " \""
        log:write("\n")
        log:write("[INFO ] Try ".. eddieName .." ...\n")
        log:write("[DEBUG] ".. cmdLine.."\n")
        --log:write("[DEBUG] sleep ...\n")sleep(3)
        local isStdioDone, isSuccess, stderrStr, stdoutStr = false, false, "", ""
        local cmd = newShellcmd{
            cmdLine = cmdLine,
            onStdout = function( buf )
                if buf then
                    if buf:find("\n"..okMarker.."\n",0,true) then isSuccess = true end
                    stdoutStr = stdoutStr .. buf
                    io.stdout:write(buf)
                else isStdioDone = true end
            end,
            onStderr = function( buf )
                stderrStr = buf and stderrStr .. buf or stderrStr
                io.stderr:write(buf or"")
            end,
        }
        cmd:start()
        cmd:closeSnk()
        local exitCode, signal = cmd:join(42)
        if exitCode ~= 0 and signal ~= nil then
            log:write("[WARN ] code="..tostring(exitCode)..", signal="..tostring(signal).."\n")
        end
        while not isStdioDone do sleep(0.042) end
        -- Analyze outcome
        if not isSuccess then
            setEddieStatus(app, "ERROR", eddieName, stderrStr, stdoutStr)
            goto nextEddie
        end
        setEddieStatus(app, "OK", eddieName, stderrStr, stdoutStr)
        ::nextEddie::
    end
end


function sortEddiesMostRecentlySeenFirst( app )
    table.sort(app.eddies, function(a, b) return a.lastSeen > b.lastSeen end)
end


function quoteCsvVal( v )
    local typ = type(v)
    if false then
    elseif typ == "string" then
        if v:find("[\"\r\n]",0,false) then
            v = '"'.. v:gsub('"', '""') ..'"'
        end
    else error("TODO_a928rzuga98oirh "..typ)end
    return v
end


function exportLatestStatus( app )
    local snk = io.stdout
    local db = getStateDb(app)
    local stmt = db:prepare("SELECT \"when\",eddieName,status,stderr,stdout FROM EddieLog"
        .." JOIN Eddie ON Eddie.id = eddieId"
        .." ORDER BY eddieId,[when]"
        .." ;")
    rs = stmt:execute()
    snk:write("c;when;eddieName;status;stderr;stdout\n")
    local prevWhen, prevEddieName, prevStatus, prevStderr, prevStdout
    local qt = quoteCsvVal
    while rs:next() do
        local when       , eddieName  , status     , stderr     , stdout
            = rs:value(1), rs:value(2), rs:value(3), rs:value(4), rs:value(5)
          --log:write("[DEBUG] "..tostring(when).."  "..tostring(eddieName).."  "..tostring(status).."\n")
        assert(when and eddieName and status and stderr and stdout)
        if eddieName == prevEddieName then
            if not prevWhen or when > prevWhen then
                --log:write("[DEBUG] ".. when .."  ".. eddieName .."  take\n")
                goto assignPrevThenNextEntry
            else
                --log:write("[DEBUG] ".. when .."  ".. eddieName .."  obsolete\n")
                goto nextEntry
            end
        elseif prevEddieName then
            --log:write("[DEBUG] ".. when .."  ".. eddieName .."  Eddie complete\n")
            snk:write("r;".. qt(when) ..";".. qt(eddieName) ..";".. qt(status) ..";".. qt(stderr) ..";".. qt(stdout) .."\n")
        else
            --log:write("[DEBUG] ".. when .."  ".. eddieName .."  Another eddie\n")
            goto assignPrevThenNextEntry
        end
        ::assignPrevThenNextEntry::
        --[[]] prevWhen, prevEddieName, prevStatus, prevStderr, prevStdout
            =  when    , eddieName    , status    , stderr    , stdout
        ::nextEntry::
    end
    snk:write("t;status;OK\n")
end


function run( app )
    if app.exportLatestStatus then
        exportLatestStatus(app)
        return
    end
    loadEddies(app)
    assert(app.eddies)
    removeCompletedEddies(app)
    sortEddiesMostRecentlySeenFirst(app)
    makeWhateverWithEddies(app)
end


function main()
    local app = objectSeal{
        isHelp = false,
        backendHost = false,
        backendPort = false,
        backendPath = false,
        sshPort = false,
        sshUser = false,
        statePath = false,
        stateDb = false,
        exportLatestStatus = false,
        eddies = false,
    }
    if parseArgs(app) ~= 0 then os.exit(1) end
    if app.isHelp then printHelp() return end
    run(app)
end


startOrExecute(main)