Skip to content

Commit b8844b0

Browse files
committed
fix: cloud-init
1 parent a88af12 commit b8844b0

14 files changed

Lines changed: 451 additions & 38 deletions

‎system/cuos/api.test.sh‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/bash
2+
3+
SCRIPT_DIR="$( cd -- "$( dirname -- "$( readlink -f "${BASH_SOURCE[0]}" )" )" &> /dev/null && pwd)"
4+
5+
set -euo pipefail
6+
7+
echo "[Test] API help"
8+
9+
"${SCRIPT_DIR}/api.sh" help >/dev/null || exit 1
10+
Lines changed: 196 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,204 @@
11
#!/bin/bash
2+
set -euo pipefail
23

3-
# Input: cloud-init YAML file
4-
INPUT_FILE="$1"
5-
# Output: System JSON
6-
OUTPUT_FILE="$2"
4+
# Usage:
5+
# ./cloud-init-to-system-json.sh <user-data.yaml> [network-config]
6+
#
7+
# Input:
8+
# USER_DATA_FILE: cloud-init user-data YAML (may include hostname, users, ntp, and optionally network)
9+
# NETWORK_DATA_FILE (optional): cloud-init network-config YAML (v1 or v2). If given, its network overrides user-data.
10+
#
11+
# Output:
12+
# Prints the resulting system-json to stdout.
713

8-
if [[ -z "${INPUT_FILE}" ]]; then
9-
echo "Input file not found."
10-
exit 2
14+
USER_DATA_FILE="${1:-}"
15+
NETWORK_DATA_FILE="${2:-}"
16+
17+
if [[ -z "$USER_DATA_FILE" ]]; then
18+
echo "Usage: $0 <user-data.yaml> [network-config]" >&2
19+
exit 1
20+
fi
21+
22+
if ! command -v yq >/dev/null 2>&1; then
23+
echo "ERROR: 'yq' (mikefarah v4) is required." >&2
24+
exit 2
1125
fi
12-
if [[ -z "${OUTPUT_FILE}" ]]; then
13-
echo "Output file not found."
14-
exit 2
26+
27+
if ! command -v jq >/dev/null 2>&1; then
28+
echo "ERROR: 'jq' is required." >&2
29+
exit 2
1530
fi
1631

17-
yq '.ntp.servers as $ntp | . * {
18-
network: (
19-
.network.config // [] | map({
20-
"dhcp": .dhcp4,
21-
"ip-address": (if .addresses and (.addresses | length > 0) then (.addresses[0] | split("/")[0]) else null end),
22-
"network-mask": (if .addresses and (.addresses | length > 0) then
23-
(
24-
(.addresses[0] | split("/")[1]) as $cidr |
32+
yaml2json() {
33+
local file="$1"
34+
if [ -n "${file:-}" ] && [ -f "$file" ]; then
35+
yq eval -o=json "${file}" 2>/dev/null || yq . "${file}"
36+
else
37+
echo '{}'
38+
fi
39+
}
40+
41+
# Prepare a merged JSON stream for conversion:
42+
# - If network-config is provided, extract its 'network' block (or treat the whole document as the network block if root is 'network').
43+
# Then merge it into user-data as '.network'.
44+
45+
jq -n --slurpfile u <(yaml2json "$USER_DATA_FILE") \
46+
--slurpfile n <(yaml2json "$NETWORK_DATA_FILE") \
47+
'
48+
($n[0].network // $n[0]) as $net |
49+
if ($net | length) == 0 then
50+
$u[0]
51+
else
52+
$u[0] + {network: $net}
53+
end
54+
' | jq '
55+
56+
# Helpers
57+
def as_array: if . == null then [] elif (type=="array") then . else [.] end;
58+
59+
def cidr_to_ip_and_mask($cidr):
60+
($cidr|tostring) as $c
61+
| ($c | split("/")) as $parts
62+
| if ($parts|length) != 2 then error("Invalid CIDR: \($c)") else
63+
($parts[0]) as $ip
64+
| ($parts[1]|tonumber) as $p
65+
| def oct(p):
66+
if p >= 8 then 255
67+
elif p <= 0 then 0
68+
else 256 - (pow(2; (8 - p)))
69+
end;
70+
[ oct($p), oct($p-8), oct($p-16), oct($p-24) ] as $mask_arr
71+
| { ip: $ip, mask: ($mask_arr | map(tostring) | join(".")) }
72+
end;
73+
74+
def parse_chpasswd_list:
75+
if . == null then {}
76+
else ( tostring
77+
| split("\n")
78+
| map(select(length>0))
79+
| map( (split(":"))
80+
| if (length>=2) then { (.[0]): (.[1:] | join(":")) } else {} end
81+
)
82+
| add )
83+
end;
84+
85+
def pick_password:
86+
(.users // []) as $users
87+
| (.chpasswd.list // null | parse_chpasswd_list) as $kv
88+
| (
89+
($users | map(select(type=="object" and (.passwd!=null)))) as $hits
90+
| if ($hits|length)>0 then $hits[0].passwd
91+
else $kv["system-admin"]
92+
end
93+
);
94+
95+
# Build interface list for v2
96+
def build_ifaces_v2:
97+
(.network.ethernets // {} | to_entries | sort_by(.key) | map(.value));
98+
99+
# Dotted mask -> prefix (for v1 static without CIDR)
100+
def dotted_to_prefix($m):
101+
($m|tostring|split(".")|map(tonumber)) as $o
102+
| def ones(x):
103+
if x==255 then 8
104+
elif x==254 then 7
105+
elif x==252 then 6
106+
elif x==248 then 5
107+
elif x==240 then 4
108+
elif x==224 then 3
109+
elif x==192 then 2
110+
elif x==128 then 1
111+
elif x==0 then 0
112+
else error("Invalid netmask octet: \(x)")
113+
end;
114+
(ones($o[0]) + ones($o[1]) + ones($o[2]) + ones($o[3]));
115+
116+
# Build interface list for v1 (legacy) from network.config
117+
def build_ifaces_v1:
118+
(.network.config // []) as $cfg
119+
| ($cfg | map(select(type=="object" and (.type//"")=="physical"))) as $phys
120+
| ($phys | sort_by(.name // ""))
121+
| map(
122+
. as $p
123+
| ( ($p.subnets // []) | if (length>0) then .[0] else {} end ) as $s
124+
| ($s.type // "") as $stype
125+
| if $stype == "dhcp" or $stype == "dhcp4" then
25126
{
26-
"0": "0.0.0.0", "1": "128.0.0.0", "2": "192.0.0.0", "3": "224.0.0.0",
27-
"4": "240.0.0.0", "5": "248.0.0.0", "6": "252.0.0.0", "7": "254.0.0.0",
28-
"8": "255.0.0.0", "9": "255.128.0.0", "10": "255.192.0.0", "11": "255.224.0.0",
29-
"12": "255.240.0.0", "13": "255.248.0.0", "14": "255.252.0.0", "15": "255.254.0.0",
30-
"16": "255.255.0.0", "17": "255.255.128.0", "18": "255.255.192.0", "19": "255.255.224.0",
31-
"20": "255.255.240.0", "21": "255.255.248.0", "22": "255.255.252.0", "23": "255.255.254.0",
32-
"24": "255.255.255.0", "25": "255.255.255.128", "26": "255.255.255.192", "27": "255.255.255.224",
33-
"28": "255.255.255.240", "29": "255.255.255.248", "30": "255.255.255.252", "31": "255.255.255.254",
34-
"32": "255.255.255.255"
35-
}[$cidr]
36-
)
37-
else null end),
38-
"gateway": .gateway4,
39-
"dns-server": .nameservers.addresses,
40-
"ntp-server": $ntp
41-
} | with_entries(select(.value != null)))
42-
),
43-
}' "${INPUT_FILE}" > "${OUTPUT_FILE}"
127+
dhcp4: true,
128+
nameservers: { addresses: (($s.dns_nameservers // ($cfg[] | select(.type=="nameserver") | .address) // []) | as_array | map(tostring)) }
129+
}
130+
elif $stype == "static" then
131+
(
132+
($s.address // null) as $addr
133+
| ($s.netmask // null) as $mask
134+
| ($s.gateway // null) as $gw
135+
| ($s.dns_nameservers // ($cfg[] | select(.type=="nameserver") | .address) // []) as $dns
136+
| if $addr == null then
137+
error("v1 static subnet missing address")
138+
else
139+
if ($addr | tostring | contains("/")) then
140+
{ addresses: [ ($addr|tostring) ] }
141+
else
142+
if $mask == null then
143+
error("v1 static subnet missing netmask for address \($addr)")
144+
else
145+
(dotted_to_prefix($mask)) as $pfx
146+
| { addresses: [ ($addr|tostring) + "/" + ($pfx|tostring) ] }
147+
end
148+
end
149+
end
150+
| {
151+
dhcp4: false
152+
}
153+
+ .
154+
+ ( if $gw != null then { gateway4: ($gw|tostring) } else {} end )
155+
+ { nameservers: { addresses: ($dns | as_array | map(tostring)) } }
156+
)
157+
else
158+
error("v1 subnet type unsupported or missing for interface \($p.name // "unknown")")
159+
end
160+
);
161+
162+
# iface -> target schema
163+
def iface_to_entry($ntp_servers):
164+
. as $iface
165+
| ($iface.dhcp4 // false) as $dhcp
166+
| ($iface.nameservers.addresses | as_array | map(tostring)) as $dns
167+
| if $dhcp then
168+
{ dhcp: true }
169+
else
170+
(
171+
($iface.addresses | as_array) as $addrs
172+
| if ($addrs|length)==0 then error("Static interface missing addresses[]") else . end
173+
| ($addrs[0] | tostring) as $cidr
174+
| (cidr_to_ip_and_mask($cidr)) as $am
175+
| {
176+
dhcp: false,
177+
"ip-address": $am.ip,
178+
"network-mask": $am.mask
179+
}
180+
+ ( if $iface.gateway4 then { gateway: ($iface.gateway4|tostring) } else {} end )
181+
)
182+
end
183+
| . as $base
184+
| $base
185+
+ ( if ($dns|length)>0 then { "dns-server": ($dns | join(" ")) } else {} end )
186+
+ ( if ($ntp_servers|length)>0 then { "ntp-server": ($ntp_servers|map(tostring)|join(" ")) } else {} end );
44187
188+
# Entry point
189+
(.fqdn // .hostname) as $hostname
190+
| (.ntp.servers | as_array | map(tostring)) as $ntp_servers
191+
| (
192+
if ((.network.version // 2) == 1) then
193+
build_ifaces_v1
194+
else
195+
build_ifaces_v2
196+
end
197+
) as $ifaces
198+
| ($ifaces | map(iface_to_entry($ntp_servers))) as $network
199+
| (.password // pick_password) as $pwd
200+
| {}
201+
+ (if $hostname then {hostname:$hostname} else {} end)
202+
+ (if ($network|length)>0 then {network:$network} else {} end)
203+
+ (if $pwd!=null then {system_admin_password:$pwd} else {} end)
204+
'
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#cloud-config
2+
hostname: test-system3
3+
manage_etc_hosts: true
4+
fqdn: test-system3.example.com
5+
password: $6$hashhashhash
6+
chpasswd:
7+
expire: False
8+
users:
9+
- default
10+
package_upgrade: true
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"hostname": "test-system3.example.com",
3+
"network": [
4+
{
5+
"dhcp": true,
6+
"dns-server": "10.100.248.1 10.100.187.1"
7+
},
8+
{
9+
"dhcp": false,
10+
"ip-address": "192.168.7.8",
11+
"network-mask": "255.255.255.0",
12+
"gateway": "192.168.7.1",
13+
"dns-server": "10.100.248.1 10.100.187.1"
14+
}
15+
],
16+
"system_admin_password": "$6$hashhashhash"
17+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
version: 1
2+
config:
3+
- type: physical
4+
name: eth0
5+
mac_address: '52:54:00:29:50:bb'
6+
subnets:
7+
- type: dhcp4
8+
- type: dhcp6
9+
- type: physical
10+
name: eth1
11+
mac_address: '52:54:00:57:43:2b'
12+
subnets:
13+
- type: static
14+
address: '192.168.7.8'
15+
netmask: '255.255.255.0'
16+
gateway: '192.168.7.1'
17+
- type: static6
18+
address: '2607:f8b0:4005:805::200e/64'
19+
gateway: '2607:f8b0:4005:805::1'
20+
- type: nameserver
21+
address:
22+
- '10.100.248.1'
23+
- '10.100.187.1'
24+
search:
25+
- 'example.com'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#cloud-config
2+
hostname: rpi-test
3+
manage_etc_hosts: true
4+
packages:
5+
- avahi-daemon
6+
apt:
7+
conf: |
8+
Acquire {
9+
Check-Date "false";
10+
};
11+
12+
users:
13+
- name: pi
14+
groups: users,adm,dialout,audio,netdev,video,plugdev,cdrom,games,input,gpio,spi,i2c,render,sudo
15+
shell: /bin/bash
16+
lock_passwd: false
17+
passwd: $6$hashhashhash
18+
ssh_authorized_keys:
19+
- ssh-rsa AAAA1234567890
20+
sudo: ALL=(ALL) NOPASSWD:ALL
21+
22+
23+
timezone: Europe/Berlin
24+
runcmd:
25+
- localectl set-x11-keymap "de" pc105
26+
- setupcon -k --force || true
27+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"hostname": "rpi-test",
3+
"system_admin_password": "$6$hashhashhash"
4+
}

0 commit comments

Comments
 (0)