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..baa11328 100644 --- a/cmd/metal-api/internal/metal/machine.go +++ b/cmd/metal-api/internal/metal/machine.go @@ -1,8 +1,10 @@ package metal import ( + "errors" "fmt" "log/slog" + "net" "net/netip" "os" "path/filepath" @@ -10,6 +12,7 @@ import ( "strings" "time" + "github.com/asaskevich/govalidator" "github.com/dustin/go-humanize" mn "github.com/metal-stack/metal-lib/pkg/net" "github.com/samber/lo" @@ -153,6 +156,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 +180,18 @@ type IngressRule struct { Comment string `rethinkdb:"comment" json:"comment"` } +type DNSServers []DNSServer + +type DNSServer struct { + IP string `rethinkdb:"ip" json:"ip" description:"ip address of this dns server"` +} + +type NTPServers []NTPServer + +type NTPServer struct { + Address string `address:"address" json:"address" description:"ip address or dns hostname of this ntp server"` +} + type Protocol string const ( @@ -717,3 +734,45 @@ func (i *MachineIPMISuperUser) User() string { func DisabledIPMISuperUser() MachineIPMISuperUser { return MachineIPMISuperUser{} } + +func (d DNSServers) Validate() error { + if len(d) == 0 { + return nil + } + + if len(d) > 3 { + return errors.New("please specify a maximum of three dns servers") + } + + for _, dnsServer := range d { + _, err := netip.ParseAddr(dnsServer.IP) + if err != nil { + return fmt.Errorf("ip: %s for dns server not correct err: %w", dnsServer, err) + } + } + return nil +} + +func (n NTPServers) Validate() error { + if len(n) == 0 { + return nil + } + + if len(n) < 3 || len(n) > 5 { + return errors.New("please specify a minimum of 3 and a maximum of 5 ntp servers") + } + + for _, ntpserver := range n { + if net.ParseIP(ntpserver.Address) != nil { + _, err := netip.ParseAddr(ntpserver.Address) + if err != nil { + return fmt.Errorf("ip: %s for ntp server not correct err: %w", ntpserver, err) + } + } else { + if !govalidator.IsDNSName(ntpserver.Address) { + return fmt.Errorf("dns name: %s for ntp server not correct", ntpserver) + } + } + } + return nil +} 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..338b2a7c 100644 --- a/cmd/metal-api/internal/service/machine-service.go +++ b/cmd/metal-api/internal/service/machine-service.go @@ -78,6 +78,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 +1145,40 @@ 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 = partition.DNSServers + ntpServers = partition.NTPServers + ) + if len(machineRequest.DNSServers) != 0 { + dnsServers = metal.DNSServers{} + for _, s := range machineRequest.DNSServers { + dnsServers = append(dnsServers, metal.DNSServer{ + IP: s.IP, + }) + } + } + if len(machineRequest.NTPServers) != 0 { + ntpServers = []metal.NTPServer{} + for _, s := range machineRequest.NTPServers { + ntpServers = append(ntpServers, metal.NTPServer{ + Address: s.Address, + }) + } + } + + if err := dnsServers.Validate(); err != nil { + return nil, err + } + + if err := ntpServers.Validate(); err != nil { + return nil, err + } + return &machineAllocationSpec{ Creator: user.EMail, UUID: uuid, @@ -1164,6 +1200,8 @@ func createMachineAllocationSpec(ds *datastore.RethinkStore, machineRequest v1.M PlacementTags: machineRequest.PlacementTags, EgressRules: egress, IngressRules: ingress, + DNSServers: dnsServers, + NTPServers: ntpServers, }, nil } @@ -1247,6 +1285,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..81fae26f 100644 --- a/cmd/metal-api/internal/service/partition-service.go +++ b/cmd/metal-api/internal/service/partition-service.go @@ -205,6 +205,33 @@ func (r *partitionResource) createPartition(request *restful.Request, response * commandLine = *requestPayload.PartitionBootConfiguration.CommandLine } + var dnsServers metal.DNSServers + if len(requestPayload.DNSServers) != 0 { + for _, s := range requestPayload.DNSServers { + dnsServers = append(dnsServers, metal.DNSServer{ + IP: s.IP, + }) + } + } + var ntpServers metal.NTPServers + if len(requestPayload.NTPServers) != 0 { + for _, s := range requestPayload.NTPServers { + ntpServers = append(ntpServers, metal.NTPServer{ + Address: s.Address, + }) + } + } + + if err := dnsServers.Validate(); err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return + } + + if err := ntpServers.Validate(); err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return + } + p := &metal.Partition{ Base: metal.Base{ ID: requestPayload.ID, @@ -219,6 +246,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..ee137fd5 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 []DNSServer `json:"dns_servers,omitempty" description:"the dns servers used for the machine" optional:"true"` + NTPServers []NTPServer `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 []DNSServer `json:"dns_servers,omitempty" description:"the dns servers used for the machine" optional:"true"` + NTPServers []NTPServer `json:"ntp_servers,omitempty" description:"the ntp servers used for the machine" optional:"true"` } type MachineAllocationNetworks []MachineAllocationNetwork @@ -332,6 +336,14 @@ type MachineIssue struct { Details string `json:"details" description:"details of the issue"` } +type DNSServer struct { + IP string `json:"ip" description:"ip address of this dns server"` +} + +type NTPServer struct { + Address string `json:"address" description:"ip address or dns hostname of this ntp server"` +} + func NewMetalIPMI(r *MachineIPMI) metal.IPMI { var chassisPartNumber string if r.Fru.ChassisPartNumber != nil { @@ -580,6 +592,22 @@ func NewMachineResponse(m *metal.Machine, s *metal.Size, p *metal.Partition, i * } } + var ( + dnsServers []DNSServer + ntpServers []NTPServer + ) + + for _, s := range m.Allocation.DNSServers { + dnsServers = append(dnsServers, DNSServer{ + IP: s.IP, + }) + } + for _, s := range m.Allocation.NTPServers { + ntpServers = append(ntpServers, NTPServer{ + Address: s.Address, + }) + } + allocation = &MachineAllocation{ Creator: m.Allocation.Creator, Created: m.Allocation.Created, @@ -597,6 +625,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: dnsServers, + NTPServers: 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..0e5d3cec 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 []DNSServer `json:"dns_servers" description:"the dns servers for this partition" optional:"true"` + NTPServers []NTPServer `json:"ntp_servers" description:"the ntp servers for this partition" optional:"true"` } type PartitionBootConfiguration struct { @@ -104,6 +106,22 @@ func NewPartitionResponse(p *metal.Partition) *PartitionResponse { labels = p.Labels } + var ( + dnsServers []DNSServer + ntpServers []NTPServer + ) + + for _, s := range p.DNSServers { + dnsServers = append(dnsServers, DNSServer{ + IP: s.IP, + }) + } + for _, s := range p.NTPServers { + ntpServers = append(ntpServers, NTPServer{ + Address: s.Address, + }) + } + return &PartitionResponse{ Common: Common{ Identifiable: Identifiable{ @@ -117,6 +135,8 @@ func NewPartitionResponse(p *metal.Partition) *PartitionResponse { PartitionBase: PartitionBase{ MgmtServiceAddress: &p.MgmtServiceAddress, PrivateNetworkPrefixLength: &prefixLength, + DNSServers: dnsServers, + NTPServers: ntpServers, }, PartitionBootConfiguration: PartitionBootConfiguration{ ImageURL: &p.BootConfiguration.ImageURL, diff --git a/go.mod b/go.mod index b9fcb7a6..e0d3fcb8 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 3c42aae5..79df1274 100644 --- a/spec/metal-api.json +++ b/spec/metal-api.json @@ -618,6 +618,17 @@ "id" ] }, + "v1.DNSServer": { + "properties": { + "ip": { + "description": "ip address of this dns server", + "type": "string" + } + }, + "required": [ + "ip" + ] + }, "v1.Describable": { "properties": { "description": { @@ -1009,6 +1020,13 @@ "description": "a description for this entity", "type": "string" }, + "dns_servers": { + "description": "the dns servers used for the machine", + "items": { + "$ref": "#/definitions/v1.DNSServer" + }, + "type": "array" + }, "filesystemlayoutid": { "description": "the filesystemlayout id to assign to this machine", "type": "string" @@ -1043,6 +1061,13 @@ }, "type": "array" }, + "ntp_servers": { + "description": "the ntp servers used for the machine", + "items": { + "$ref": "#/definitions/v1.NTPServer" + }, + "type": "array" + }, "partitionid": { "description": "the partition id to assign this machine to", "type": "string" @@ -1984,6 +2009,13 @@ "description": "a description for this entity", "type": "string" }, + "dns_servers": { + "description": "the dns servers used for the machine", + "items": { + "$ref": "#/definitions/v1.DNSServer" + }, + "type": "array" + }, "filesystemlayoutid": { "description": "the filesystemlayout id to assign to this machine", "type": "string" @@ -2014,6 +2046,13 @@ }, "type": "array" }, + "ntp_servers": { + "description": "the ntp servers used for the machine", + "items": { + "$ref": "#/definitions/v1.NTPServer" + }, + "type": "array" + }, "partitionid": { "description": "the partition id to assign this machine to", "type": "string" @@ -2087,6 +2126,13 @@ "description": "a description for this machine", "type": "string" }, + "dns_servers": { + "description": "the dns servers used for the machine", + "items": { + "$ref": "#/definitions/v1.DNSServer" + }, + "type": "array" + }, "filesystemlayout": { "$ref": "#/definitions/v1.FilesystemLayoutResponse", "description": "filesystemlayout to create on this machine" @@ -2115,6 +2161,13 @@ }, "type": "array" }, + "ntp_servers": { + "description": "the ntp servers used for the machine", + "items": { + "$ref": "#/definitions/v1.NTPServer" + }, + "type": "array" + }, "project": { "description": "the project id that this machine is assigned to", "type": "string" @@ -3560,6 +3613,17 @@ "vendor" ] }, + "v1.NTPServer": { + "properties": { + "address": { + "description": "ip address or dns hostname of this ntp server", + "type": "string" + } + }, + "required": [ + "address" + ] + }, "v1.NetworkAllocateRequest": { "properties": { "description": { @@ -4023,6 +4087,13 @@ }, "v1.PartitionBase": { "properties": { + "dns_servers": { + "description": "the dns servers for this partition", + "items": { + "$ref": "#/definitions/v1.DNSServer" + }, + "type": "array" + }, "labels": { "additionalProperties": { "type": "string" @@ -4034,6 +4105,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/v1.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 +4187,13 @@ "description": "a description for this entity", "type": "string" }, + "dns_servers": { + "description": "the dns servers for this partition", + "items": { + "$ref": "#/definitions/v1.DNSServer" + }, + "type": "array" + }, "id": { "description": "the unique ID of this entity", "type": "string" @@ -4128,6 +4213,13 @@ "description": "a readable name for this entity", "type": "string" }, + "ntp_servers": { + "description": "the ntp servers for this partition", + "items": { + "$ref": "#/definitions/v1.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 +4255,13 @@ "description": "a description for this entity", "type": "string" }, + "dns_servers": { + "description": "the dns servers for this partition", + "items": { + "$ref": "#/definitions/v1.DNSServer" + }, + "type": "array" + }, "id": { "description": "the unique ID of this entity", "type": "string" @@ -4182,6 +4281,13 @@ "description": "a readable name for this entity", "type": "string" }, + "ntp_servers": { + "description": "the ntp servers for this partition", + "items": { + "$ref": "#/definitions/v1.NTPServer" + }, + "type": "array" + }, "privatenetworkprefixlength": { "description": "the length of private networks for the machine's child networks in this partition, default 22", "format": "int32",