Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

systemd: Add lastlog2 support for "last login" health card item #21176

Merged
merged 5 commits into from
Oct 30, 2024

Conversation

martinpitt
Copy link
Member

@martinpitt martinpitt commented Oct 28, 2024

Latest debian testing dropped support for the old wtmp/lastlog stack. Other distributions will follow soon.

Blocks cockpit-project/bots#7033

The extra debian-testing/{other,expensive}@Bots#7033 tests here prove that this works.

@martinpitt
Copy link
Member Author

martinpitt commented Oct 28, 2024

This lastlog2 parsery is just gross.. I'll reattempt this with Python, I just discovered that sqlite3 is an included battery.

# python3 -m sqlite3 /var/lib/lastlog/lastlog2.db 'SELECT * FROM Lastlog2'
('debian', 1730109226, '', '::ffff:172.27.0.2', 'cockpit')
('admin', 1730120286, '', '::ffff:172.27.0.2', 'cockpit')
('root', 1730123689, 'ssh', '172.27.0.2', 'sshd')

Update: As the failures on old OSes show, it's not only gross, but wrong:

# lastlog
Username         Port     From                                       Latest
root             pts/0    172.27.0.2                                Mon Oct 28 16:01:08 +0000 2024
daemon                                                              **Never logged in**

There is no hope of parsing this properly. I'll leave lastlog1 parsing as-is, and re-do lastlog2 as SQL.

@martinpitt martinpitt force-pushed the health-last-login branch 2 times, most recently from f0a1291 to 8d31bbc Compare October 29, 2024 09:46
@martinpitt martinpitt marked this pull request as ready for review October 29, 2024 09:46
@martinpitt
Copy link
Member Author

martinpitt commented Oct 29, 2024

This flake is rather annoying.. As this test touches the users page, they all have to pass 3x. "Unexpected error when getting locked account information" sounds rather unrelated.

I tried this locally, and it passed 10 times in a row. Polluted VM from a previous test? -- No, I ran the exact same tests in the same order as in CI, and it passes.

Failed to try-restart sssd.service: Unit sssd.service not found.

warn: Unexpected error when getting locked account information Object(5)

These happen in successful runs, too..

Job for systemd-logind.service canceled.

This on the other hand doesn't, so may be related to the terminate_sessions() failure.

Moving to draft, and amplifying/debugging.

@martinpitt martinpitt self-assigned this Oct 29, 2024
@martinpitt martinpitt marked this pull request as draft October 29, 2024 18:34
@martinpitt martinpitt removed the request for review from mvollmer October 29, 2024 18:34
@martinpitt martinpitt added the no-test For doc/workflow changes, or experiments which don't need a full CI run, label Oct 30, 2024
@martinpitt
Copy link
Member Author

Cool, works! Good enough.

@martinpitt martinpitt removed the no-test For doc/workflow changes, or experiments which don't need a full CI run, label Oct 30, 2024
@martinpitt martinpitt marked this pull request as ready for review October 30, 2024 07:36
This avoids calling `cockpit.LoginMessages` and `cockpit.user` multiple
times during each initialization, and simplifies the structure.
We'll soon need more information (tty and remote host) from lastlog2.
This is impossible to parse from the `lastlog[2]` CLI: With modern
versions and their variable-width multi-space-separated columns it's
merely hard and gross, but older `lastlog` versions misalign the
columns.

So give up on this, and read lastlog2's sqlite database directly.
Fortunately Python comes with the `sqlite3` module built in.
This also gets rid of the brittle date/time parsing.

Make this a library, as we will use it from the Overview page as well.
Phase out the last login information from cockpit-session. With distributions
moving to lastlog2 [1], we can get that information from the Overview page
directly.

For now, support both sources.

[1] cockpit-project/bots#7033
Otherwise in our test logs it comes out as "Object(5)", which is
useless.
Log out the root session at the end. This helps with stopping logind
in our test cleanup.

DRY, use `login_and_go()` to drive the login page.

con = sqlite3.connect(DB)
cur = con.cursor()
query = "SELECT Name, Time, TTY, RemoteHost FROM Lastlog2"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the schema documented somewhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just got it from .schema in sqlite3. The manpage doesn't document it. Otherwise, it's just in the upstream source code.

The format doesn't look prone to changes, but of course it could happen in principle. Then our tests should pick it up and we need to deal with it.

Copy link
Member

@mvollmer mvollmer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks!

Comment on lines +30 to +31
const out = await python.spawn(lastlog2_py, user ? [user] : [], { err: "message" });
return JSON.parse(out);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test.

} else {
// empty reply is okay -- older bridges just don't send that information
if (obj.version !== undefined)
console.error("unknown login-messages:", reply);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

Comment on lines +76 to +77
} catch (error) {
console.error("failed to fetch login messages:", error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test.

Comment on lines +81 to +82
setLastlog((await getLastlog2(user.name))[user.name]);
} catch (error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test.


if (messages === null || !messages['last-login-time']) {
const lastLoginTime = messages?.['last-login-time'] ?? lastlog?.time;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

Comment on lines +91 to +92
const lastLoginFrom = messages?.['last-login-host'] || lastlog?.from;
const lastLoginPort = messages?.['last-login-line'] || lastlog?.port;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test.

@@ -207,43 +208,42 @@ async function getLoginDetails(logind_client) {
details[name] = { ...details[name], isLocked: hash.startsWith("!") };
}
} catch (err) {
console.warn("Unexpected error when getting locked accounts from /etc/shadow:", err);
console.warn("Unexpected error when getting locked accounts from /etc/shadow:", err.toString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

Comment on lines +222 to +224
for (const [name, last] of Object.entries(await getLastlog2()))
details[name] = { ...details[name], lastLogin: last.time * 1000 };
} catch (err) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 added lines are not executed by any test.

Comment on lines +240 to +241
} catch (e) {
console.warn(`Failed to parse date from lastlog line '${line}': ${e.toString()}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test.

Comment on lines +244 to +245
} catch (ex) {
console.warn(`Failed to run lastlog: ${ex.toString()}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test.

@martinpitt martinpitt merged commit fc28500 into cockpit-project:main Oct 30, 2024
86 of 87 checks passed
@martinpitt martinpitt deleted the health-last-login branch October 30, 2024 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants