blob: 42acca596a84aad919e4fd750ea71b18924f7eef [file] [log] [blame]
package iso
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import (
"bytes"
"context"
"io/ioutil"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/jmoiron/sqlx"
"gopkg.in/DATA-DOG/go-sqlmock.v1"
)
func TestStreamISOCmd_stdout(t *testing.T) {
s, err := newStreamISOCmd("/tmp/nothing/here")
if err != nil {
t.Fatal(err)
}
// No custom/generate executable exists in the given directory,
// so isoDest should be blank meaning the command will write to
// STDOUT.
if s.isoDest != "" {
t.Fatalf("isoDest = %q; expected blank string", s.isoDest)
}
// Store the command for later comparison
expected := s.String()
// Modify the command to use the mock command helper
s.cmd = mockISOCmd(s.cmd, false, "")
// stream will invoke the command. It's expected
// to receive back an echo of the given command.
var b bytes.Buffer
if err = s.stream(&b); err != nil {
t.Fatal(err)
}
got := b.String()
if got != expected {
t.Fatalf("got: %q; expected: %q", got, expected)
}
t.Logf("got: %q", b.String())
}
func TestStreamISOCmd_stdout_err(t *testing.T) {
s, err := newStreamISOCmd("/tmp/nothing/here")
if err != nil {
t.Fatal(err)
}
// No custom/generate executable exists in the given directory,
// so isoDest should be blank meaning the command will write to
// STDOUT.
if s.isoDest != "" {
t.Fatalf("isoDest = %q; expected blank string", s.isoDest)
}
// Modify the command to use the mock command helper
// which will return an error.
s.cmd = mockISOCmd(s.cmd, true, "")
// stream will invoke the command. It's expected
// to receive back an error (since the mocked command
// is setup to return an error).
var b bytes.Buffer
err = s.stream(&b)
if err == nil {
t.Fatalf("stream() error: %v; expected non-nil", err)
}
t.Logf("got (expected) error: %q", err)
}
func TestStreamISOCmd_file(t *testing.T) {
// Create scratch directory
dir, err := ioutil.TempDir("", t.Name())
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
// Create custom executable that should be used instead of mkisofs
fd, err := os.OpenFile(filepath.Join(dir, ksAltCommand), os.O_CREATE|os.O_EXCL, 0777)
if err != nil {
t.Fatal(err)
}
defer fd.Close()
s, err := newStreamISOCmd(dir)
if err != nil {
t.Fatal(err)
}
// Ensure that the custom executable was found. The custom executable
// is expected to write the ISO image to a temporary file, which is what
// isoDest represents.
if s.isoDest == "" {
t.Fatalf("isoDest = %q; expected non-blank string", s.isoDest)
}
t.Logf("isoDest = %q", s.isoDest)
expected := s.String()
// Modify the command to use the mock command helper
s.cmd = mockISOCmd(s.cmd, false, s.isoDest)
// stream will invoke the command. It's expected
// to receive back an echo of the given command.
var b bytes.Buffer
if err = s.stream(&b); err != nil {
t.Fatal(err)
}
got := b.String()
if got != expected {
t.Fatalf("got: %q; expected: %q", got, expected)
}
t.Logf("got: %q", b.String())
// cleanup should remove the isoDest directory
s.cleanup()
isoDir := filepath.Dir(s.isoDest)
_, err = os.Stat(isoDir)
if !os.IsNotExist(err) {
t.Fatalf("stat of %q expected to receive NotExist error; got %v", isoDir, err)
}
t.Logf("stat of %q (expected): %v", isoDir, err)
}
func TestStreamISOCmd_file_err(t *testing.T) {
// Create scratch directory
dir, err := ioutil.TempDir("", t.Name())
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
// Create custom executable that should be used instead of mkisofs
fd, err := os.OpenFile(filepath.Join(dir, ksAltCommand), os.O_CREATE|os.O_EXCL, 0777)
if err != nil {
t.Fatal(err)
}
defer fd.Close()
s, err := newStreamISOCmd(dir)
if err != nil {
t.Fatal(err)
}
// Ensure that the custom executable was found. The custom executable
// is expected to write the ISO image to a temporary file, which is what
// isoDest represents.
if s.isoDest == "" {
t.Fatalf("isoDest = %q; expected non-blank string", s.isoDest)
}
t.Logf("isoDest = %q", s.isoDest)
// Modify the command to use the mock command helper
// which will return an error
s.cmd = mockISOCmd(s.cmd, true, s.isoDest)
// stream will invoke the command. It's expected
// to receive back an error (since the mocked command
// is setup to return an error).
var b bytes.Buffer
err = s.stream(&b)
if err == nil {
t.Fatalf("stream() error: %v; expected non-nil", err)
}
t.Logf("got (expected) error: %q", err)
}
func TestKickstarterDir(t *testing.T) {
cases := []struct {
name string
parameterValue string
osVersionsDir string
expected string
}{
{
"default",
"", // No DB parameter entry
"templeOS",
cfgDefaultDir + "/templeOS",
},
{
"param-override",
"/var/override/dir",
"anotherOS/dir",
"/var/override/dir/anotherOS/dir",
},
}
// Mock DB setup ...
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New() err: %v", err)
}
defer mockDB.Close()
db := sqlx.NewDb(mockDB, "sqlmock")
defer db.Close()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
// <mock DB row>
dbCtx, cancel := context.WithTimeout(context.TODO(), time.Duration(10)*time.Second)
defer cancel()
// Setup mock DB to return rows for SELECT query on parameter table.
// If parameterValue is empty, no rows will be returned.
mock.ExpectBegin()
cols := []string{"value"}
rows := sqlmock.NewRows(cols)
if tc.parameterValue != "" {
rows = rows.AddRow(tc.parameterValue)
}
mock.ExpectQuery("SELECT").WillReturnRows(rows)
mock.ExpectCommit()
tx, err := db.BeginTxx(dbCtx, nil)
if err != nil {
t.Fatalf("BeginTxx() err: %v", err)
}
defer tx.Commit()
// </mock DB row>
got, err := kickstarterDir(tx, tc.osVersionsDir)
if err != nil {
t.Fatalf("kickstarterDir(tx, %q) error: %v", tc.osVersionsDir, err)
}
if got != tc.expected {
t.Fatalf("kickstarterDir(tx, %q) = %q; expected %q", tc.osVersionsDir, got, tc.expected)
}
t.Logf("kickstarterDir(tx, %q) = %q", tc.osVersionsDir, got)
})
}
}
func TestWriteKSCfgs(t *testing.T) {
tmpPrefix := t.Name()
cases := []struct {
name string
cmd string
input isoRequest
}{
{
"empty",
"",
isoRequest{},
},
{
"complete",
"/usr/local/bin mkisofs arg1 -opt1 -opt2",
isoRequest{
RootPass: "password",
Disk: "sda1",
IPAddr: net.IP{192, 168, 1, 2},
IPNetmask: net.IP{255, 255, 255, 0},
IPGateway: net.IP{192, 168, 1, 255},
InterfaceName: "bond0",
InterfaceMTU: 1500,
HostName: "test.server",
DomainName: "cdn.example",
IP6Address: "beef::1",
IP6Gateway: net.ParseIP("::1"),
DHCP: boolStr{true, true},
MgmtIPAddress: net.IP{10, 10, 0, 1},
MgmtIPGateway: net.IP{10, 10, 0, 255},
MgmtIPNetmask: net.IP{10, 10, 255, 255},
MgmtInterface: "eth0",
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
dir, err := ioutil.TempDir("", tmpPrefix)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
if err := writeKSCfgs(dir, tc.input, tc.cmd); err != nil {
t.Fatalf("writeKSCfgs(%q, isoRequest) failed: %v", dir, err)
}
t.Logf("writeKSCfgs(%q, isoRequest):", dir)
expectedFiles := []string{
ksStateOut,
ksCfgNetwork,
ksCfgMgmtNetwork,
ksCfgPassword,
ksCfgDisk,
}
// Ensure that the expected files exist and are not empty.
for _, expectedFile := range expectedFiles {
p := filepath.Join(dir, expectedFile)
gotB, err := ioutil.ReadFile(p)
if err != nil {
t.Errorf("error reading %q: %v", p, err)
continue
}
got := string(gotB)
if strings.TrimSpace(got) == "" {
t.Errorf("%q file is empty; expected non-empty", p)
continue
}
t.Logf("%s:\n%s", expectedFile, got)
}
})
}
}
func TestWriteNetworkCfg(t *testing.T) {
cases := []struct {
name string
input isoRequest
nameservers []string
expected string
}{
{
"empty",
isoRequest{},
nil,
`
IPADDR=""
NETMASK=""
GATEWAY=""
DEVICE=""
MTU="0"
NAMESERVER=""
HOSTNAME=""
NETWORKING_IPV6="yes"
IPV6ADDR=""
IPV6_DEFAULTGW=""
DHCP="no"
`,
},
{
"no domain",
isoRequest{
IPAddr: net.IP{192, 168, 1, 2},
IPNetmask: net.IP{255, 255, 255, 0},
IPGateway: net.IP{192, 168, 1, 255},
InterfaceName: "eth0",
InterfaceMTU: 1500,
HostName: "test.server",
DomainName: "",
IP6Address: "beef::1",
IP6Gateway: net.ParseIP("::1"),
DHCP: boolStr{true, true},
},
[]string{"8.8.8.8", "1.1.1.1"},
`
IPADDR="192.168.1.2"
NETMASK="255.255.255.0"
GATEWAY="192.168.1.255"
DEVICE="eth0"
MTU="1500"
NAMESERVER="8.8.8.8,1.1.1.1"
HOSTNAME="test.server"
NETWORKING_IPV6="yes"
IPV6ADDR="beef::1"
IPV6_DEFAULTGW="::1"
DHCP="yes"
`,
},
{
"non-bonded",
isoRequest{
IPAddr: net.IP{192, 168, 1, 2},
IPNetmask: net.IP{255, 255, 255, 0},
IPGateway: net.IP{192, 168, 1, 255},
InterfaceName: "eth0",
InterfaceMTU: 1500,
HostName: "test.server",
DomainName: "example.com",
IP6Address: "beef::1",
IP6Gateway: net.ParseIP("::1"),
DHCP: boolStr{true, true},
},
[]string{"8.8.8.8", "1.1.1.1"},
`
IPADDR="192.168.1.2"
NETMASK="255.255.255.0"
GATEWAY="192.168.1.255"
DEVICE="eth0"
MTU="1500"
NAMESERVER="8.8.8.8,1.1.1.1"
HOSTNAME="test.server.example.com"
NETWORKING_IPV6="yes"
IPV6ADDR="beef::1"
IPV6_DEFAULTGW="::1"
DHCP="yes"
`,
},
{
"bonded",
isoRequest{
IPAddr: net.IP{192, 168, 1, 2},
IPNetmask: net.IP{255, 255, 255, 0},
IPGateway: net.IP{192, 168, 1, 255},
InterfaceName: "bond01",
InterfaceMTU: 1500,
HostName: "test.server",
DomainName: "",
IP6Address: "beef::1",
IP6Gateway: net.ParseIP("::1"),
DHCP: boolStr{true, true},
},
[]string{"8.8.8.8", "1.1.1.1"},
`
IPADDR="192.168.1.2"
NETMASK="255.255.255.0"
GATEWAY="192.168.1.255"
BOND_DEVICE="bond01"
MTU="1500"
NAMESERVER="8.8.8.8,1.1.1.1"
HOSTNAME="test.server"
NETWORKING_IPV6="yes"
IPV6ADDR="beef::1"
IPV6_DEFAULTGW="::1"
BONDING_OPTS="miimon=100 mode=4 lacp_rate=fast xmit_hash_policy=layer3+4"
DHCP="yes"
`,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
var w bytes.Buffer
if err := writeNetworkCfg(&w, tc.input, tc.nameservers); err != nil {
t.Fatalf("writeNetworkCfg() err = %v", err)
}
got := w.String()
expected := strings.TrimSpace(tc.expected)
if got != expected {
t.Fatalf("writeNetworkCfg() got != expected\n got:\n%s\n expected:\n%s", got, expected)
}
t.Logf("writeNetworkCfg():\n%s", got)
})
}
}
func TestWriteMgmtNetworkCfg(t *testing.T) {
cases := []struct {
name string
input isoRequest
expected string
}{
{
"empty",
isoRequest{},
`
IPADDR=""
NETMASK=""
GATEWAY=""
DEVICE=""
`,
},
{
"IPv4",
isoRequest{
MgmtIPAddress: net.IP{192, 168, 2, 3},
MgmtIPNetmask: net.IP{255, 255, 255, 255},
MgmtIPGateway: net.IP{192, 168, 1, 255},
MgmtInterface: "eth0",
},
`
IPADDR="192.168.2.3"
NETMASK="255.255.255.255"
GATEWAY="192.168.1.255"
DEVICE="eth0"
`,
},
{
"IPv6",
isoRequest{
MgmtIPAddress: net.ParseIP("beef::1"),
MgmtIPNetmask: net.IP{255, 255, 255, 255},
MgmtIPGateway: net.IP{192, 168, 1, 255},
MgmtInterface: "eth0",
},
`
IPV6ADDR="beef::1"
NETMASK="255.255.255.255"
GATEWAY="192.168.1.255"
DEVICE="eth0"
`,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
var w bytes.Buffer
if err := writeMgmtNetworkCfg(&w, tc.input); err != nil {
t.Fatalf("writeMgmtNetworkCfg() err = %v", err)
}
got := w.String()
expected := strings.TrimSpace(tc.expected)
if got != expected {
t.Fatalf("writeMgmtNetworkCfg() got != expected\n got:\n%s\n expected:\n%s", got, expected)
}
t.Logf("writeMgmtNetworkCfg():\n%s", got)
})
}
}
func TestWriteDiskCfg(t *testing.T) {
cases := []struct {
name string
input isoRequest
expected string
}{
{
"empty",
isoRequest{},
`
boot_drives=""
`,
},
{
"non-empty",
isoRequest{
Disk: "sda1",
},
`
boot_drives="sda1"
`,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
var w bytes.Buffer
if err := writeDiskCfg(&w, tc.input); err != nil {
t.Fatalf("writeDiskCfg() err = %v", err)
}
got := w.String()
expected := strings.TrimSpace(tc.expected)
if got != expected {
t.Fatalf("writeDiskCfg() got != expected\n got:\n%s\n expected:\n%s", got, expected)
}
t.Logf("writeDiskCfg():\n%s", got)
})
}
}
func TestWritePasswordCfg(t *testing.T) {
cases := []struct {
name string
input isoRequest
salt string
expected string
}{
{
"empty",
isoRequest{},
"salt",
"rootpw --iscrypted $1$salt$UsdFqFVB.FsuinRDK5eE..\n",
},
{
"non-empty",
isoRequest{
RootPass: "Traffic Ops",
},
"salt",
"rootpw --iscrypted $1$salt$17HeaymOIi.65dl76MkK01\n",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
var w bytes.Buffer
if err := writePasswordCfg(&w, tc.input, tc.salt); err != nil {
t.Fatalf("writePasswordCfg() err = %v", err)
}
got := w.String()
if got != tc.expected {
t.Fatalf("writePasswordCfg() got != expected\n got:\n%s\n expected:\n%s", got, tc.expected)
}
t.Logf("writePasswordCfg():\n%q", got)
})
}
}
func TestWritePasswordCfg_rndSalt(t *testing.T) {
cases := []struct {
name string
input isoRequest
}{
{
"empty",
isoRequest{},
},
{
"non-empty",
isoRequest{
RootPass: "Traffic Ops",
},
},
{
"long",
isoRequest{
RootPass: "this is a long password made longer even now",
},
},
}
const (
expectedPrefix = "rootpw --iscrypted $1$"
expectedPWLen = 32
)
// Ensure use of random salt generates correct looking passwords.
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
var w bytes.Buffer
if err := writePasswordCfg(&w, tc.input, ""); err != nil {
t.Fatalf("writePasswordCfg() err = %v", err)
}
got := w.String()
if !strings.HasPrefix(got, expectedPrefix) {
t.Fatalf("writePasswordCfg() got: %q\nexpected prefix of: %q", got, expectedPrefix)
}
if pwLen := len(got) - len(expectedPrefix); pwLen != expectedPWLen {
t.Fatalf("writePasswordCfg() got: %q with password length %d\nexpected password length of at least: %d", got, pwLen, expectedPWLen)
}
t.Logf("writePasswordCfg():\n%q", got)
})
}
}