hydra packet importer: load keys from metadata, not ssh-keyscan
Graham Christensen committed
Oct 6, 2019 at 21:40 UTC
fe57400644731acc418701b58da9cb774f5ab815
2 files changed
+57
-16
hydra-packet-importer/README.md
new
+27
@@ -0,0 +1,27 @@
1
+Imports builders' and their SSH keys from the Packet API. Requires the
2
+builder run this script at startup:
3
+
4
+```bash
5
+#!/usr/bin/env nix-shell
6
+#!nix-shell -i bash -p curl jq
7
+
8
+set -eux
9
+
10
+root_url=$(curl https://metadata.packet.net/metadata | jq -r .phone_home_url | rev | cut -d '/' -f2- | rev)
11
+url="$root_url/events"
12
+
13
+
14
+tell() {
15
+ data=$(
16
+ echo "{}" \
17
+ | jq '.state = $state | .code = ($code | tonumber) | .message = $message' \
18
+ --arg state "$1" \
19
+ --arg code "$2" \
20
+ --arg message "$3"
21
+ )
22
+
23
+ curl -v -X POST -d "$data" "$url"
24
+}
25
+
26
+tell succeeded 1001 "$(cat /etc/ssh/ssh_host_ed25519_key.pub)"
27
+```
hydra-packet-importer/import.py
+30
-16
@@ -33,14 +33,42 @@ def get_devices(manager):
33
if not set(device['tags']).isdisjoint(config['skip_tags']):
34
continue
35
36
+ host_key = get_device_key(manager, device)
37
+ if host_key is None:
38
+ continue
39
+
40
devices.append({
41
"hostname": device['hostname'],
42
"address": "{}.packethost.net".format(device['short_id']),
39
- "type": device['plan']['name']
43
+ "type": device['plan']['name'],
44
+ "host_key": host_key,
45
})
46
47
return devices
48
49
+def get_device_key(manager, device):
50
+ # ... 50 is probably enough.
51
+ events_url = 'devices/{}/events?per_page=50'.format(device['id'])
52
+ debug(events_url)
53
+ data = manager.call_api(events_url)
54
+
55
+ ssh_key = None
56
+ for event in data['events']:
57
+ if event['type'] == 'provisioning.104.01':
58
+ # we reached a "Device connected to DHCP system" event,
59
+ # indicating a reboot.
60
+ #
61
+ # The most first SSH key after DHCP is the one we want,
62
+ # in case someone sends a bogus SSH key to the metadata
63
+ # API after the post-boot hook.
64
+ #
65
+ # If we receive a LOT of spam (> 50 spams!) like that, we
66
+ # will return None because we never reach this message.
67
+ return ssh_key
68
+ if event['type'] == 'user.1001':
69
+ ssh_key = event['body']
70
+ return None
71
+
72
def main(config):
73
rows = []
74
manager = packet.Manager(auth_token=config['token'])
@@ -64,20 +92,6 @@ def main(config):
92
lookup = lambda key: specific_stats.get(key, device.get(key, default_stats.get(key)))
93
lookup_default = lambda key, default: default if not lookup(key) else lookup(key)
94
67
- r = subprocess.check_output([
68
- "ssh-keyscan",
69
- "-4", # force IPv4
70
- "-T", "5", # Timeout 5 seconds
71
- "-t", "ed25519", # Only ed25519 keys
72
- lookup("address")
73
- ]).decode("utf-8")
74
-
75
- elems = r.split(" ", 1)
76
- if len(elems) != 2:
77
- debug("# Skipped due keyscan failed to split on ' '")
78
- continue
79
- key = elems[1]
80
-
95
# root@address system,list /var/lib/ssh.key maxJobs speedFactor feature,list mandatory,features public-host-key
96
rows.append(" ".join([
97
"{user}@{host}".format(user=lookup("user"),host=lookup("address")),
@@ -87,7 +101,7 @@ def main(config):
101
str(lookup("speed_factor")),
102
",".join(lookup_default("features", ["-"])),
103
",".join(lookup_default("mandatory_features", ["-"])),
90
- base64.b64encode(key.encode()).decode("utf-8")
104
+ base64.b64encode(device['host_key'].encode()).decode("utf-8")
105
]))
106
107
debug("# {} / {}".format(len(rows),found))