diff --git a/examples/tectonic.libvirt.yaml b/examples/tectonic.libvirt.yaml index 4dad4dcbdf..099da1c1f7 100644 --- a/examples/tectonic.libvirt.yaml +++ b/examples/tectonic.libvirt.yaml @@ -1,6 +1,6 @@ admin: - email: "a@b.c" - password: "verysecure" + email: a@b.c + password: verysecure # The base DNS domain of the cluster. It must NOT contain a trailing period. Some # DNS providers will automatically add this if necessary. # @@ -15,12 +15,12 @@ admin: baseDomain: libvirt: - uri: "qemu:///system" + uri: qemu:///system network: name: tectonic ifName: tt0 - dnsServer: "8.8.8.8" - ipRange: "192.168.124.0/24" + dnsServer: 8.8.8.8 + ipRange: 192.168.124.0/24 sshKey: "ssh-rsa ..." imagePath: /path/to/image diff --git a/installer/pkg/config-generator/ignition.go b/installer/pkg/config-generator/ignition.go index 188334f9e0..4da0ba4194 100644 --- a/installer/pkg/config-generator/ignition.go +++ b/installer/pkg/config-generator/ignition.go @@ -96,7 +96,7 @@ func (c *ConfigGenerator) embedUserBlock(ignCfg *ignconfigtypes.Config) { userBlock := ignconfigtypes.User{ Name: "core", SSHAuthorizedKeys: []string{ - c.Libvirt.SshKey, + c.Libvirt.SSHKey, }, } diff --git a/installer/pkg/config/cluster.go b/installer/pkg/config/cluster.go index 2b1d2453b4..d0529e055e 100644 --- a/installer/pkg/config/cluster.go +++ b/installer/pkg/config/cluster.go @@ -35,6 +35,12 @@ var defaultCluster = Cluster{ Channel: ContainerLinuxChannelStable, Version: ContainerLinuxVersionLatest, }, + Libvirt: libvirt.Libvirt{ + Network: libvirt.Network{ + DNSServer: libvirt.DefaultDNSServer, + IfName: libvirt.DefaultIfName, + }, + }, Networking: Networking{ MTU: "1480", PodCIDR: "10.2.0.0/16", diff --git a/installer/pkg/config/libvirt/libvirt.go b/installer/pkg/config/libvirt/libvirt.go index eafa90b294..95f0de4594 100644 --- a/installer/pkg/config/libvirt/libvirt.go +++ b/installer/pkg/config/libvirt/libvirt.go @@ -7,32 +7,39 @@ import ( "github.com/apparentlymart/go-cidr/cidr" ) +const ( + // DefaultDNSServer is the default DNS server for libvirt. + DefaultDNSServer = "8.8.8.8" + // DefaultIfName is the default interface name for libvirt. + DefaultIfName = "osbr0" +) + // Libvirt-specific configuration type Libvirt struct { - URI string `json:"tectonic_libvirt_uri,omitempty" yaml:"uri"` - SshKey string `json:"tectonic_libvirt_ssh_key,omitempty" yaml:"sshKey"` - QowImagePath string `json:"tectonic_coreos_qow_path,omitempty" yaml:"imagePath"` - Network `json:",inline" yaml:"network"` - MasterIPs []string `json:"tectonic_libvirt_master_ips,omitempty" yaml:"masterIps"` + URI string `json:"tectonic_libvirt_uri,omitempty" yaml:"uri"` + SSHKey string `json:"tectonic_libvirt_ssh_key,omitempty" yaml:"sshKey"` + QCOWImagePath string `json:"tectonic_coreos_qcow_path,omitempty" yaml:"imagePath"` + Network `json:",inline" yaml:"network"` + MasterIPs []string `json:"tectonic_libvirt_master_ips,omitempty" yaml:"masterIPs"` } type Network struct { Name string `json:"tectonic_libvirt_network_name,omitempty" yaml:"name"` IfName string `json:"tectonic_libvirt_network_if,omitempty" yaml"ifName"` - DnsServer string `json:"tectonic_libvirt_resolver,omitempty" yaml:"dnsServer"` - IpRange string `json:"tectonic_libvirt_ip_range,omitempty" yaml:"ipRange"` + DNSServer string `json:"tectonic_libvirt_resolver,omitempty" yaml:"dnsServer"` + IPRange string `json:"tectonic_libvirt_ip_range,omitempty" yaml:"ipRange"` } // Fill in any variables for terraform func (l *Libvirt) TFVars(masterCount int) error { - _, network, err := net.ParseCIDR(l.Network.IpRange) + _, network, err := net.ParseCIDR(l.Network.IPRange) if err != nil { - return fmt.Errorf("failed to parse libvirt.network.iprange: %v", err) + return fmt.Errorf("failed to parse libvirt network ipRange: %v", err) } if len(l.MasterIPs) > 0 { if len(l.MasterIPs) != masterCount { - return fmt.Errorf("length of MasterIPs does't match master count") + return fmt.Errorf("length of MasterIPs doesn't match master count") } else { return nil } @@ -41,7 +48,7 @@ func (l *Libvirt) TFVars(masterCount int) error { for i := 0; i < masterCount; i++ { ip, err := cidr.Host(network, i+10) if err != nil { - return fmt.Errorf("failed to generate masterips: %v", err) + return fmt.Errorf("failed to generate master IPs: %v", err) } l.MasterIPs = append(l.MasterIPs, ip.String()) } diff --git a/installer/pkg/config/validate.go b/installer/pkg/config/validate.go index a63bfe035f..22006729d6 100644 --- a/installer/pkg/config/validate.go +++ b/installer/pkg/config/validate.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "io/ioutil" - "net" "regexp" "strings" @@ -20,6 +19,10 @@ const ( maxS3BucketNameLength = 63 ) +var ( + qcowMagic = []byte{'Q', 'F', 'I', 0xfb} +) + // ErrUnmatchedNodePool is returned when a nodePool was specified but not found in the nodePools list. type ErrUnmatchedNodePool struct { name string @@ -81,6 +84,7 @@ func (c *Cluster) Validate() []error { errs = append(errs, c.validateAWS()...) errs = append(errs, c.validateCL()...) errs = append(errs, c.validateTectonicFiles()...) + errs = append(errs, c.validateLibvirt()...) if err := validate.PrefixError("cluster name", validate.ClusterName(c.Name)); err != nil { errs = append(errs, err) } @@ -130,6 +134,52 @@ func (c *Cluster) validateCL() []error { return errs } +// validateLibvirt validates all fields specific to libvirt. +func (c *Cluster) validateLibvirt() []error { + var errs []error + if c.Platform != PlatformLibvirt { + return errs + } + if err := validate.PrefixError("libvirt network ipRange", validate.SubnetCIDR(c.Libvirt.Network.IPRange)); err != nil { + errs = append(errs, err) + } + if len(c.Libvirt.MasterIPs) > 0 { + if len(c.Libvirt.MasterIPs) != c.NodeCount(c.Master.NodePools) { + errs = append(errs, fmt.Errorf("length of masterIPs does't match master count")) + } + for i, ip := range c.Libvirt.MasterIPs { + if err := validate.PrefixError(fmt.Sprintf("libvirt masterIPs[%d] %q", i, ip), validate.IPv4(ip)); err != nil { + errs = append(errs, err) + } + } + } + if err := validate.PrefixError("libvirt uri", validate.NonEmpty(c.Libvirt.URI)); err != nil { + errs = append(errs, err) + } + if err := validate.PrefixError("libvirt imagePath is not a valid QCOW image", validate.FileHeader(c.Libvirt.QCOWImagePath, qcowMagic)); err != nil { + errs = append(errs, err) + } + if err := validate.PrefixError("libvirt sshKey", validate.NonEmpty(c.Libvirt.SSHKey)); err != nil { + errs = append(errs, err) + } + if err := validate.PrefixError("libvirt network name", validate.NonEmpty(c.Libvirt.Network.Name)); err != nil { + errs = append(errs, err) + } + if err := validate.PrefixError("libvirt network ifName", validate.NonEmpty(c.Libvirt.Network.IfName)); err != nil { + errs = append(errs, err) + } + if err := validate.PrefixError("libvirt network dnsServer", validate.IPv4(c.Libvirt.Network.DNSServer)); err != nil { + errs = append(errs, err) + } + if err := validate.PrefixError("libvirt ipRange and podCIDR", validate.CIDRsDontOverlap(c.Libvirt.Network.IPRange, c.Networking.PodCIDR)); err != nil { + errs = append(errs, err) + } + if err := validate.PrefixError("libvirt ipRange and serviceCIDR", validate.CIDRsDontOverlap(c.Libvirt.Network.IPRange, c.Networking.ServiceCIDR)); err != nil { + errs = append(errs, err) + } + return errs +} + func (c *Cluster) validateNetworking() []error { var errs []error // https://en.wikipedia.org/wiki/Maximum_transmission_unit#MTUs_for_common_media @@ -145,26 +195,8 @@ func (c *Cluster) validateNetworking() []error { if err := c.validateNetworkType(); err != nil { errs = append(errs, err) } - - var podOK, serviceOK bool - _, pod, err := net.ParseCIDR(c.Networking.PodCIDR) - if err != nil { - errs = append(errs, fmt.Errorf("invalid pod CIDR %q: %v", c.Networking.PodCIDR, err)) - } else if err := validate.CanonicalizeIP(&pod.IP); err != nil { - errs = append(errs, fmt.Errorf("invalid pod CIDR %q: %v", c.Networking.PodCIDR, err)) - } else { - podOK = true - } - _, service, err := net.ParseCIDR(c.Networking.ServiceCIDR) - if err != nil { - errs = append(errs, fmt.Errorf("invalid service CIDR %q: %v", c.Networking.ServiceCIDR, err)) - } else if err := validate.CanonicalizeIP(&service.IP); err != nil { - errs = append(errs, fmt.Errorf("invalid service CIDR %q: %v", c.Networking.ServiceCIDR, err)) - } else { - serviceOK = true - } - if podOK && serviceOK && validate.CIDRsOverlap(pod, service) { - errs = append(errs, errors.New("pod and service CIDRs overlap")) + if err := validate.PrefixError("pod and service CIDRs", validate.CIDRsDontOverlap(c.Networking.PodCIDR, c.Networking.ServiceCIDR)); err != nil { + errs = append(errs, err) } return errs } diff --git a/installer/pkg/config/validate_test.go b/installer/pkg/config/validate_test.go index e1863678a0..5c31794ffe 100644 --- a/installer/pkg/config/validate_test.go +++ b/installer/pkg/config/validate_test.go @@ -1,10 +1,12 @@ package config import ( + "io/ioutil" "os" "testing" "github.com/coreos/tectonic-installer/installer/pkg/config/aws" + "github.com/coreos/tectonic-installer/installer/pkg/config/libvirt" ) func TestMissingNodePool(t *testing.T) { @@ -570,3 +572,142 @@ func TestValidateCL(t *testing.T) { } } } + +func TestValidateLibvirt(t *testing.T) { + fValid, err := ioutil.TempFile("", "qcow") + if err != nil { + t.Fatalf("failed to create temporary file: %v", err) + } + if _, err := fValid.Write(qcowMagic); err != nil { + t.Fatalf("failed to write to temporary file: %v", err) + } + fValid.Close() + defer os.Remove(fValid.Name()) + fInvalid, err := ioutil.TempFile("", "qcow") + if err != nil { + t.Fatalf("failed to create temporary file: %v", err) + } + fInvalid.Close() + defer os.Remove(fInvalid.Name()) + cases := []struct { + cluster Cluster + err bool + }{ + { + cluster: Cluster{}, + err: true, + }, + { + cluster: defaultCluster, + err: true, + }, + { + cluster: Cluster{ + Libvirt: libvirt.Libvirt{ + Network: libvirt.Network{}, + QCOWImagePath: "", + SSHKey: "", + URI: "", + }, + Networking: defaultCluster.Networking, + }, + err: true, + }, + { + cluster: Cluster{ + Libvirt: libvirt.Libvirt{ + Network: libvirt.Network{ + Name: "tectonic", + IfName: libvirt.DefaultIfName, + DNSServer: libvirt.DefaultDNSServer, + IPRange: "10.0.1.0/24", + }, + QCOWImagePath: fInvalid.Name(), + SSHKey: "bar", + URI: "baz", + }, + Networking: defaultCluster.Networking, + }, + err: true, + }, + { + cluster: Cluster{ + Libvirt: libvirt.Libvirt{ + Network: libvirt.Network{ + Name: "tectonic", + IfName: libvirt.DefaultIfName, + DNSServer: libvirt.DefaultDNSServer, + IPRange: "10.0.1.0/24", + }, + QCOWImagePath: fValid.Name(), + SSHKey: "bar", + URI: "baz", + }, + Networking: defaultCluster.Networking, + }, + err: false, + }, + { + cluster: Cluster{ + Libvirt: libvirt.Libvirt{ + Network: libvirt.Network{ + Name: "tectonic", + IfName: libvirt.DefaultIfName, + DNSServer: libvirt.DefaultDNSServer, + IPRange: "10.2.1.0/24", + }, + QCOWImagePath: fValid.Name(), + SSHKey: "bar", + URI: "baz", + }, + Networking: defaultCluster.Networking, + }, + err: true, + }, + { + cluster: Cluster{ + Libvirt: libvirt.Libvirt{ + Network: libvirt.Network{ + Name: "tectonic", + IfName: libvirt.DefaultIfName, + DNSServer: libvirt.DefaultDNSServer, + IPRange: "x", + }, + QCOWImagePath: "foo", + SSHKey: "bar", + URI: "baz", + }, + Networking: defaultCluster.Networking, + }, + err: true, + }, + { + cluster: Cluster{ + Libvirt: libvirt.Libvirt{ + Network: libvirt.Network{ + Name: "tectonic", + IfName: libvirt.DefaultIfName, + DNSServer: "foo", + IPRange: "192.168.0.1/24", + }, + QCOWImagePath: "foo", + SSHKey: "bar", + URI: "baz", + }, + Networking: defaultCluster.Networking, + }, + err: true, + }, + } + + for i, c := range cases { + c.cluster.Platform = PlatformLibvirt + if err := c.cluster.validateLibvirt(); (err != nil) != c.err { + no := "no" + if c.err { + no = "an" + } + t.Errorf("test case %d: expected %s error, got %v", i, no, err) + } + } +} diff --git a/installer/pkg/validate/validate.go b/installer/pkg/validate/validate.go index 7d50e5dc37..6a881b3fe1 100644 --- a/installer/pkg/validate/validate.go +++ b/installer/pkg/validate/validate.go @@ -1,6 +1,7 @@ package validate import ( + "bytes" "encoding/json" "errors" "fmt" @@ -415,27 +416,42 @@ func OpenSSHPublicKey(v string) error { return nil } -// CIDRsOverlap checks whether two given CIDRs overlap -// with one another. CIDR starting IPs should be canonicalized +// CIDRsDontOverlap ensures two given CIDRs don't overlap +// with one another. CIDR starting IPs are canonicalized // before being compared. -func CIDRsOverlap(a, b *net.IPNet) bool { +func CIDRsDontOverlap(acidr, bcidr string) error { + _, a, err := net.ParseCIDR(acidr) + if err != nil { + return fmt.Errorf("invalid CIDR %q: %v", acidr, err) + } + if err := CanonicalizeIP(&a.IP); err != nil { + return fmt.Errorf("invalid CIDR %q: %v", acidr, err) + } + _, b, err := net.ParseCIDR(bcidr) + if err != nil { + return fmt.Errorf("invalid CIDR %q: %v", bcidr, err) + } + if err := CanonicalizeIP(&b.IP); err != nil { + return fmt.Errorf("invalid CIDR %q: %v", bcidr, err) + } + err = fmt.Errorf("%q and %q overlap", acidr, bcidr) // IPs are of different families. if len(a.IP) != len(b.IP) { - return false + return nil } if a.Contains(b.IP) { - return true + return err } if a.Contains(lastIP(b)) { - return true + return err } if b.Contains(a.IP) { - return true + return err } if b.Contains(lastIP(a)) { - return true + return err } - return false + return nil } // CanonicalizeIP ensures that the given IP is in standard form @@ -459,3 +475,19 @@ func lastIP(cidr *net.IPNet) net.IP { } return last } + +// FileHeader validates that the file at the specified path begins with the given string. +func FileHeader(path string, header []byte) error { + f, err := os.Open(path) + if err != nil { + return err + } + buf := make([]byte, len(header)) + if _, err := f.Read(buf); err != nil { + return err + } + if !bytes.Equal(buf, header) { + return fmt.Errorf("file %q does not begin with %q", path, string(header)) + } + return nil +} diff --git a/installer/pkg/validate/validate_test.go b/installer/pkg/validate/validate_test.go index 7abe3ab18a..3e6000b6ee 100644 --- a/installer/pkg/validate/validate_test.go +++ b/installer/pkg/validate/validate_test.go @@ -446,73 +446,46 @@ func TestOpenSSHPublicKey(t *testing.T) { runTests(t, "OpenSSHPublicKey", OpenSSHPublicKey, tests) } -func TestCIDRsOverlap(t *testing.T) { +func TestCIDRsDontOverlap(t *testing.T) { cases := []struct { - a net.IPNet - b net.IPNet - out bool + a string + b string + err bool }{ { - a: net.IPNet{ - IP: net.ParseIP("192.168.0.0").To4(), - Mask: net.CIDRMask(24, 32), - }, - b: net.IPNet{ - IP: net.ParseIP("192.168.0.0").To4(), - Mask: net.CIDRMask(24, 32), - }, - out: true, + a: "192.168.0.0/24", + b: "192.168.0.0/24", + err: true, }, { - a: net.IPNet{ - IP: net.ParseIP("192.168.0.0").To4(), - Mask: net.CIDRMask(24, 32), - }, - b: net.IPNet{ - IP: net.ParseIP("192.168.0.3").To4(), - Mask: net.CIDRMask(24, 32), - }, - out: true, + a: "192.168.0.0/24", + b: "192.168.0.3/24", + err: true, }, { - a: net.IPNet{ - IP: net.ParseIP("192.168.0.0").To4(), - Mask: net.CIDRMask(30, 32), - }, - b: net.IPNet{ - IP: net.ParseIP("192.168.0.3").To4(), - Mask: net.CIDRMask(30, 32), - }, - out: true, + a: "192.168.0.0/30", + b: "192.168.0.3/30", + err: true, }, { - a: net.IPNet{ - IP: net.ParseIP("192.168.0.0").To4(), - Mask: net.CIDRMask(30, 32), - }, - b: net.IPNet{ - IP: net.ParseIP("192.168.0.4").To4(), - Mask: net.CIDRMask(30, 32), - }, - out: false, + a: "192.168.0.0/30", + b: "192.168.0.4/30", + err: false, }, { - a: net.IPNet{ - IP: net.ParseIP("0.0.0.0").To4(), - Mask: net.CIDRMask(0, 32), - }, - b: net.IPNet{ - IP: net.ParseIP("192.168.0.0").To4(), - Mask: net.CIDRMask(24, 32), - }, - out: true, + a: "0.0.0.0/0", + b: "192.168.0.0/24", + err: true, }, } - var out bool for i, c := range cases { - if out = CIDRsOverlap(&c.a, &c.b); out != c.out { - t.Errorf("test case %d: expected %T but got %s", i, c.out, out) + if err := CIDRsDontOverlap(c.a, c.b); (err != nil) != c.err { + no := "no" + if c.err { + no = "an" + } + t.Errorf("test case %d: expected %s error, got %v", i, no, err) } } } @@ -711,3 +684,52 @@ func TestLicense(t *testing.T) { } } } + +func TestFileHeader(t *testing.T) { + cases := []struct { + actual []byte + expected []byte + err bool + }{ + { + actual: []byte{}, + expected: []byte("foo"), + err: true, + }, + { + actual: []byte("foo"), + expected: []byte("bar"), + err: true, + }, + { + actual: []byte("fooooo"), + expected: []byte("foo"), + err: false, + }, + { + actual: []byte("fooooo"), + expected: []byte{}, + err: false, + }, + } + + for i, c := range cases { + f, err := ioutil.TempFile("", "fileheader") + if err != nil { + t.Errorf("test case %d: failed to create temporary file: %v", i, err) + continue + } + if _, err := f.Write(c.actual); err != nil { + t.Errorf("test case %d: failed to write to temporary file: %v", i, err) + } + f.Close() + if err := FileHeader(f.Name(), c.expected); (err != nil) != c.err { + no := "no" + if c.err { + no = "an" + } + t.Errorf("test case %d: expected %s error, got %v", i, no, err) + } + os.Remove(f.Name()) + } +} diff --git a/installer/pkg/workflow/fixtures/terraform.tfvars b/installer/pkg/workflow/fixtures/terraform.tfvars index 15741bf80a..ed52362543 100644 --- a/installer/pkg/workflow/fixtures/terraform.tfvars +++ b/installer/pkg/workflow/fixtures/terraform.tfvars @@ -27,6 +27,8 @@ "tectonic_aws_worker_root_volume_iops": 100, "tectonic_aws_worker_root_volume_size": 30, "tectonic_aws_worker_root_volume_type": "gp2", + "tectonic_libvirt_network_if": "osbr0", + "tectonic_libvirt_resolver": "8.8.8.8", "tectonic_ignition_master": "ignition-master.ign", "tectonic_ignition_worker": "ignition-worker.ign", "tectonic_ignition_etcd": "ignition-etcd.ign" diff --git a/modules/libvirt/volume/main.tf b/modules/libvirt/volume/main.tf index 842ce0a425..97900b278b 100644 --- a/modules/libvirt/volume/main.tf +++ b/modules/libvirt/volume/main.tf @@ -1,5 +1,5 @@ -# Create a QOW volume from the downloaded path +# Create a QCOW volume from the downloaded path resource "libvirt_volume" "coreos_base" { name = "coreos_base" - source = "file://${var.coreos_qow_path}" + source = "file://${var.coreos_qcow_path}" } diff --git a/modules/libvirt/volume/variables.tf b/modules/libvirt/volume/variables.tf index 43898c542e..620a839d64 100644 --- a/modules/libvirt/volume/variables.tf +++ b/modules/libvirt/volume/variables.tf @@ -1,4 +1,4 @@ -variable "coreos_qow_path" { +variable "coreos_qcow_path" { description = "The path on disk to the coreos disk image" type = "string" } diff --git a/steps/topology/libvirt/main.tf b/steps/topology/libvirt/main.tf index 3685246bc7..9a5f1d6a15 100644 --- a/steps/topology/libvirt/main.tf +++ b/steps/topology/libvirt/main.tf @@ -23,7 +23,7 @@ resource "libvirt_network" "tectonic_net" { module "libvirt_base_volume" { source = "../../../modules/libvirt/volume" - coreos_qow_path = "${var.tectonic_coreos_qow_path}" + coreos_qcow_path = "${var.tectonic_coreos_qcow_path}" } locals { diff --git a/steps/variables-libvirt.tf b/steps/variables-libvirt.tf index af81b3e8ac..bc0cf10a5b 100644 --- a/steps/variables-libvirt.tf +++ b/steps/variables-libvirt.tf @@ -11,7 +11,6 @@ variable "tectonic_libvirt_network_name" { variable "tectonic_libvirt_network_if" { type = "string" description = "The name of the bridge to use" - default = "tt0" } variable "tectonic_libvirt_ip_range" { @@ -22,12 +21,11 @@ variable "tectonic_libvirt_ip_range" { variable "tectonic_libvirt_resolver" { type = "string" description = "the upstream dns resolver" - default = "8.8.8.8" } -variable "tectonic_coreos_qow_path" { +variable "tectonic_coreos_qcow_path" { type = "string" - description = "path to a container linux qow image" + description = "path to a container linux qcow image" } variable "tectonic_libvirt_master_ips" {