Skip to content

Commit 46a8874

Browse files
committed
Support VolumeConfig for EPHEMERAL partition (including RAID devices)
1 parent 32cb400 commit 46a8874

File tree

7 files changed

+168
-6
lines changed

7 files changed

+168
-6
lines changed

app/controllers/machine_configs_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def create
2424
:private_ip,
2525
:already_configured,
2626
:install_disk,
27+
:ephemeral_disk_identifier,
2728
)
2829
@machine_config = MachineConfig.new(machine_config_params)
2930
@server = @machine_config.server

app/models/machine_config.rb

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def initialize(message, output)
2323
validates_presence_of :private_ip
2424
validate :validate_private_ip_format
2525
validates_presence_of :install_disk
26+
validate :validate_ephemeral_disk_identifier_format
2627

2728
after_create :set_configured, if: :already_configured
2829

@@ -97,7 +98,38 @@ def generate_config(output_type: server.talos_type)
9798
end
9899

99100
# NOTE: This also gives us consistent 2 space indentation
100-
talosconfig.to_yaml.delete_prefix("---\n")
101+
config = talosconfig.to_yaml
102+
103+
# Add a VolumeConfig document if the server has an ephemeral disk identifier
104+
if ephemeral_disk_identifier.present?
105+
# id_type will be "wwid" for regular disks or "uuid" for raid arrays
106+
id_type, id = ephemeral_disk_identifier.split(":", 2)
107+
108+
# https://www.talos.dev/v1.10/talos-guides/configuration/disk-management/#disk-selector
109+
disk_selector_cel =
110+
if id_type == "wwid"
111+
"disk.wwid == '#{id}'"
112+
elsif id_type == "uuid"
113+
# Talos takes the UUID hex and puts colons every 8 characters
114+
# 1a462672-bd83-888c-df8f-a57e6b38f998 -> 1a462672:bd83888c:df8fa57e:6b38f998
115+
uuid_talos_style = id.delete("-").chars.each_slice(8).map(&:join).join(":")
116+
"'dev/disk/by-id/md-uuid-#{uuid_talos_style}' in disk.symlinks"
117+
end
118+
119+
config += <<~YAML
120+
---
121+
apiVersion: v1alpha1
122+
kind: VolumeConfig
123+
name: EPHEMERAL
124+
provisioning:
125+
diskSelector:
126+
match: "#{disk_selector_cel}"
127+
minSize: 10GB
128+
grow: true
129+
YAML
130+
end
131+
132+
config
101133
end
102134

103135
private
@@ -144,6 +176,14 @@ def validate_private_ip_format
144176
end
145177
end
146178

179+
def validate_ephemeral_disk_identifier_format
180+
return if ephemeral_disk_identifier.blank?
181+
182+
unless ephemeral_disk_identifier.match?(/^(wwid|uuid):.+$/)
183+
errors.add(:ephemeral_disk_identifier, "must be in the format 'wwid:<wwid>' or 'uuid:<uuid>'")
184+
end
185+
end
186+
147187
def set_configured
148188
server.update!(
149189
last_configured_at: Time.now,

app/views/machine_configs/new.html.erb

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,51 @@
55

66
<%= f.text_field :hostname, required: true, placeholder: "worker-x", hint: "Will substitute ${hostname} in your config patches." %>
77
<%= f.text_field :private_ip, required: true, placeholder: "10.0.x.x", label: "Private IP", hint: "Will substitute ${private_ip} in your config patches." %>
8-
<%= f.text_field :install_disk, required: true, placeholder: "/dev/sda", hint: "Gets passed into the --install-disk argument for talosctl gen config." %>
8+
<% if f.object.server.lsblk.present? %>
9+
<%= f.text_field :install_disk, required: true, disabled: true, hint: "Gets passed into the --install-disk argument for talosctl gen config. Non modifiable: must use the same disk which was used for bootstrapping." %>
10+
<%
11+
# - RAID devices are listed as children of disks
12+
# - UUID of disks containing RAID devices are actually the UUID of the RAID device
13+
# Hence we reduce RAID disks to a single entry per RAID device, using their UUID as value.
14+
disks = f.object.server.lsblk.fetch("blockdevices").select { |disk| disk.fetch("type") == "disk" }
15+
raids, other = disks.partition { |disk| disk.fetch("children", []).any? { |child| child.fetch("type").start_with?("raid") } }
16+
raids = raids
17+
.group_by { |disk| disk.fetch("children").find { |child| child.fetch("type").start_with?("raid") }&.fetch("name") }
18+
.map do |raid_name, disks|
19+
raid = disks.first.fetch("children").find { |child| child.fetch("type").start_with?("raid") }
20+
type = raid.fetch("type").upcase
21+
label = "/dev/#{raid_name} (#{number_to_human_size(raid.fetch('size'))}) [#{type}]"
22+
value = disks.first.fetch("uuid")
23+
24+
[label, "uuid:#{value}"]
25+
end
26+
other = other
27+
.reject { |disk| disk.fetch("wwn") == f.object.server.bootstrap_disk_wwid }
28+
.map do |disk|
29+
partitions = disk.fetch("children", []).select { it.fetch("type").start_with?("part") }
30+
suffix = partitions.empty? ? "" : "[#{partitions.length} existing partition#{'s' if partitions.length != 1}]"
31+
label = "/dev/#{disk['name']} (#{number_to_human_size(disk['size'])}) #{suffix}"
32+
value = disk.fetch("wwn")
33+
34+
[label, "wwid:#{value}"]
35+
end
36+
ephemeral_disk_options = (raids + other).sort_by(&:first)
37+
%>
38+
<%=
39+
f.select(
40+
:ephemeral_disk_identifier,
41+
ephemeral_disk_options,
42+
{ include_blank: "Same as install disk" },
43+
required: false,
44+
label: "Disk for EPHEMERAL partition (optional)",
45+
hint: "If an ephemeral disk is selected, a <a href='https://www.talos.dev/v1.10/reference/configuration/block/volumeconfig/' class='text-blue-500 hover:underline' target='_blank'>VolumeConfig</a> will be added to the server's MachineConfig to use the disk for Talos Linux's EPHEMERAL partition. This is the disk mounted at /var which includes local persistent volumes, so the name ephemeral is misleading. RAID devices will be listed here but note that your Talos Factory Image must contain the <a href='https://github.com/siderolabs/extensions/tree/main/storage/mdadm' target='_blank' class='text-blue-500 hover:underline'>mdadm extension</a> for them to work.",
46+
)
47+
%>
48+
<% else %>
49+
<%= f.text_field :install_disk, required: true, placeholder: "/dev/nvme0n1", hint: "Gets passed into the --install-disk argument for talosctl gen config." %>
50+
<% end %>
951
<%= f.select :config_id, Config.pluck(:name, :id), { include_blank: true }, required: true %>
1052
<%= f.check_box :already_configured, class: "mr-1", hint: "Check to apply green status to the server immediately" %>
1153

1254
<%= f.submit "Configure" %>
13-
<% end %>
55+
<% end %>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddEphemeralDiskIdentifierToMachineConfigs < ActiveRecord::Migration[8.0]
2+
def change
3+
add_column :machine_configs, :ephemeral_disk_identifier, :string
4+
end
5+
end

db/schema.rb

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/fixtures/clusters.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
default:
22
name: default
3-
endpoint: http://kubernetes.example.com:6443
3+
endpoint: https://kubernetes.example.com:6443
44
secrets: |
55
cluster:
66
id: d4ztP13Cr27K_0eTNQmM0I1_k50EHxci7bT_KROwxrI=

spec/models/machine_config_spec.rb

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
11
RSpec.describe MachineConfig do
2+
it "validates format of ephemeral_disk_identifier" do
3+
machine_config = MachineConfig.new
4+
machine_config.validate
5+
expect(machine_config.errors[:ephemeral_disk_identifier]).to be_empty
6+
7+
machine_config.ephemeral_disk_identifier = "wwid:1234567890abcdef"
8+
machine_config.validate
9+
expect(machine_config.errors[:ephemeral_disk_identifier]).to be_empty
10+
11+
machine_config.ephemeral_disk_identifier = "uuid:12345678-1234-5678-1234-567812345678"
12+
machine_config.validate
13+
expect(machine_config.errors[:ephemeral_disk_identifier]).to be_empty
14+
15+
machine_config.ephemeral_disk_identifier = "invalid_format"
16+
machine_config.validate
17+
expect(machine_config.errors[:ephemeral_disk_identifier]).to include("must be in the format 'wwid:<wwid>' or 'uuid:<uuid>'")
18+
end
19+
220
describe "#generate_config" do
321
it "raises an error if hostname is blank" do
422
server = Server.new(name: "worker-1")
@@ -12,7 +30,7 @@
1230
expect { machine_config.generate_config }.to raise_error "can't generate config before assigning private_ip"
1331
end
1432

15-
it "generates a config" do
33+
it "generates a config including substitution variables: bootstrap_disk_wwid, hostname, public_ip, private_ip and vlan" do
1634
hetzner_vswitch = HetznerVswitch.new(name: "vswitch", vlan: 1337)
1735
cluster = Cluster.create!(
1836
name: "cluster",
@@ -69,6 +87,7 @@
6987
server:,
7088
)
7189
expect(machine_config.generate_config).to eq <<~YAML
90+
---
7291
version: v1alpha1
7392
debug: false
7493
persist: true
@@ -147,5 +166,59 @@
147166
service: {}
148167
YAML
149168
end
169+
170+
context "with an ephemeral_disk_identifier" do
171+
it "generates a config including a VolumeConfig for the ephemeral disk with different matchers for uuid and wwid" do
172+
server = servers(:cloud_botstrapped)
173+
174+
config = Config.new(
175+
name: "config",
176+
install_image: "ghcr.io/siderolabs/installer:v1.10.6",
177+
kubernetes_version: "1.33.3",
178+
kubespan: true,
179+
patch: "",
180+
)
181+
machine_config = MachineConfig.new(
182+
hostname: server.name,
183+
private_ip: "10.0.1.2",
184+
install_disk: "/dev/nvme0n1",
185+
ephemeral_disk_identifier: "wwid:eui.36344630528029720025384500000002",
186+
config:,
187+
server:,
188+
)
189+
190+
generated_config = machine_config.generate_config
191+
expect(YAML.load_stream(generated_config).length).to eq 2 # sanity check that YAML is valid
192+
193+
volume_config = generated_config.split("---\n").last # expect on raw String to ensure good formatting
194+
expect(volume_config).to eq <<~YAML
195+
apiVersion: v1alpha1
196+
kind: VolumeConfig
197+
name: EPHEMERAL
198+
provisioning:
199+
diskSelector:
200+
match: "disk.wwid == 'eui.36344630528029720025384500000002'"
201+
minSize: 10GB
202+
grow: true
203+
YAML
204+
205+
machine_config.ephemeral_disk_identifier = "uuid:1a462672-bd83-888c-df8f-a57e6b38f998"
206+
207+
generated_config = machine_config.generate_config
208+
expect(YAML.load_stream(generated_config).length).to eq 2 # sanity check that YAML is valid
209+
210+
volume_config = generated_config.split("---\n").last # expect on raw String to ensure good formatting
211+
expect(volume_config).to eq <<~YAML
212+
apiVersion: v1alpha1
213+
kind: VolumeConfig
214+
name: EPHEMERAL
215+
provisioning:
216+
diskSelector:
217+
match: "'dev/disk/by-id/md-uuid-1a462672:bd83888c:df8fa57e:6b38f998' in disk.symlinks"
218+
minSize: 10GB
219+
grow: true
220+
YAML
221+
end
222+
end
150223
end
151224
end

0 commit comments

Comments
 (0)