@@ -21,6 +21,7 @@ import (
2121 exutil "github.com/openshift/origin/test/extended/util"
2222 corev1 "k8s.io/api/core/v1"
2323 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+ k8srand "k8s.io/apimachinery/pkg/util/rand"
2425 "k8s.io/kubernetes/test/e2e/framework"
2526)
2627
@@ -414,6 +415,230 @@ var _ = g.Describe("[sig-etcd][apigroup:config.openshift.io][OCPFeatureGate:Dual
414415 memberPromotedVotingTimeout , utils .FiveSecondPollInterval )
415416 })
416417
418+ g .It ("should update fencing credentials and validate fencing with updated credentials" , func () {
419+ bmcNode := targetNode
420+ survivedNode := peerNode
421+
422+ g .By (fmt .Sprintf ("Reading current fencing credentials for node %s" , bmcNode .Name ))
423+ creds , err := apis .FindFencingCredentialsByNodeName (oc , bmcNode .Name )
424+ o .Expect (err ).ToNot (o .HaveOccurred (), "expected to find fencing credentials secret" )
425+ framework .Logf ("Found fencing credentials secret %s (address: %s, username: %s)" ,
426+ creds .SecretName , creds .Address , creds .Username )
427+
428+ g .By ("Parsing Redfish address from fencing credentials" )
429+ redfishHost , redfishPort , redfishPath , err := apis .ParseRedfishAddress (creds .Address )
430+ o .Expect (err ).ToNot (o .HaveOccurred (), "expected to parse Redfish address" )
431+ framework .Logf ("Redfish endpoint: host=%s port=%s path=%s" , redfishHost , redfishPort , redfishPath )
432+
433+ isSushy := apis .IsSushyEmulator (redfishPath )
434+ var hypervisorSSH * core.SSHConfig
435+ var hypervisorKnownHosts string
436+ if isSushy {
437+ if ! exutil .HasHypervisorConfig () {
438+ g .Skip ("sushy-tools detected but no hypervisor SSH config available" )
439+ }
440+ sshCfg := exutil .GetHypervisorConfig ()
441+ o .Expect (sshCfg ).ToNot (o .BeNil (), "expected hypervisor config to parse" )
442+ hypervisorSSH = & core.SSHConfig {
443+ IP : sshCfg .HypervisorIP ,
444+ User : sshCfg .SSHUser ,
445+ PrivateKeyPath : sshCfg .PrivateKeyPath ,
446+ }
447+ var khErr error
448+ hypervisorKnownHosts , khErr = core .PrepareLocalKnownHostsFile (hypervisorSSH )
449+ o .Expect (khErr ).ToNot (o .HaveOccurred (), "expected to prepare hypervisor known_hosts" )
450+ framework .Logf ("Using sushy-tools password change via hypervisor SSH (%s)" , hypervisorSSH .IP )
451+ }
452+
453+ changeBMCPassword := func (currentPw , newPw string ) error {
454+ if isSushy {
455+ return apis .ChangeSushyToolsPassword (creds .Username , newPw , hypervisorSSH , hypervisorKnownHosts )
456+ }
457+ return apis .ChangeBMCPasswordViaRedfish (oc , bmcNode .Name , redfishHost , redfishPort ,
458+ creds .Username , currentPw , newPw )
459+ }
460+
461+ hasPacemakerCR := apis .IsPacemakerClusterAvailable (oc )
462+ if hasPacemakerCR {
463+ g .By ("Verifying PacemakerCluster CR is healthy before credential change" )
464+ pc , pcErr := apis .GetPacemakerCluster (oc )
465+ o .Expect (pcErr ).ToNot (o .HaveOccurred (), "expected to get PacemakerCluster CR" )
466+ o .Expect (apis .ExpectClusterHealthy (pc )).ToNot (o .HaveOccurred (), "expected PacemakerCluster to be healthy before credential change" )
467+ o .Expect (apis .ExpectNodeFencingHealthy (pc , bmcNode .Name )).ToNot (o .HaveOccurred (),
468+ "expected fencing to be healthy for %s before credential change" , bmcNode .Name )
469+ } else {
470+ framework .Logf ("PacemakerCluster CRD not available, skipping CR health checks" )
471+ }
472+
473+ sslInsecure := creds .CertificateVerification == "Disabled"
474+ originalPassword := creds .Password
475+ newPassword := k8srand .String (32 )
476+ nodeIdentifier := strings .TrimPrefix (creds .SecretName , "fencing-credentials-" )
477+
478+ scriptPath := "/etc/kubernetes/static-pod-resources/etcd-certs/configmaps/etcd-scripts/update-fencing-credentials.sh"
479+ bashCmd := scriptPath + ` --node "$1" --username "$2" --password "$3" --address "$4"`
480+ if sslInsecure {
481+ bashCmd += " --ssl-insecure"
482+ }
483+
484+ // On sushy-tools, a single htpasswd file serves all BMC endpoints, so changing
485+ // the password affects all nodes. Fetch the survived node's credentials so we
486+ // can update its stonith device and secret in lockstep.
487+ var survivedNodeCreds * apis.FencingCredentials
488+ var survivedNodeIdentifier string
489+ var survivedBashCmd string
490+ if isSushy {
491+ survivedNodeCreds , err = apis .FindFencingCredentialsByNodeName (oc , survivedNode .Name )
492+ o .Expect (err ).ToNot (o .HaveOccurred (), "expected to find survived node fencing credentials" )
493+ survivedNodeIdentifier = strings .TrimPrefix (survivedNodeCreds .SecretName , "fencing-credentials-" )
494+ survivedBashCmd = scriptPath + ` --node "$1" --username "$2" --password "$3" --address "$4"`
495+ if survivedNodeCreds .CertificateVerification == "Disabled" {
496+ survivedBashCmd += " --ssl-insecure"
497+ }
498+ framework .Logf ("sushy-tools: will also update survived node %s credentials (secret: %s)" ,
499+ survivedNode .Name , survivedNodeCreds .SecretName )
500+ }
501+
502+ g .DeferCleanup (func () {
503+ var cleanupFailed bool
504+
505+ framework .Logf ("Restoring original BMC password" )
506+ if restoreErr := changeBMCPassword (newPassword , originalPassword ); restoreErr != nil {
507+ fmt .Fprintf (g .GinkgoWriter , "Warning: failed to restore BMC password: %v\n " , restoreErr )
508+ cleanupFailed = true
509+ }
510+
511+ scriptPassword := originalPassword
512+ if cleanupFailed {
513+ scriptPassword = newPassword
514+ }
515+
516+ framework .Logf ("Re-running update-fencing-credentials.sh with original credentials" )
517+ output , restoreErr := exutil .DebugNodeRetryWithOptionsAndChroot (oc , bmcNode .Name , "openshift-etcd" ,
518+ "bash" , "-c" , bashCmd , "update-fencing-credentials" ,
519+ nodeIdentifier , creds .Username , scriptPassword , creds .Address )
520+ if restoreErr != nil {
521+ fmt .Fprintf (g .GinkgoWriter , "Warning: failed to restore fencing credentials via script: %v\n output: %s\n " ,
522+ restoreErr , output )
523+ }
524+
525+ if isSushy {
526+ framework .Logf ("Restoring survived node %s fencing credentials (sushy-tools shares credentials)" , survivedNode .Name )
527+ survivedOutput , survivedErr := exutil .DebugNodeRetryWithOptionsAndChroot (oc , survivedNode .Name , "openshift-etcd" ,
528+ "bash" , "-c" , survivedBashCmd , "update-fencing-credentials" ,
529+ survivedNodeIdentifier , survivedNodeCreds .Username , scriptPassword , survivedNodeCreds .Address )
530+ if survivedErr != nil {
531+ fmt .Fprintf (g .GinkgoWriter , "Warning: failed to restore survived node fencing credentials: %v\n output: %s\n " ,
532+ survivedErr , survivedOutput )
533+ }
534+ }
535+ })
536+
537+ g .By (fmt .Sprintf ("Changing BMC password on %s" , bmcNode .Name ))
538+ err = changeBMCPassword (originalPassword , newPassword )
539+ o .Expect (err ).ToNot (o .HaveOccurred (), "expected to change BMC password" )
540+
541+ g .By (fmt .Sprintf ("Validating new BMC credentials via fence_redfish on %s" , bmcNode .Name ))
542+ err = apis .ValidateBMCCredentials (oc , bmcNode .Name , redfishHost , redfishPort , redfishPath ,
543+ creds .Username , newPassword , sslInsecure )
544+ o .Expect (err ).ToNot (o .HaveOccurred (), "expected new BMC credentials to be valid" )
545+
546+ g .By (fmt .Sprintf ("Running update-fencing-credentials.sh on %s with new credentials" , bmcNode .Name ))
547+ output , err := exutil .DebugNodeRetryWithOptionsAndChroot (oc , bmcNode .Name , "openshift-etcd" ,
548+ "bash" , "-c" , bashCmd , "update-fencing-credentials" ,
549+ nodeIdentifier , creds .Username , newPassword , creds .Address )
550+ o .Expect (err ).ToNot (o .HaveOccurred (), "expected update-fencing-credentials.sh to succeed" )
551+ framework .Logf ("update-fencing-credentials.sh output:\n %s" , output )
552+
553+ if isSushy {
554+ g .By (fmt .Sprintf ("Updating survived node %s fencing credentials (sushy-tools shares credentials)" , survivedNode .Name ))
555+ survivedOutput , survivedErr := exutil .DebugNodeRetryWithOptionsAndChroot (oc , survivedNode .Name , "openshift-etcd" ,
556+ "bash" , "-c" , survivedBashCmd , "update-fencing-credentials" ,
557+ survivedNodeIdentifier , survivedNodeCreds .Username , newPassword , survivedNodeCreds .Address )
558+ o .Expect (survivedErr ).ToNot (o .HaveOccurred (),
559+ "expected update-fencing-credentials.sh for survived node to succeed" )
560+ framework .Logf ("update-fencing-credentials.sh output for survived node:\n %s" , survivedOutput )
561+ }
562+
563+ g .By ("Validating pacemaker health after credential update" )
564+ ctx , cancel := context .WithTimeout (context .Background (), nodeIsHealthyTimeout )
565+ defer cancel ()
566+ pcsOutput , err := services .PcsStatusViaDebug (ctx , oc , bmcNode .Name )
567+ o .Expect (err ).ToNot (o .HaveOccurred (), "expected pcs status to succeed" )
568+ failedActions := services .ExtractPcsFailedActions (pcsOutput )
569+ o .Expect (failedActions ).To (o .BeEmpty (), "expected no failed pacemaker resource actions after credential update" )
570+
571+ g .By ("Ensuring etcd members remain healthy after fencing credentials update" )
572+ o .Eventually (func () error {
573+ if err := helpers .EnsureHealthyMember (g .GinkgoT (), etcdClientFactory , survivedNode .Name ); err != nil {
574+ return err
575+ }
576+ if err := helpers .EnsureHealthyMember (g .GinkgoT (), etcdClientFactory , bmcNode .Name ); err != nil {
577+ return err
578+ }
579+ return nil
580+ }, nodeIsHealthyTimeout , utils .FiveSecondPollInterval ).ShouldNot (o .HaveOccurred (),
581+ "etcd members should be healthy after fencing credentials update" )
582+
583+ if hasPacemakerCR {
584+ g .By ("Verifying PacemakerCluster CR remains healthy after credential update" )
585+ o .Eventually (func () error {
586+ pc , pcErr := apis .GetPacemakerCluster (oc )
587+ if pcErr != nil {
588+ return pcErr
589+ }
590+ if pcErr = apis .ExpectClusterHealthy (pc ); pcErr != nil {
591+ return pcErr
592+ }
593+ return apis .ExpectNodeFencingHealthy (pc , bmcNode .Name )
594+ }, nodeIsHealthyTimeout , utils .FiveSecondPollInterval ).ShouldNot (o .HaveOccurred (),
595+ "expected PacemakerCluster to remain healthy after credential update" )
596+ }
597+
598+ g .By (fmt .Sprintf ("Triggering fencing-style network disruption between %s and %s" , bmcNode .Name , survivedNode .Name ))
599+ command , err := exutil .TriggerNetworkDisruption (oc .KubeClient (), & bmcNode , & survivedNode , networkDisruptionDuration )
600+ o .Expect (err ).To (o .BeNil (), "Expected to disrupt network without errors" )
601+ framework .Logf ("network disruption command: %q" , command )
602+
603+ g .By (fmt .Sprintf ("Ensuring cluster recovery after network disruption (timeout: %v)" , memberIsLeaderTimeout ))
604+ leaderNode , learnerNode , learnerStarted := validateEtcdRecoveryStateWithoutAssumingLeader (oc , etcdClientFactory ,
605+ & survivedNode , & bmcNode , memberIsLeaderTimeout , utils .FiveSecondPollInterval )
606+
607+ if learnerStarted {
608+ framework .Logf ("Learner node %q already started as learner after disruption" , learnerNode .Name )
609+ } else {
610+ g .By (fmt .Sprintf ("Ensuring '%s' rejoins as learner (timeout: %v)" , learnerNode .Name , memberRejoinedLearnerTimeout ))
611+ validateEtcdRecoveryState (oc , etcdClientFactory ,
612+ leaderNode ,
613+ learnerNode , true , true ,
614+ memberRejoinedLearnerTimeout , utils .FiveSecondPollInterval )
615+ }
616+
617+ g .By (fmt .Sprintf ("Ensuring learner node '%s' is promoted back as voting member (timeout: %v)" , learnerNode .Name , memberPromotedVotingTimeout ))
618+ validateEtcdRecoveryState (oc , etcdClientFactory ,
619+ leaderNode ,
620+ learnerNode , true , false ,
621+ memberPromotedVotingTimeout , utils .FiveSecondPollInterval )
622+
623+ if hasPacemakerCR {
624+ g .By ("Verifying PacemakerCluster CR is healthy after full recovery" )
625+ o .Eventually (func () error {
626+ pc , pcErr := apis .GetPacemakerCluster (oc )
627+ if pcErr != nil {
628+ return pcErr
629+ }
630+ if pcErr = apis .ExpectClusterHealthy (pc ); pcErr != nil {
631+ return pcErr
632+ }
633+ if pcErr = apis .ExpectNodeFencingHealthy (pc , bmcNode .Name ); pcErr != nil {
634+ return pcErr
635+ }
636+ return apis .ExpectNodeFencingHealthy (pc , survivedNode .Name )
637+ }, nodeIsHealthyTimeout , utils .FiveSecondPollInterval ).ShouldNot (o .HaveOccurred (),
638+ "expected PacemakerCluster to be fully healthy after recovery" )
639+ }
640+ })
641+
417642 g .It ("should compute etcd revision bump and preserve backup container after kernel panic recovery" , func () {
418643 // Note: This test triggers a kernel panic on one node via sysrq trigger, then verifies
419644 // the surviving node computes the etcd revision bump as floor(maxRaftIndex * 0.2) per
0 commit comments