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
1 change: 1 addition & 0 deletions app/controllers/machine_configs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def create
:private_ip,
:already_configured,
:install_disk,
:ephemeral_disk_identifier,
)
@machine_config = MachineConfig.new(machine_config_params)
@server = @machine_config.server
Expand Down
42 changes: 41 additions & 1 deletion app/models/machine_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def initialize(message, output)
validates_presence_of :private_ip
validate :validate_private_ip_format
validates_presence_of :install_disk
validate :validate_ephemeral_disk_identifier_format

after_create :set_configured, if: :already_configured

Expand Down Expand Up @@ -97,7 +98,38 @@ def generate_config(output_type: server.talos_type)
end

# NOTE: This also gives us consistent 2 space indentation
talosconfig.to_yaml.delete_prefix("---\n")
config = talosconfig.to_yaml

# Add a VolumeConfig document if the server has an ephemeral disk identifier
if ephemeral_disk_identifier.present?
# id_type will be "wwid" for regular disks or "uuid" for raid arrays
id_type, id = ephemeral_disk_identifier.split(":", 2)

# https://www.talos.dev/v1.10/talos-guides/configuration/disk-management/#disk-selector
disk_selector_cel =
if id_type == "wwid"
"disk.wwid == '#{id}'"
elsif id_type == "uuid"
# Talos takes the UUID hex and puts colons every 8 characters
# 1a462672-bd83-888c-df8f-a57e6b38f998 -> 1a462672:bd83888c:df8fa57e:6b38f998
uuid_talos_style = id.delete("-").chars.each_slice(8).map(&:join).join(":")
"'dev/disk/by-id/md-uuid-#{uuid_talos_style}' in disk.symlinks"
end

config += <<~YAML
---
apiVersion: v1alpha1
kind: VolumeConfig
name: EPHEMERAL
provisioning:
diskSelector:
match: "#{disk_selector_cel}"
minSize: 10GB
grow: true
YAML
end

config
end

private
Expand Down Expand Up @@ -144,6 +176,14 @@ def validate_private_ip_format
end
end

def validate_ephemeral_disk_identifier_format
return if ephemeral_disk_identifier.blank?

unless ephemeral_disk_identifier.match?(/^(wwid|uuid):.+$/)
errors.add(:ephemeral_disk_identifier, "must be in the format 'wwid:<wwid>' or 'uuid:<uuid>'")
end
end

def set_configured
server.update!(
last_configured_at: Time.now,
Expand Down
46 changes: 44 additions & 2 deletions app/views/machine_configs/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,51 @@

<%= f.text_field :hostname, required: true, placeholder: "worker-x", hint: "Will substitute ${hostname} in your config patches." %>
<%= f.text_field :private_ip, required: true, placeholder: "10.0.x.x", label: "Private IP", hint: "Will substitute ${private_ip} in your config patches." %>
<%= f.text_field :install_disk, required: true, placeholder: "/dev/sda", hint: "Gets passed into the --install-disk argument for talosctl gen config." %>
<% if f.object.server.lsblk.present? %>
<%= 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." %>
<%
# - RAID devices are listed as children of disks
# - UUID of disks containing RAID devices are actually the UUID of the RAID device
# Hence we reduce RAID disks to a single entry per RAID device, using their UUID as value.
disks = f.object.server.lsblk.fetch("blockdevices").select { |disk| disk.fetch("type") == "disk" }
raids, other = disks.partition { |disk| disk.fetch("children", []).any? { |child| child.fetch("type").start_with?("raid") } }
raids = raids
.group_by { |disk| disk.fetch("children").find { |child| child.fetch("type").start_with?("raid") }&.fetch("name") }
.map do |raid_name, disks|
raid = disks.first.fetch("children").find { |child| child.fetch("type").start_with?("raid") }
type = raid.fetch("type").upcase
label = "/dev/#{raid_name} (#{number_to_human_size(raid.fetch('size'))}) [#{type}]"
value = disks.first.fetch("uuid")

[label, "uuid:#{value}"]
end
other = other
.reject { |disk| disk.fetch("wwn") == f.object.server.bootstrap_disk_wwid }
.map do |disk|
partitions = disk.fetch("children", []).select { it.fetch("type").start_with?("part") }
suffix = partitions.empty? ? "" : "[#{partitions.length} existing partition#{'s' if partitions.length != 1}]"
label = "/dev/#{disk['name']} (#{number_to_human_size(disk['size'])}) #{suffix}"
value = disk.fetch("wwn")

[label, "wwid:#{value}"]
end
ephemeral_disk_options = (raids + other).sort_by(&:first)
%>
<%=
f.select(
:ephemeral_disk_identifier,
ephemeral_disk_options,
{ include_blank: "Same as install disk" },
required: false,
label: "Disk for EPHEMERAL partition (optional)",
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.",
)
%>
<% else %>
<%= f.text_field :install_disk, required: true, placeholder: "/dev/nvme0n1", hint: "Gets passed into the --install-disk argument for talosctl gen config." %>
<% end %>
<%= f.select :config_id, Config.pluck(:name, :id), { include_blank: true }, required: true %>
<%= f.check_box :already_configured, class: "mr-1", hint: "Check to apply green status to the server immediately" %>

<%= f.submit "Configure" %>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddEphemeralDiskIdentifierToMachineConfigs < ActiveRecord::Migration[8.0]
def change
add_column :machine_configs, :ephemeral_disk_identifier, :string
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion spec/fixtures/clusters.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
default:
name: default
endpoint: http://kubernetes.example.com:6443
endpoint: https://kubernetes.example.com:6443
secrets: |
cluster:
id: d4ztP13Cr27K_0eTNQmM0I1_k50EHxci7bT_KROwxrI=
Expand Down
75 changes: 74 additions & 1 deletion spec/models/machine_config_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
RSpec.describe MachineConfig do
it "validates format of ephemeral_disk_identifier" do
machine_config = MachineConfig.new
machine_config.validate
expect(machine_config.errors[:ephemeral_disk_identifier]).to be_empty

machine_config.ephemeral_disk_identifier = "wwid:1234567890abcdef"
machine_config.validate
expect(machine_config.errors[:ephemeral_disk_identifier]).to be_empty

machine_config.ephemeral_disk_identifier = "uuid:12345678-1234-5678-1234-567812345678"
machine_config.validate
expect(machine_config.errors[:ephemeral_disk_identifier]).to be_empty

machine_config.ephemeral_disk_identifier = "invalid_format"
machine_config.validate
expect(machine_config.errors[:ephemeral_disk_identifier]).to include("must be in the format 'wwid:<wwid>' or 'uuid:<uuid>'")
end

describe "#generate_config" do
it "raises an error if hostname is blank" do
server = Server.new(name: "worker-1")
Expand All @@ -12,7 +30,7 @@
expect { machine_config.generate_config }.to raise_error "can't generate config before assigning private_ip"
end

it "generates a config" do
it "generates a config including substitution variables: bootstrap_disk_wwid, hostname, public_ip, private_ip and vlan" do
hetzner_vswitch = HetznerVswitch.new(name: "vswitch", vlan: 1337)
cluster = Cluster.create!(
name: "cluster",
Expand Down Expand Up @@ -69,6 +87,7 @@
server:,
)
expect(machine_config.generate_config).to eq <<~YAML
---
version: v1alpha1
debug: false
persist: true
Expand Down Expand Up @@ -147,5 +166,59 @@
service: {}
YAML
end

context "with an ephemeral_disk_identifier" do
it "generates a config including a VolumeConfig for the ephemeral disk with different matchers for uuid and wwid" do
server = servers(:cloud_botstrapped)

config = Config.new(
name: "config",
install_image: "ghcr.io/siderolabs/installer:v1.10.6",
kubernetes_version: "1.33.3",
kubespan: true,
patch: "",
)
machine_config = MachineConfig.new(
hostname: server.name,
private_ip: "10.0.1.2",
install_disk: "/dev/nvme0n1",
ephemeral_disk_identifier: "wwid:eui.36344630528029720025384500000002",
config:,
server:,
)

generated_config = machine_config.generate_config
expect(YAML.load_stream(generated_config).length).to eq 2 # sanity check that YAML is valid

volume_config = generated_config.split("---\n").last # expect on raw String to ensure good formatting
expect(volume_config).to eq <<~YAML
apiVersion: v1alpha1
kind: VolumeConfig
name: EPHEMERAL
provisioning:
diskSelector:
match: "disk.wwid == 'eui.36344630528029720025384500000002'"
minSize: 10GB
grow: true
YAML

machine_config.ephemeral_disk_identifier = "uuid:1a462672-bd83-888c-df8f-a57e6b38f998"

generated_config = machine_config.generate_config
expect(YAML.load_stream(generated_config).length).to eq 2 # sanity check that YAML is valid

volume_config = generated_config.split("---\n").last # expect on raw String to ensure good formatting
expect(volume_config).to eq <<~YAML
apiVersion: v1alpha1
kind: VolumeConfig
name: EPHEMERAL
provisioning:
diskSelector:
match: "'dev/disk/by-id/md-uuid-1a462672:bd83888c:df8fa57e:6b38f998' in disk.symlinks"
minSize: 10GB
grow: true
YAML
end
end
end
end