Skip to content

Commit

Permalink
Merge pull request #1453 from Nordix/e2e-live-iso-ssh-implementation/max
Browse files Browse the repository at this point in the history
🌱E2e live iso ssh implementation
  • Loading branch information
metal3-io-bot authored Dec 1, 2023
2 parents 08b1857 + 111df80 commit 98b4dff
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 10 deletions.
2 changes: 1 addition & 1 deletion hack/ci-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ IMAGE_DIR="${REPO_ROOT}/test/e2e/images"
## Download and run image server
mkdir -p "${IMAGE_DIR}"
pushd "${IMAGE_DIR}"
wget --quiet "https://download.cirros-cloud.net/${CIRROS_VERSION}/${IMAGE_FILE}"
wget --quiet https://artifactory.nordix.org/artifactory/metal3/images/iso/"${IMAGE_FILE}"
popd

docker run --name image-server-e2e -d \
Expand Down
8 changes: 4 additions & 4 deletions hack/clean-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ docker rm -f vbmc
docker rm -f image-server-e2e
docker rm -f sushy-tools
virsh -c qemu:///system destroy --domain bmo-e2e-0
virsh -c qemu:///system undefine --domain bmo-e2e-0 --remove-all-storage
virsh -c qemu:///system undefine --domain bmo-e2e-0 --nvram --remove-all-storage
virsh -c qemu:///system net-destroy baremetal-e2e
virsh -c qemu:///system net-undefine baremetal-e2e

rm -rfv "${REPO_ROOT}/test/e2e/_artifacts"
rm -rfv "${REPO_ROOT}/artifacts.tar.gz"
rm -rfv "${REPO_ROOT}/test/e2e/images"
rm -rf "${REPO_ROOT}/test/e2e/_artifacts"
rm -rf "${REPO_ROOT}/artifacts.tar.gz"
rm -rf "${REPO_ROOT}/test/e2e/images"
3 changes: 2 additions & 1 deletion hack/e2e/net.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
<ip address='192.168.222.1' netmask='255.255.255.0'>
<dhcp>
<range start='192.168.222.2' end='192.168.222.199'/>
<host mac='52:54:00:6c:3c:01' name='vm1' ip='192.168.222.199'/>
<host mac='52:54:00:6c:3c:01' name='minikube' ip='192.168.222.199'/>
<host mac='00:60:2f:31:81:01' name='bmo-e2e-0' ip='192.168.222.122'/>
<bootp file='http://192.168.222.199:6180/boot.ipxe'/>
</dhcp>
</ip>
Expand Down
63 changes: 63 additions & 0 deletions test/e2e/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"strings"
"time"

"golang.org/x/crypto/ssh"

"github.com/pkg/errors"
"gopkg.in/yaml.v2"

Expand All @@ -19,6 +21,8 @@ import (

metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"

capm3_e2e "github.com/metal3-io/cluster-api-provider-metal3/test/e2e"

"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/cluster-api/test/framework"

Expand Down Expand Up @@ -296,3 +300,62 @@ func CreateBMHCredentialsSecret(ctx context.Context, client client.Client, secre

return nil
}

func executeSSHCommand(client *ssh.Client, command string) (string, error) {
session, err := client.NewSession()
if err != nil {
return "", fmt.Errorf("failed to create SSH session: %v", err)
}
defer session.Close()

output, err := session.CombinedOutput(command)
if err != nil {
return "", fmt.Errorf("failed to execute command '%s': %v", command, err)
}

return string(output), nil
}

// HasRootOnDisk parses the output from 'df -h' and checks if the root filesystem is on a disk (as opposed to tmpfs).
func HasRootOnDisk(output string) bool {
lines := strings.Split(output, "\n")

for _, line := range lines[1:] { // Skip header line
if line == "" {
continue
}

fields := strings.Fields(line)
if len(fields) < 6 {
continue // Skip malformed lines
}

if fields[5] == "/" && !strings.Contains(fields[0], "tmpfs") {
return true // Found a non-tmpfs root filesystem
}
}

return false
}

// IsBootedFromDisk checks if the system, accessed via the provided ssh.Client, is booted
// from a disk. It executes the 'df -h' command on the remote system to analyze the filesystem
// layout. In the case of a disk boot, the output includes a disk-based root filesystem
// (e.g., '/dev/vda1'). Conversely, in the case of a Live-ISO boot, the primary filesystems
// are memory-based (tmpfs).
func IsBootedFromDisk(client *ssh.Client) (bool, error) {
cmd := "df -h"
output, err := executeSSHCommand(client, cmd)
if err != nil {
return false, fmt.Errorf("error executing 'df -h': %w", err)
}

isDisk := HasRootOnDisk(output)
if isDisk {
capm3_e2e.Logf("System is booted from a disk.")
} else {
capm3_e2e.Logf("System is booted from a live ISO.")
}

return isDisk, nil
}
1 change: 1 addition & 0 deletions test/e2e/config/fixture.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ variables:
IMAGE_URL: "http://192.168.222.1/cirros-0.6.2-x86_64-disk.img"
IMAGE_CHECKSUM: "c8fc807773e5354afe61636071771906"
CERT_MANAGER_VERSION: "v1.13.0"
SSH_CHECK_PROVISIONED: "false"

intervals:
inspection/wait-unmanaged: ["1m", "10ms"]
Expand Down
6 changes: 5 additions & 1 deletion test/e2e/config/ironic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ variables:
DEPLOY_CERT_MANAGER: "true"
BMO_KUSTOMIZATION: "../../config/overlays/e2e"
IRONIC_KUSTOMIZATION: "../../ironic-deployment/overlays/e2e"
EXPECTED_HOST_NAME: "localhost.localdomain"
EXPECTED_HOST_NAME: "bmo-e2e-0"
# Test credentials. The tests will create a BMH with these.
BMC_USER: admin
BMC_PASSWORD: password
Expand All @@ -32,6 +32,9 @@ variables:
IMAGE_URL: "http://192.168.222.1/cirros-0.6.2-x86_64-disk.img"
IMAGE_CHECKSUM: "c8fc807773e5354afe61636071771906"
CERT_MANAGER_VERSION: "v1.13.0"
SSH_CHECK_PROVISIONED: "true"
IP_BMO_E2E_0: "192.168.222.122"
SSH_PORT: "22"

intervals:
inspection/wait-unmanaged: ["1m", "5s"]
Expand All @@ -49,3 +52,4 @@ intervals:
default/wait-deprovisioning: ["1m", "10ms"]
default/wait-deleted: ["20s", "10ms"]
default/wait-secret-deletion: ["1m", "1s"]
default/wait-connect-ssh: ["2m", "10s"]
2 changes: 1 addition & 1 deletion test/e2e/external_inspection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ const hardwareDetails = `
"version": "1.15.0-1"
}
},
"hostname": "localhost.localdomain",
"hostname": "bmo-e2e-0",
"nics": [
{
"ip": "192.168.222.122",
Expand Down
161 changes: 161 additions & 0 deletions test/e2e/live_iso_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package e2e

import (
"context"
"fmt"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"golang.org/x/crypto/ssh"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
"sigs.k8s.io/cluster-api/test/framework"
"sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/patch"

metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"

capm3_e2e "github.com/metal3-io/cluster-api-provider-metal3/test/e2e"
)

var _ = Describe("Live-ISO", func() {
var (
specName = "live-iso-ops"
secretName = "bmc-credentials"
namespace *corev1.Namespace
cancelWatches context.CancelFunc
bmcUser string
bmcPassword string
bmcAddress string
bootMacAddress string
imageURL string
)

BeforeEach(func() {
bmcUser = e2eConfig.GetVariable("BMC_USER")
bmcPassword = e2eConfig.GetVariable("BMC_PASSWORD")
bmcAddress = e2eConfig.GetVariable("BMC_ADDRESS")
bootMacAddress = e2eConfig.GetVariable("BOOT_MAC_ADDRESS")
imageURL = e2eConfig.GetVariable("IMAGE_URL")

namespace, cancelWatches = framework.CreateNamespaceAndWatchEvents(ctx, framework.CreateNamespaceAndWatchEventsInput{
Creator: clusterProxy.GetClient(),
ClientSet: clusterProxy.GetClientSet(),
Name: fmt.Sprintf("%s-%s", specName, util.RandomString(6)),
LogFolder: artifactFolder,
})
})

It("should provision a BMH with live ISO and then deprovision it", func() {
By("Creating a secret with BMH credentials")
CreateBMHCredentialsSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcUser, bmcPassword)

By("Creating a BMH with inspection disabled and hardware details added")
bmh := metal3api.BareMetalHost{
ObjectMeta: metav1.ObjectMeta{
Name: specName,
Namespace: namespace.Name,
Annotations: map[string]string{
metal3api.InspectAnnotationPrefix: "disabled",
metal3api.HardwareDetailsAnnotation: hardwareDetails,
},
},
Spec: metal3api.BareMetalHostSpec{
Online: true,
BMC: metal3api.BMCDetails{
Address: bmcAddress,
CredentialsName: secretName,
},
Image: &metal3api.Image{
URL: imageURL,
DiskFormat: pointer.String("live-iso"),
},
BootMode: metal3api.Legacy,
BootMACAddress: bootMacAddress,
AutomatedCleaningMode: "disabled",
RootDeviceHints: &metal3api.RootDeviceHints{
DeviceName: "/dev/vda",
},
},
}
err := clusterProxy.GetClient().Create(ctx, &bmh)
Expect(err).NotTo(HaveOccurred())

By("Waiting for the BMH to be in provisioning state")
WaitForBmhInProvisioningState(ctx, WaitForBmhInProvisioningStateInput{
Client: clusterProxy.GetClient(),
Bmh: bmh,
State: metal3api.StateProvisioning,
}, e2eConfig.GetIntervals(specName, "wait-provisioning")...)

By("Waiting for the BMH to become provisioned")
WaitForBmhInProvisioningState(ctx, WaitForBmhInProvisioningStateInput{
Client: clusterProxy.GetClient(),
Bmh: bmh,
State: metal3api.StateProvisioned,
}, e2eConfig.GetIntervals(specName, "wait-provisioned")...)

// The ssh check is not possible in all situations (e.g. fixture) so it can be skipped
if e2eConfig.GetVariable("SSH_CHECK_PROVISIONED") == "true" {
By("Verifying the node booted from live ISO image")

// Set up SSH client configuration
config := &ssh.ClientConfig{
User: "cirros",
Auth: []ssh.AuthMethod{
ssh.Password("gocubsgo"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // #nosec G106
}

ip := e2eConfig.GetVariable("IP_BMO_E2E_0")
sshPort := e2eConfig.GetVariable("SSH_PORT")
address := fmt.Sprintf("%s:%s", ip, sshPort)

// Establish an SSH connection
var client *ssh.Client
var err error

Eventually(func() error {
client, err = ssh.Dial("tcp", address, config)
return err
}, e2eConfig.GetIntervals(specName, "wait-connect-ssh")...).Should(Succeed(), "Failed to establish SSH connection")

defer func() {
if client != nil {
client.Close()
}
}()
isDisk, err := IsBootedFromDisk(client)
Expect(err).NotTo(HaveOccurred(), "Error in verifying boot mode")
Expect(isDisk).To(Equal(false), "Error booting from disk when live ISO is expected")
} else {
capm3_e2e.Logf("WARNING: Skipping ssh check since SSH_CHECK_PROVISIONED != true")
}

By("Triggering the deprovisioning of the BMH")
helper, err := patch.NewHelper(&bmh, clusterProxy.GetClient())
Expect(err).NotTo(HaveOccurred())
bmh.Spec.Image = nil
Expect(helper.Patch(ctx, &bmh)).To(Succeed())

By("Waiting for the BMH to be in deprovisioning state")
WaitForBmhInProvisioningState(ctx, WaitForBmhInProvisioningStateInput{
Client: clusterProxy.GetClient(),
Bmh: bmh,
State: metal3api.StateDeprovisioning,
}, e2eConfig.GetIntervals(specName, "wait-deprovisioning")...)

By("Waiting for the BMH to become available again")
WaitForBmhInProvisioningState(ctx, WaitForBmhInProvisioningStateInput{
Client: clusterProxy.GetClient(),
Bmh: bmh,
State: metal3api.StateAvailable,
}, e2eConfig.GetIntervals(specName, "wait-available")...)
})

AfterEach(func() {
cleanup(ctx, clusterProxy, namespace, cancelWatches, e2eConfig.GetIntervals("default", "wait-namespace-deleted")...)
})
})
2 changes: 1 addition & 1 deletion test/e2e/re_inspection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ var _ = Describe("Re-Inspection", func() {
CreateBMHCredentialsSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcUser, bmcPassword)

By("creating a BMH with inspection disabled and hardware details added with wrong HostName")
newHardwareDetails := strings.Replace(hardwareDetails, "localhost.localdomain", wrongHostName, 1)
newHardwareDetails := strings.Replace(hardwareDetails, "bmo-e2e-0", wrongHostName, 1)
bmh := metal3api.BareMetalHost{
ObjectMeta: metav1.ObjectMeta{
Name: specName + "-reinspect",
Expand Down
2 changes: 1 addition & 1 deletion test/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/onsi/ginkgo/v2 v2.13.0
github.com/onsi/gomega v1.29.0
github.com/pkg/errors v0.9.1
golang.org/x/crypto v0.14.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.27.7
k8s.io/apimachinery v0.27.7
Expand Down Expand Up @@ -108,7 +109,6 @@ require (
github.com/valyala/fastjson v1.6.4 // indirect
github.com/xlab/treeprint v1.1.0 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.17.0 // indirect
Expand Down

0 comments on commit 98b4dff

Please sign in to comment.