Skip to content

Commit 4f8314f

Browse files
committed
Support VolumeConfig for EPHEMERAL partition
1 parent 32cb400 commit 4f8314f

File tree

7 files changed

+165
-6
lines changed

7 files changed

+165
-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: 38 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,35 @@ 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 can be "wwid" or "uuid"
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+
"'dev/disk/by-uuid/#{id}' in disk.symlinks"
114+
end
115+
116+
config += <<~YAML
117+
---
118+
apiVersion: v1alpha1
119+
kind: VolumeConfig
120+
name: EPHEMERAL
121+
provisioning:
122+
diskSelector:
123+
match: "#{disk_selector_cel}"
124+
minSize: 10GB
125+
grow: true
126+
YAML
127+
end
128+
129+
config
101130
end
102131

103132
private
@@ -144,6 +173,14 @@ def validate_private_ip_format
144173
end
145174
end
146175

176+
def validate_ephemeral_disk_identifier_format
177+
return if ephemeral_disk_identifier.blank?
178+
179+
unless ephemeral_disk_identifier.match?(/^(wwid|uuid):.+$/)
180+
errors.add(:ephemeral_disk_identifier, "must be in the format 'wwid:<wwid>' or 'uuid:<uuid>'")
181+
end
182+
end
183+
147184
def set_configured
148185
server.update!(
149186
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: "Ephemeral Disk (optional)",
45+
hint: "If an ephemeral disk is selected, a VolumeConfig will be added to the server's MachineConfig to use the disk for Talos Linux's EPHEMERAL partition. 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:12345678-1234-5678-1234-567812345678"
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-uuid/12345678-1234-5678-1234-567812345678' in disk.symlinks"
218+
minSize: 10GB
219+
grow: true
220+
YAML
221+
end
222+
end
150223
end
151224
end

0 commit comments

Comments
 (0)