Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/tailscale/migrations/0003_alter_devicesnapshot_last_seen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.2.2 on 2025-10-09 21:02

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("tailscale", "0002_devicesnapshot_device_alter_device_latest_snapshot"),
]

operations = [
migrations.AlterField(
model_name="devicesnapshot",
name="last_seen",
field=models.DateTimeField(
blank=True, help_text="When device was last active on the tailnet.", null=True
),
),
]
4 changes: 3 additions & 1 deletion apps/tailscale/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ class DeviceSnapshot(models.Model):
null=True, blank=True, help_text="The expiration date of the device's auth key."
)
hostname = models.CharField(max_length=255, help_text="The machine name in the admin console.")
last_seen = models.DateTimeField(help_text="When device was last active on the tailnet.")
last_seen = models.DateTimeField(
null=True, blank=True, help_text="When device was last active on the tailnet."
)
name = models.CharField(max_length=255, help_text="The MagicDNS name of the device.")
node_id = models.CharField(max_length=128, help_text="The preferred identifier for a device.")
os = models.CharField(
Expand Down
19 changes: 11 additions & 8 deletions dagster_publish_mdm/assets/tailscale/tailscale_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def tailscale_append_device_snapshot_table(
created=device["created"],
expires=expires,
hostname=device["hostname"],
last_seen=device["lastSeen"],
last_seen=device.get("lastSeen"),
name=device["name"],
node_id=device["nodeId"],
os=device["os"],
Expand Down Expand Up @@ -111,13 +111,16 @@ def stale_tailscale_devices(
hostname = device.get("hostname")
device_id = device.get("id")

try:
seen_time = dt.datetime.fromisoformat(last_seen.replace("Z", "+00:00"))
if seen_time < time_delta:
context.log.info(f"Device {hostname} last seen at {seen_time} — marking as stale.")
stale_devices.append(device)
except Exception as e:
context.log.warning(f"Failed to process device {hostname}: (ID: {device_id}) {e}")
if last_seen:
try:
seen_time = dt.datetime.fromisoformat(last_seen.replace("Z", "+00:00"))
if seen_time < time_delta:
context.log.info(
f"Device {hostname} last seen at {seen_time} — marking as stale."
)
stale_devices.append(device)
except Exception as e:
context.log.warning(f"Failed to process device {hostname}: (ID: {device_id}) {e}")

context.add_output_metadata({"Stale Devices Preview": stale_devices[:2]})
return stale_devices
Expand Down
39 changes: 39 additions & 0 deletions tests/dagster_publish_mdm/tailscale/test_tailscale_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ def test_device_no_expiration(devices):
assert device.expires is None


@pytest.mark.django_db
def test_device_with_no_last_seen(devices):
"""Test asset handles devices JSON with no last seen."""
devices["devices"][0].pop("lastSeen")
assets.tailscale_append_device_snapshot_table(
context=dg.build_asset_context(), tailscale_device_snapshot=devices
)
device = DeviceSnapshot.objects.first()
assert device.last_seen is None


@pytest.mark.django_db
def test_tailscale_insert_and_update_devices(devices):
"""Test asset inserts and updates devices."""
Expand Down Expand Up @@ -186,3 +197,31 @@ def test_stale_tailscale_devices(monkeypatch):
dg.build_asset_context(), tailscale_device_snapshot=snapshot
)
assert len(result) == 1


def test_stale_tailscale_devices_with_no_last_seen(monkeypatch):
"""Test stale devices asset handles devices with no last seen."""

now = dt.datetime.now(dt.timezone.utc)
monkeypatch.delenv("TAILSCALE_DEVICE_STALE_MINUTES", raising=False)

# Only one device: device-3, should be marked stale.
snapshot = {
"devices": [
{
"id": "1",
"hostname": "device-1",
}, # Device with no last seen, meaning its connected. Should NOT be deleted.
{
"id": "2",
"hostname": "device-2",
"lastSeen": (now - dt.timedelta(days=90, seconds=1)).strftime(
TAILSCALE_FORMAT
), # Device inactive for 90 days + 1 sec. Should be deleted.
},
]
}
result = assets.stale_tailscale_devices(
dg.build_asset_context(), tailscale_device_snapshot=snapshot
)
assert len(result) == 1