diff --git a/cmd/metal-api/internal/datastore/machine_integration_test.go b/cmd/metal-api/internal/datastore/machine_integration_test.go index b95dc5ff..e45a7681 100644 --- a/cmd/metal-api/internal/datastore/machine_integration_test.go +++ b/cmd/metal-api/internal/datastore/machine_integration_test.go @@ -107,6 +107,12 @@ func (_ *machineTestable) defaultBody(m *metal.Machine) *metal.Machine { if m.Allocation.SSHPubKeys == nil { m.Allocation.SSHPubKeys = []string{} } + if m.Allocation.DNSServers == nil { + m.Allocation.DNSServers = metal.DNSServers{} + } + if m.Allocation.NTPServers == nil { + m.Allocation.NTPServers = metal.NTPServers{} + } for i := range m.Allocation.MachineNetworks { n := m.Allocation.MachineNetworks[i] if n.Prefixes == nil { diff --git a/cmd/metal-api/internal/metal/machine.go b/cmd/metal-api/internal/metal/machine.go index 3226d64b..083d3cf1 100644 --- a/cmd/metal-api/internal/metal/machine.go +++ b/cmd/metal-api/internal/metal/machine.go @@ -153,6 +153,8 @@ type MachineAllocation struct { VPN *MachineVPN `rethinkdb:"vpn" json:"vpn"` UUID string `rethinkdb:"uuid" json:"uuid"` FirewallRules *FirewallRules `rethinkdb:"firewall_rules" json:"firewall_rules"` + DNSServers DNSServers `rethinkdb:"dns_servers" json:"dns_servers"` + NTPServers NTPServers `rethinkdb:"ntp_servers" json:"ntp_servers"` } type FirewallRules struct { @@ -175,6 +177,18 @@ type IngressRule struct { Comment string `rethinkdb:"comment" json:"comment"` } +type DNSServers []DNSServer + +type DNSServer struct { + IP string `rethinkdb:"ip" json:"ip"` +} + +type NTPServers []NTPServer + +type NTPServer struct { + Address string `address:"address" json:"address"` +} + type Protocol string const ( diff --git a/cmd/metal-api/internal/metal/partition.go b/cmd/metal-api/internal/metal/partition.go index 2db7c2f8..de732c04 100644 --- a/cmd/metal-api/internal/metal/partition.go +++ b/cmd/metal-api/internal/metal/partition.go @@ -7,6 +7,8 @@ type Partition struct { MgmtServiceAddress string `rethinkdb:"mgmtserviceaddr" json:"mgmtserviceaddr"` PrivateNetworkPrefixLength uint8 `rethinkdb:"privatenetworkprefixlength" json:"privatenetworkprefixlength"` Labels map[string]string `rethinkdb:"labels" json:"labels"` + DNSServers DNSServers `rethinkdb:"dns_servers" json:"dns_servers"` + NTPServers NTPServers `rethinkdb:"ntp_servers" json:"ntp_servers"` } // BootConfiguration defines the metal-hammer initrd, kernel and commandline diff --git a/cmd/metal-api/internal/service/machine-service.go b/cmd/metal-api/internal/service/machine-service.go index 24570e34..af8ee715 100644 --- a/cmd/metal-api/internal/service/machine-service.go +++ b/cmd/metal-api/internal/service/machine-service.go @@ -7,10 +7,12 @@ import ( "log/slog" "net" "net/http" + "net/netip" "strconv" "strings" "time" + "github.com/asaskevich/govalidator" "github.com/google/uuid" "github.com/metal-stack/metal-api/cmd/metal-api/internal/headscale" "github.com/metal-stack/metal-api/cmd/metal-api/internal/issues" @@ -78,6 +80,8 @@ type machineAllocationSpec struct { PlacementTags []string EgressRules []metal.EgressRule IngressRules []metal.IngressRule + DNSServers metal.DNSServers + NTPServers metal.NTPServers } // allocationNetwork is intermediate struct to create machine networks from regular networks during machine allocation @@ -1143,6 +1147,51 @@ func createMachineAllocationSpec(ds *datastore.RethinkStore, machineRequest v1.M return nil, fmt.Errorf("size:%s not found err:%w", sizeID, err) } + partition, err := ds.FindPartition(partitionID) + if err != nil { + return nil, fmt.Errorf("partition:%s not found err:%w", partitionID, err) + } + var ( + dnsServers metal.DNSServers + ntpServers metal.NTPServers + ) + if len(machineRequest.DNSServers) != 0 { + if len(machineRequest.DNSServers) > 3 { + return nil, errors.New("please specify a maximum of three dns servers") + } + dnsServers = machineRequest.DNSServers + } else { + dnsServers = partition.DNSServers + } + for _, dnsip := range dnsServers { + _, err := netip.ParseAddr(dnsip.IP) + if err != nil { + return nil, fmt.Errorf("IP: %s for DNS server not correct err: %w", dnsip, err) + } + } + + if len(machineRequest.NTPServers) != 0 { + if len(machineRequest.NTPServers) <= 3 || len(machineRequest.NTPServers) > 5 { + return nil, errors.New("please specify a minimum of 3 and a maximum of 5 ntp servers") + } + ntpServers = machineRequest.NTPServers + } else { + ntpServers = partition.NTPServers + } + + for _, ntpserver := range ntpServers { + if net.ParseIP(ntpserver.Address) != nil { + _, err := netip.ParseAddr(ntpserver.Address) + if err != nil { + return nil, fmt.Errorf("IP: %s for NTP server not correct err: %w", ntpserver, err) + } + } else { + if !govalidator.IsDNSName(ntpserver.Address) { + return nil, fmt.Errorf("DNS name: %s for NTP server not correct err: %w", ntpserver, err) + } + } + } + return &machineAllocationSpec{ Creator: user.EMail, UUID: uuid, @@ -1164,6 +1213,8 @@ func createMachineAllocationSpec(ds *datastore.RethinkStore, machineRequest v1.M PlacementTags: machineRequest.PlacementTags, EgressRules: egress, IngressRules: ingress, + DNSServers: dnsServers, + NTPServers: ntpServers, }, nil } @@ -1247,6 +1298,8 @@ func allocateMachine(ctx context.Context, logger *slog.Logger, ds *datastore.Ret VPN: allocationSpec.VPN, FirewallRules: firewallRules, UUID: uuid.New().String(), + DNSServers: allocationSpec.DNSServers, + NTPServers: allocationSpec.NTPServers, } rollbackOnError := func(err error) error { if err != nil { diff --git a/cmd/metal-api/internal/service/partition-service.go b/cmd/metal-api/internal/service/partition-service.go index 5f5dfd7c..5172490e 100644 --- a/cmd/metal-api/internal/service/partition-service.go +++ b/cmd/metal-api/internal/service/partition-service.go @@ -205,6 +205,16 @@ func (r *partitionResource) createPartition(request *restful.Request, response * commandLine = *requestPayload.PartitionBootConfiguration.CommandLine } + var dnsServers metal.DNSServers + if len(requestPayload.DNSServers) > 0 { + dnsServers = requestPayload.DNSServers + } + + var ntpServers metal.NTPServers + if len(requestPayload.NTPServers) > 0 { + ntpServers = requestPayload.NTPServers + } + p := &metal.Partition{ Base: metal.Base{ ID: requestPayload.ID, @@ -219,6 +229,8 @@ func (r *partitionResource) createPartition(request *restful.Request, response * KernelURL: kernelURL, CommandLine: commandLine, }, + DNSServers: dnsServers, + NTPServers: ntpServers, } fqn := metal.TopicMachine.GetFQN(p.GetID()) diff --git a/cmd/metal-api/internal/service/v1/machine.go b/cmd/metal-api/internal/service/v1/machine.go index 5ab28851..4d83a375 100644 --- a/cmd/metal-api/internal/service/v1/machine.go +++ b/cmd/metal-api/internal/service/v1/machine.go @@ -45,6 +45,8 @@ type MachineAllocation struct { VPN *MachineVPN `json:"vpn" description:"vpn connection info for machine" optional:"true"` AllocationUUID string `json:"allocationuuid" description:"a unique identifier for this machine allocation, can be used to distinguish between machine allocations over time."` FirewallRules *FirewallRules `json:"firewall_rules,omitempty" description:"a set of firewall rules to apply" optional:"true"` + DNSServers metal.DNSServers `json:"dns_servers,omitempty" description:"the dns servers used for the machine" optional:"true"` + NTPServers metal.NTPServers `json:"ntp_servers,omitempty" description:"the ntp servers used for the machine" optional:"true"` } type FirewallRules struct { @@ -229,6 +231,8 @@ type MachineAllocateRequest struct { Networks MachineAllocationNetworks `json:"networks" description:"the networks that this machine will be placed in." optional:"true"` IPs []string `json:"ips" description:"the ips to attach to this machine additionally" optional:"true"` PlacementTags []string `json:"placement_tags,omitempty" description:"by default machines are spread across the racks inside a partition for every project. if placement tags are provided, the machine candidate has an additional anti-affinity to other machines having the same tags"` + DNSServers metal.DNSServers `json:"dns_servers,omitempty" description:"the dns servers used for the machine" optional:"true"` + NTPServers metal.NTPServers `json:"ntp_servers,omitempty" description:"the ntp servers used for the machine" optional:"true"` } type MachineAllocationNetworks []MachineAllocationNetwork @@ -597,6 +601,8 @@ func NewMachineResponse(m *metal.Machine, s *metal.Size, p *metal.Partition, i * VPN: NewMachineVPN(m.Allocation.VPN), AllocationUUID: m.Allocation.UUID, FirewallRules: firewallRules, + DNSServers: m.Allocation.DNSServers, + NTPServers: m.Allocation.NTPServers, } allocation.Reinstall = m.Allocation.Reinstall diff --git a/cmd/metal-api/internal/service/v1/partition.go b/cmd/metal-api/internal/service/v1/partition.go index f3304241..531d8fbb 100644 --- a/cmd/metal-api/internal/service/v1/partition.go +++ b/cmd/metal-api/internal/service/v1/partition.go @@ -8,6 +8,8 @@ type PartitionBase struct { MgmtServiceAddress *string `json:"mgmtserviceaddress" description:"the address to the management service of this partition" optional:"true"` PrivateNetworkPrefixLength *int `json:"privatenetworkprefixlength" description:"the length of private networks for the machine's child networks in this partition, default 22" optional:"true" minimum:"16" maximum:"30"` Labels map[string]string `json:"labels" description:"free labels that you associate with this partition" optional:"true"` + DNSServers metal.DNSServers `json:"dns_servers" description:"the dns servers for this partition" optional:"true"` + NTPServers metal.NTPServers `json:"ntp_servers" description:"the ntp servers for this partition" optional:"true"` } type PartitionBootConfiguration struct { @@ -117,6 +119,8 @@ func NewPartitionResponse(p *metal.Partition) *PartitionResponse { PartitionBase: PartitionBase{ MgmtServiceAddress: &p.MgmtServiceAddress, PrivateNetworkPrefixLength: &prefixLength, + DNSServers: p.DNSServers, + NTPServers: p.NTPServers, }, PartitionBootConfiguration: PartitionBootConfiguration{ ImageURL: &p.BootConfiguration.ImageURL, diff --git a/go.mod b/go.mod index a889c763..a32f1a37 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/andybalholm/brotli v1.1.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/spec/metal-api.json b/spec/metal-api.json index d73f9e05..24cb3fa4 100644 --- a/spec/metal-api.json +++ b/spec/metal-api.json @@ -348,6 +348,26 @@ "type": "HTTPErrorResponse" } }, + "metal.DNSServer": { + "properties": { + "ip": { + "type": "string" + } + }, + "required": [ + "ip" + ] + }, + "metal.NTPServer": { + "properties": { + "address": { + "type": "string" + } + }, + "required": [ + "address" + ] + }, "rest.HealthResponse": { "properties": { "message": { @@ -1009,6 +1029,13 @@ "description": "a description for this entity", "type": "string" }, + "dns_servers": { + "description": "the dns servers used for the machine", + "items": { + "$ref": "#/definitions/metal.DNSServer" + }, + "type": "array" + }, "filesystemlayoutid": { "description": "the filesystemlayout id to assign to this machine", "type": "string" @@ -1043,6 +1070,13 @@ }, "type": "array" }, + "ntp_servers": { + "description": "the ntp servers used for the machine", + "items": { + "$ref": "#/definitions/metal.NTPServer" + }, + "type": "array" + }, "partitionid": { "description": "the partition id to assign this machine to", "type": "string" @@ -1984,6 +2018,13 @@ "description": "a description for this entity", "type": "string" }, + "dns_servers": { + "description": "the dns servers used for the machine", + "items": { + "$ref": "#/definitions/metal.DNSServer" + }, + "type": "array" + }, "filesystemlayoutid": { "description": "the filesystemlayout id to assign to this machine", "type": "string" @@ -2014,6 +2055,13 @@ }, "type": "array" }, + "ntp_servers": { + "description": "the ntp servers used for the machine", + "items": { + "$ref": "#/definitions/metal.NTPServer" + }, + "type": "array" + }, "partitionid": { "description": "the partition id to assign this machine to", "type": "string" @@ -2087,6 +2135,13 @@ "description": "a description for this machine", "type": "string" }, + "dns_servers": { + "description": "the dns servers used for the machine", + "items": { + "$ref": "#/definitions/metal.DNSServer" + }, + "type": "array" + }, "filesystemlayout": { "$ref": "#/definitions/v1.FilesystemLayoutResponse", "description": "filesystemlayout to create on this machine" @@ -2115,6 +2170,13 @@ }, "type": "array" }, + "ntp_servers": { + "description": "the ntp servers used for the machine", + "items": { + "$ref": "#/definitions/metal.NTPServer" + }, + "type": "array" + }, "project": { "description": "the project id that this machine is assigned to", "type": "string" @@ -4023,6 +4085,13 @@ }, "v1.PartitionBase": { "properties": { + "dns_servers": { + "description": "the dns servers for this partition", + "items": { + "$ref": "#/definitions/metal.DNSServer" + }, + "type": "array" + }, "labels": { "additionalProperties": { "type": "string" @@ -4034,6 +4103,13 @@ "description": "the address to the management service of this partition", "type": "string" }, + "ntp_servers": { + "description": "the ntp servers for this partition", + "items": { + "$ref": "#/definitions/metal.NTPServer" + }, + "type": "array" + }, "privatenetworkprefixlength": { "description": "the length of private networks for the machine's child networks in this partition, default 22", "format": "int32", @@ -4109,6 +4185,13 @@ "description": "a description for this entity", "type": "string" }, + "dns_servers": { + "description": "the dns servers for this partition", + "items": { + "$ref": "#/definitions/metal.DNSServer" + }, + "type": "array" + }, "id": { "description": "the unique ID of this entity", "type": "string" @@ -4128,6 +4211,13 @@ "description": "a readable name for this entity", "type": "string" }, + "ntp_servers": { + "description": "the ntp servers for this partition", + "items": { + "$ref": "#/definitions/metal.NTPServer" + }, + "type": "array" + }, "privatenetworkprefixlength": { "description": "the length of private networks for the machine's child networks in this partition, default 22", "format": "int32", @@ -4163,6 +4253,13 @@ "description": "a description for this entity", "type": "string" }, + "dns_servers": { + "description": "the dns servers for this partition", + "items": { + "$ref": "#/definitions/metal.DNSServer" + }, + "type": "array" + }, "id": { "description": "the unique ID of this entity", "type": "string" @@ -4182,6 +4279,13 @@ "description": "a readable name for this entity", "type": "string" }, + "ntp_servers": { + "description": "the ntp servers for this partition", + "items": { + "$ref": "#/definitions/metal.NTPServer" + }, + "type": "array" + }, "privatenetworkprefixlength": { "description": "the length of private networks for the machine's child networks in this partition, default 22", "format": "int32",