Repository: chef/automate
Pull request #7939 (2023-06-02 06:05:46)

Changes to make OS and PG fields optional for managed setup
### :nut_and_bolt: Description: What code changed, and why?

<!-- /!\ Please ensure that you are NOT disclosing any customer information without their consent /!\ -->
Added the changes to make the OS instance count and OS instance type along with PG instance count and PG instance type as optional for the managed setup.
These changes are applicable for both AWS and Onprem setup

### :chains: Related Resources
https://chefio.atlassian.net/browse/CHEF-3206

### :+1: Definition of Done
- AWS Managed setup should be successful without providing the OS and PG instance count and type
- Onprem Managed setup should be successful without providing the OS and PG instance count and type

### :athletic_shoe: How to Build and Test the Change

### :white_check_mark: Checklist

**All PRs** must tick these:

- [ ] I have read the [CONTRIBUTING document](https://github.com/chef/automate/blob/master/CONTRIBUTING.md).
- [ ] All commits signed-off for the [Developer Certification of Origin](https://github.com/chef/chef/blob/master/CONTRIBUTING.md#developer-certification-of-origin-dco).

With occasional exceptions, all PRs **from Progress employees** must tick these:

- [ ] Is the code clear? *(complicated code or lots of comments--subdivide and use well-named methods, meaningful variable names, etc.)*
- [ ] Consistency checked? *(user notifications, user prompts, visual patterns, code patterns, variable names)*
- [ ] Repeated code blocks eliminated? *(adapt and reuse existing components, blocks, functions, etc.)*
- [ ] Spelling, grammar, typos checked? *(at a minimum use `make spell` in any component directory)*
- [ ] Code well-formatted? *(indents, line breaks, etc. improve rather than hinder readability)*

All PRs **from Progress employees** should tick these if appropriate:

- [ ] Tests added/updated? (all new code needs new tests)
- [ ] Docs added/updated? (all customer-facing changes)

*Please add a note next to any checkbox above if you are NOT ticking it.*

### :camera: Screenshots, if applicable
https://progresssoftware.sharepoint.com/:v:/s/ChefCoreC/Eecj8a3gf95Pq0HukGrgmOIBc6spTJgeuU9o1R6zfuvVkA?e=UXRHYS

https://progresssoftware.sharepoint.com/:v:/s/ChefCoreC/ESjqnymm8FtLqTPdpQHEd7oBSIBwcegWZWJ99ouo8RuyXQ?e=4CsqFk

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *chef-automate* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 70919a83d30e27a9ee0c52a570409547a705cdb2 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/chef-automate/deploys/647986bd96c864000808fe77 |
sonarqube-for-infrastructure-prod[bot]: **SonarQube Quality Gate**

[![Quality Gate failed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v97/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonar.progress.com/dashboard?id=chef_automate_AYeOVEbqJ4YHsO5MtKWC&pullRequest=7939)

[![Failed condition](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v97/checks/FailedConditionIcon-16px.png 'Failed condition')](https://sonar.progress.com/dashboard?id=chef_automate_AYeOVEbqJ4YHsO5MtKWC&pullRequest=7939) [![C](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v97/checks/RatingBadge/C-16px.png 'C')](https://sonar.progress.com/dashboard?id=chef_automate_AYeOVEbqJ4YHsO5MtKWC&pullRequest=7939) Maintainability Rating on New Code (is worse than A)  

[See analysis details on SonarQube](https://sonar.progress.com/dashboard?id=chef_automate_AYeOVEbqJ4YHsO5MtKWC&pullRequest=7939)

Fix issues before they fail your Quality Gate with [![SonarLint](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v97/common/sonarlint-16px.png 'SonarLint')](https://www.sonarqube.org/sonarlint/?referrer=pull-request) [SonarLint in your IDE](https://www.sonarqube.org/sonarlint/?referrer=pull-request).
sonarqube-for-infrastructure-prod[bot]: **SonarQube Quality Gate**

[![Quality Gate failed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v97/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonar.progress.com/dashboard?id=chef_automate_AYeOVEbqJ4YHsO5MtKWC&pullRequest=7939)

[![Failed condition](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v97/checks/FailedConditionIcon-16px.png 'Failed condition')](https://sonar.progress.com/dashboard?id=chef_automate_AYeOVEbqJ4YHsO5MtKWC&pullRequest=7939) [![C](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v97/checks/RatingBadge/C-16px.png 'C')](https://sonar.progress.com/dashboard?id=chef_automate_AYeOVEbqJ4YHsO5MtKWC&pullRequest=7939) Maintainability Rating on New Code (is worse than A)  

[See analysis details on SonarQube](https://sonar.progress.com/dashboard?id=chef_automate_AYeOVEbqJ4YHsO5MtKWC&pullRequest=7939)

Fix issues before they fail your Quality Gate with [![SonarLint](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v97/common/sonarlint-16px.png 'SonarLint')](https://www.sonarqube.org/sonarlint/?referrer=pull-request) [SonarLint in your IDE](https://www.sonarqube.org/sonarlint/?referrer=pull-request).

Patch:
diff --git a/VERSION b/VERSION
index 75aac71b3..50e5eb6d9 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-4.7.41
\ No newline at end of file
+4.7.43
\ No newline at end of file
diff --git a/components/automate-cli/cmd/chef-automate/automateConfigUtils.go b/components/automate-cli/cmd/chef-automate/automateConfigUtils.go
index 02c282cc4..6cfa1d2df 100644
--- a/components/automate-cli/cmd/chef-automate/automateConfigUtils.go
+++ b/components/automate-cli/cmd/chef-automate/automateConfigUtils.go
@@ -7,6 +7,7 @@ import (
 	"io/ioutil"
 	"net"
 	"path/filepath"
+	"strings"
 
 	"github.com/chef/automate/components/automate-cli/pkg/status"
 	ptoml "github.com/pelletier/go-toml"
@@ -29,12 +30,12 @@ func getModeFromConfig(configPath string) (string, error) {
 		} else if config.Get("architecture.aws") != nil {
 			return AWS_MODE, nil
 		} else {
-			return AUTOMATE, nil
+			return strings.ToUpper(AUTOMATE), nil
 		}
 	} else if checkIfFileExist(filepath.Join(initConfigHabA2HAPathFlag.a2haDirPath, "a2ha.rb")) {
 		return HA_MODE, nil
 	} else {
-		return AUTOMATE, nil
+		return strings.ToUpper(AUTOMATE), nil
 	}
 }
 
diff --git a/components/automate-cli/cmd/chef-automate/automateConstants.go b/components/automate-cli/cmd/chef-automate/automateConstants.go
index 4adc03cca..e82cdba02 100644
--- a/components/automate-cli/cmd/chef-automate/automateConstants.go
+++ b/components/automate-cli/cmd/chef-automate/automateConstants.go
@@ -3,7 +3,6 @@ package main
 const (
 	AWS_MODE            = "AWS_MODE"
 	EXISTING_INFRA_MODE = "EXISTING_INFRA_MODE"
-	AUTOMATE            = "AUTOMATE"
 	HA_MODE             = "HA_MODE"
 )
 
@@ -72,4 +71,19 @@ const (
 	`
 
 	SUDO_PASSWORD_CMD = `echo "%s" | sudo -S bash -c "`
+
+	STOP_FE_SERVICES_CMD = `sudo systemctl stop chef-automate`
+	STOP_BE_SERVICES_CMD = `sudo systemctl stop hab-sup`
+
+	EXCLUDE_OPENSEARCH_NODE_REQUEST = `
+	curl --location --request PUT 'http://localhost:10144/_cluster/settings' \
+	--header 'Content-Type: application/json' \
+	--data-raw '{
+	  "persistent" :{
+		  "cluster.routing.allocation.exclude._ip" : "%s"
+	   }
+	}'
+	`
+
+	GET_OPENSEARCH_CLUSTER_SETTINGS = `curl --location --request GET 'http://localhost:10144/_cluster/settings'`
 )
diff --git a/components/automate-cli/cmd/chef-automate/automateHAAwsDeployment.go b/components/automate-cli/cmd/chef-automate/automateHAAwsDeployment.go
index c81bc0ffe..acb0463c4 100644
--- a/components/automate-cli/cmd/chef-automate/automateHAAwsDeployment.go
+++ b/components/automate-cli/cmd/chef-automate/automateHAAwsDeployment.go
@@ -164,11 +164,13 @@ func (a *awsDeployment) validateConfigFields() *list.List {
 	if len(a.config.ChefServer.Config.InstanceCount) < 1 {
 		errorList.PushBack("Invalid or empty chef-server instance_count")
 	}
-	if len(a.config.Opensearch.Config.InstanceCount) < 1 {
-		errorList.PushBack("Invalid or empty elastic-search instance_count")
-	}
-	if len(a.config.Postgresql.Config.InstanceCount) < 1 {
-		errorList.PushBack("Invalid or empty postgres-sql instance_count")
+	if !a.config.Aws.Config.SetupManagedServices {
+		if len(a.config.Opensearch.Config.InstanceCount) < 1 {
+			errorList.PushBack("Invalid or empty open-search instance_count")
+		}
+		if len(a.config.Postgresql.Config.InstanceCount) < 1 {
+			errorList.PushBack("Invalid or empty postgres-sql instance_count")
+		}
 	}
 	errorList.PushBackList(a.validateEnvFields())
 	errorList.PushBackList(a.validateCerts())
@@ -195,12 +197,6 @@ func (a *awsDeployment) validateEnvFields() *list.List {
 	if len(a.config.Aws.Config.ChefServerInstanceType) < 1 {
 		errorList.PushBack("Invalid or empty aws chef_server_instance_type")
 	}
-	if len(a.config.Aws.Config.OpensearchServerInstanceType) < 1 {
-		errorList.PushBack("Invalid or empty aws opensearch_server_instance_type")
-	}
-	if len(a.config.Aws.Config.PostgresqlServerInstanceType) < 1 {
-		errorList.PushBack("Invalid or empty aws postgresql_server_instance_type")
-	}
 	if len(a.config.Aws.Config.AutomateLbCertificateArn) < 1 {
 		errorList.PushBack("Invalid or empty aws automate_lb_certificate_arn")
 	}
@@ -226,6 +222,12 @@ func (a *awsDeployment) validateEnvFields() *list.List {
 		errorList.PushBack("Invalid or empty aws chef_ebs_volume_type")
 	}
 	if !a.config.Aws.Config.SetupManagedServices {
+		if len(a.config.Aws.Config.OpensearchServerInstanceType) < 1 {
+			errorList.PushBack("Invalid or empty aws opensearch_server_instance_type")
+		}
+		if len(a.config.Aws.Config.PostgresqlServerInstanceType) < 1 {
+			errorList.PushBack("Invalid or empty aws postgresql_server_instance_type")
+		}
 		if len(a.config.Aws.Config.OpensearchEbsVolumeIops) < 1 {
 			errorList.PushBack("Invalid or empty aws opensearch_ebs_volume_iops")
 		}
diff --git a/components/automate-cli/cmd/chef-automate/automateHADeployExistingInfra.go b/components/automate-cli/cmd/chef-automate/automateHADeployExistingInfra.go
index 0afb26545..ba0228055 100644
--- a/components/automate-cli/cmd/chef-automate/automateHADeployExistingInfra.go
+++ b/components/automate-cli/cmd/chef-automate/automateHADeployExistingInfra.go
@@ -149,12 +149,6 @@ func (e *existingInfra) validateConfigFields() *list.List {
 	if len(e.config.ChefServer.Config.InstanceCount) < 1 {
 		errorList.PushBack("Invalid or empty chef-server instance_count")
 	}
-	if len(e.config.Opensearch.Config.InstanceCount) < 1 {
-		errorList.PushBack("Invalid or empty open-search instance_count")
-	}
-	if len(e.config.Postgresql.Config.InstanceCount) < 1 {
-		errorList.PushBack("Invalid or empty postgres-sql instance_count")
-	}
 
 	if len(e.config.ExistingInfra.Config.AutomatePrivateIps) < 1 {
 		errorList.PushBack("Invalid or empty automate_private_ips")
@@ -164,6 +158,13 @@ func (e *existingInfra) validateConfigFields() *list.List {
 		errorList.PushBack("Invalid or empty chef_server_private_ips")
 	}
 	if (e.config.ExternalDB.Database.Type != "aws") && (e.config.ExternalDB.Database.Type != "self-managed") {
+		if len(e.config.Opensearch.Config.InstanceCount) < 1 {
+			errorList.PushBack("Invalid or empty open-search instance_count")
+		}
+		if len(e.config.Postgresql.Config.InstanceCount) < 1 {
+			errorList.PushBack("Invalid or empty postgres-sql instance_count")
+		}
+
 		if len(e.config.ExistingInfra.Config.OpensearchPrivateIps) < 1 {
 			errorList.PushBack("Invalid or empty opensearch_private_ips")
 		}
diff --git a/components/automate-cli/cmd/chef-automate/certRotate.go b/components/automate-cli/cmd/chef-automate/certRotate.go
index ac16be022..d8aedc14f 100644
--- a/components/automate-cli/cmd/chef-automate/certRotate.go
+++ b/components/automate-cli/cmd/chef-automate/certRotate.go
@@ -15,6 +15,7 @@ import (
 	"github.com/chef/automate/components/automate-cli/pkg/docs"
 	"github.com/chef/automate/components/automate-cli/pkg/status"
 	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/platform/command"
 	"github.com/chef/automate/lib/stringutils"
 	"github.com/chef/toml"
 	"github.com/pkg/errors"
@@ -165,13 +166,13 @@ func init() {
 		},
 	}
 
-	certRotateCmd.PersistentFlags().BoolVarP(&flagsObj.automate, CONST_AUTOMATE, "a", false, "Automate Certificate Rotation")
+	certRotateCmd.PersistentFlags().BoolVarP(&flagsObj.automate, AUTOMATE, "a", false, "Automate Certificate Rotation")
 	certRotateCmd.PersistentFlags().BoolVar(&flagsObj.automate, "a2", false, "Automate Certificate Rotation")
-	certRotateCmd.PersistentFlags().BoolVarP(&flagsObj.chefserver, CONST_CHEF_SERVER, "c", false, "Chef Infra Server Certificate Rotation")
+	certRotateCmd.PersistentFlags().BoolVarP(&flagsObj.chefserver, CHEF_SERVER, "c", false, "Chef Infra Server Certificate Rotation")
 	certRotateCmd.PersistentFlags().BoolVar(&flagsObj.chefserver, "cs", false, "Chef Infra Server Certificate Rotation")
-	certRotateCmd.PersistentFlags().BoolVarP(&flagsObj.postgres, CONST_POSTGRESQL, "p", false, "Postgres Certificate Rotation")
+	certRotateCmd.PersistentFlags().BoolVarP(&flagsObj.postgres, POSTGRESQL, "p", false, "Postgres Certificate Rotation")
 	certRotateCmd.PersistentFlags().BoolVar(&flagsObj.postgres, "pg", false, "Postgres Certificate Rotation")
-	certRotateCmd.PersistentFlags().BoolVarP(&flagsObj.opensearch, CONST_OPENSEARCH, "o", false, "OS Certificate Rotation")
+	certRotateCmd.PersistentFlags().BoolVarP(&flagsObj.opensearch, OPENSEARCH, "o", false, "OS Certificate Rotation")
 	certRotateCmd.PersistentFlags().BoolVar(&flagsObj.opensearch, "os", false, "OS Certificate Rotation")
 
 	certRotateCmd.PersistentFlags().StringVar(&flagsObj.privateCertPath, "private-cert", "", "Private certificate")
@@ -216,7 +217,7 @@ func (c *certRotateFlow) certRotate(cmd *cobra.Command, args []string, flagsObj
 
 		sshConfig := c.getSshDetails(infra)
 		sshUtil := NewSSHUtil(sshConfig)
-		certShowFlow := NewCertShowImpl(certShowFlags{}, NewNodeUtils(), sshUtil, writer)
+		certShowFlow := NewCertShowImpl(certShowFlags{}, NewNodeUtils(NewRemoteCmdExecutorWithoutNodeMap(NewSSHUtil(&SSHConfig{}), writer), command.NewExecExecutor(), writer), sshUtil, writer)
 		currentCertsInfo, err := certShowFlow.fetchCurrentCerts()
 
 		if err != nil {
@@ -262,9 +263,9 @@ func (c *certRotateFlow) certRotateFrontend(sshUtil SSHUtil, certs *certificates
 	var remoteService string
 
 	if flagsObj.automate {
-		remoteService = CONST_AUTOMATE
+		remoteService = AUTOMATE
 	} else if flagsObj.chefserver {
-		remoteService = CONST_CHEF_SERVER
+		remoteService = CHEF_SERVER
 	}
 	//get ips to exclude
 	skipIpsList := c.compareCurrentCertsWithNewCerts(remoteService, certs, flagsObj, currentCertsInfo)
@@ -296,8 +297,8 @@ func (c *certRotateFlow) certRotateFrontend(sshUtil SSHUtil, certs *certificates
 
 	// If we pass root-ca flag in automate then we need to update root-ca in the ChefServer to maintain the connection
 	if flagsObj.automate {
-		skipIpsList := c.getFrontEndIpsForSkippingRootCAPatching(CONST_AUTOMATE, certs.rootCA, infra, currentCertsInfo)
-		c.skipMessagePrinter(CONST_CHEF_SERVER, SKIP_FRONT_END_IPS_MSG_A2, "", skipIpsList)
+		skipIpsList := c.getFrontEndIpsForSkippingRootCAPatching(AUTOMATE, certs.rootCA, infra, currentCertsInfo)
+		c.skipMessagePrinter(CHEF_SERVER, SKIP_FRONT_END_IPS_MSG_A2, "", skipIpsList)
 		err = c.patchRootCAinCS(sshUtil, certs.rootCA, timestamp, infra, flagsObj, skipIpsList)
 		if err != nil {
 			return err
@@ -309,11 +310,11 @@ func (c *certRotateFlow) certRotateFrontend(sshUtil SSHUtil, certs *certificates
 // certRotatePG will rotate the certificates of Postgres.
 func (c *certRotateFlow) certRotatePG(sshUtil SSHUtil, certs *certificates, infra *AutomateHAInfraDetails, flagsObj *certRotateFlags, currentCertsInfo *certShowCertificates) error {
 	if isManagedServicesOn() {
-		return status.Errorf(status.InvalidCommandArgsError, ERROR_SELF_MANAGED_DB_CERT_ROTATE, CONST_POSTGRESQL)
+		return status.Errorf(status.InvalidCommandArgsError, ERROR_SELF_MANAGED_DB_CERT_ROTATE, POSTGRESQL)
 	}
 	fileName := "cert-rotate-pg.toml"
 	timestamp := time.Now().Format("20060102150405")
-	remoteService := CONST_POSTGRESQL
+	remoteService := POSTGRESQL
 
 	// Creating and patching the required configurations.
 	var config string
@@ -372,11 +373,11 @@ func (c *certRotateFlow) certRotatePG(sshUtil SSHUtil, certs *certificates, infr
 // certRotateOS will rotate the certificates of OpenSearch.
 func (c *certRotateFlow) certRotateOS(sshUtil SSHUtil, certs *certificates, infra *AutomateHAInfraDetails, flagsObj *certRotateFlags, currentCertsInfo *certShowCertificates) error {
 	if isManagedServicesOn() {
-		return status.Errorf(status.InvalidCommandArgsError, ERROR_SELF_MANAGED_DB_CERT_ROTATE, CONST_OPENSEARCH)
+		return status.Errorf(status.InvalidCommandArgsError, ERROR_SELF_MANAGED_DB_CERT_ROTATE, OPENSEARCH)
 	}
 	fileName := "cert-rotate-os.toml"
 	timestamp := time.Now().Format("20060102150405")
-	remoteService := CONST_OPENSEARCH
+	remoteService := OPENSEARCH
 
 	var adminDn pkix.Name
 	var err error
@@ -449,8 +450,8 @@ func (c *certRotateFlow) certRotateOS(sshUtil SSHUtil, certs *certificates, infr
 	if err != nil {
 		return err
 	}
-	
-	oldCn:=getOldCn(automatesConfig)
+
+	oldCn := getOldCn(automatesConfig)
 
 	// Patching root-ca to frontend-nodes for maintaining the connection.
 	cn := nodesCn
@@ -548,9 +549,9 @@ func (c *certRotateFlow) patchConfig(param *patchFnParameters) error {
 
 	// Defining set of commands which run on particular remoteservice nodes
 	var scriptCommands string
-	if param.remoteService == CONST_AUTOMATE || param.remoteService == CONST_CHEF_SERVER || param.remoteService == "frontend" {
+	if param.remoteService == AUTOMATE || param.remoteService == CHEF_SERVER || param.remoteService == "frontend" {
 		scriptCommands = fmt.Sprintf(FRONTEND_COMMAND, PATCH, param.remoteService+param.timestamp, dateFormat)
-	} else if param.remoteService == CONST_POSTGRESQL || param.remoteService == CONST_OPENSEARCH {
+	} else if param.remoteService == POSTGRESQL || param.remoteService == OPENSEARCH {
 		scriptCommands = fmt.Sprintf(COPY_USER_CONFIG, param.remoteService+param.timestamp, param.remoteService)
 	}
 	err = c.copyAndExecute(filteredIps, param.sshUtil, param.timestamp, param.remoteService, param.fileName, scriptCommands, param.flagsObj)
@@ -564,7 +565,7 @@ func (c *certRotateFlow) patchConfig(param *patchFnParameters) error {
 func (c *certRotateFlow) patchRootCAinCS(sshUtil SSHUtil, rootCA, timestamp string, infra *AutomateHAInfraDetails, flagsObj *certRotateFlags, skipIpsList []string) error {
 
 	fileName := "rotate-root_CA.toml"
-	remoteService := CONST_CHEF_SERVER
+	remoteService := CHEF_SERVER
 	cmd := `sudo chef-automate config show | grep fqdn | awk '{print $3}' | tr -d '"'`
 	ips := c.getIps(remoteService, infra)
 	if len(ips) == 0 {
@@ -653,13 +654,13 @@ func (c *certRotateFlow) getSshDetails(infra *AutomateHAInfraDetails) *SSHConfig
 
 // getIps will return the Ips based on the given remote service.
 func (c *certRotateFlow) getIps(remoteService string, infra *AutomateHAInfraDetails) []string {
-	if remoteService == CONST_AUTOMATE {
+	if remoteService == AUTOMATE {
 		return infra.Outputs.AutomatePrivateIps.Value
-	} else if remoteService == CONST_CHEF_SERVER {
+	} else if remoteService == CHEF_SERVER {
 		return infra.Outputs.ChefServerPrivateIps.Value
-	} else if remoteService == CONST_POSTGRESQL {
+	} else if remoteService == POSTGRESQL {
 		return infra.Outputs.PostgresqlPrivateIps.Value
-	} else if remoteService == CONST_OPENSEARCH {
+	} else if remoteService == OPENSEARCH {
 		return infra.Outputs.OpensearchPrivateIps.Value
 	} else if remoteService == "frontend" {
 		return append(infra.Outputs.AutomatePrivateIps.Value, infra.Outputs.ChefServerPrivateIps.Value...)
@@ -686,7 +687,7 @@ func (c *certRotateFlow) getAllIPs(infra *AutomateHAInfraDetails) []string {
 	return ips
 }
 
-//getFilteredIps will return ips on which cert-rotation need to perform.
+// getFilteredIps will return ips on which cert-rotation need to perform.
 func (c *certRotateFlow) getFilteredIps(serviceIps, skipIpsList []string) []string {
 	filteredIps := []string{}
 	for _, ip := range serviceIps {
@@ -697,11 +698,11 @@ func (c *certRotateFlow) getFilteredIps(serviceIps, skipIpsList []string) []stri
 	return filteredIps
 }
 
-//compareCurrentCertsWithNewCerts compare current certs and new certs and returns ips to skip cert-rotation.
+// compareCurrentCertsWithNewCerts compare current certs and new certs and returns ips to skip cert-rotation.
 func (c *certRotateFlow) compareCurrentCertsWithNewCerts(remoteService string, newCerts *certificates, flagsObj *certRotateFlags, currentCertsInfo *certShowCertificates) []string {
 	skipIpsList := []string{}
 	isCertsSame := true
-	if remoteService == CONST_AUTOMATE {
+	if remoteService == AUTOMATE {
 		if flagsObj.node == "" {
 			isCertsSame = strings.TrimSpace(currentCertsInfo.AutomateRootCert) == newCerts.rootCA
 		}
@@ -709,12 +710,12 @@ func (c *certRotateFlow) compareCurrentCertsWithNewCerts(remoteService string, n
 		return skipIpsList
 	}
 
-	if remoteService == CONST_CHEF_SERVER {
+	if remoteService == CHEF_SERVER {
 		skipIpsList = c.comparePublicCertAndPrivateCert(newCerts, currentCertsInfo.ChefServerCertsByIP, isCertsSame, flagsObj)
 		return skipIpsList
 	}
 
-	if remoteService == CONST_OPENSEARCH {
+	if remoteService == OPENSEARCH {
 		if flagsObj.node == "" {
 			isCertsSame = strings.TrimSpace(currentCertsInfo.OpensearchRootCert) == newCerts.rootCA
 			isCertsSame = (strings.TrimSpace(currentCertsInfo.OpensearchAdminCert) == newCerts.adminCert) && isCertsSame
@@ -724,7 +725,7 @@ func (c *certRotateFlow) compareCurrentCertsWithNewCerts(remoteService string, n
 		return skipIpsList
 	}
 
-	if remoteService == CONST_POSTGRESQL {
+	if remoteService == POSTGRESQL {
 		if flagsObj.node == "" {
 			isCertsSame = strings.TrimSpace(currentCertsInfo.PostgresqlRootCert) == newCerts.rootCA
 		}
@@ -734,7 +735,7 @@ func (c *certRotateFlow) compareCurrentCertsWithNewCerts(remoteService string, n
 	return skipIpsList
 }
 
-//comparePublicCertAndPrivateCert compare new public-cert and private cert with current public-cert and private-cert and returns ips to skip cert-rotation.
+// comparePublicCertAndPrivateCert compare new public-cert and private cert with current public-cert and private-cert and returns ips to skip cert-rotation.
 func (c *certRotateFlow) comparePublicCertAndPrivateCert(newCerts *certificates, certByIpList []CertByIP, isCertsSame bool, flagsObj *certRotateFlags) []string {
 	skipIpsList := []string{}
 	for _, currentCerts := range certByIpList {
@@ -753,26 +754,26 @@ func (c *certRotateFlow) comparePublicCertAndPrivateCert(newCerts *certificates,
 	return skipIpsList
 }
 
-//getFrontEndIpsForSkippingRootCAPatching compare new root-ca and current root-ca of remoteService and returns ips to skip root-ca patching.
+// getFrontEndIpsForSkippingRootCAPatching compare new root-ca and current root-ca of remoteService and returns ips to skip root-ca patching.
 func (c *certRotateFlow) getFrontEndIpsForSkippingRootCAPatching(remoteService string, newRootCA string, infra *AutomateHAInfraDetails, currentCertsInfo *certShowCertificates) []string {
 	skipIpsList := []string{}
 
-	if remoteService == CONST_POSTGRESQL {
+	if remoteService == POSTGRESQL {
 		if strings.TrimSpace(currentCertsInfo.PostgresqlRootCert) == newRootCA {
 			skipIpsList = append(skipIpsList, c.getIps("frontend", infra)...)
 		}
 	}
 
-	if remoteService == CONST_AUTOMATE {
+	if remoteService == AUTOMATE {
 		if strings.TrimSpace(currentCertsInfo.AutomateRootCert) == newRootCA {
-			skipIpsList = append(skipIpsList, c.getIps(CONST_CHEF_SERVER, infra)...)
+			skipIpsList = append(skipIpsList, c.getIps(CHEF_SERVER, infra)...)
 		}
 	}
 
 	return skipIpsList
 }
 
-//getFrontEndIpsForSkippingCnAndRootCaPatching compare new root-ca and new cn with current root-ca and cn and returns ips to skip root-ca patching.
+// getFrontEndIpsForSkippingCnAndRootCaPatching compare new root-ca and new cn with current root-ca and cn and returns ips to skip root-ca patching.
 func (c *certRotateFlow) getFrontEndIpsForSkippingCnAndRootCaPatching(newRootCA, newCn, oldCn, node string, currentCertsInfo *certShowCertificates, infra *AutomateHAInfraDetails) []string {
 	isRootCaSame := false
 
@@ -1015,7 +1016,7 @@ func (c *certRotateFlow) getMerger(fileName string, timestamp string, remoteType
 		dest interface{}
 		err1 error
 	)
-	if remoteType == CONST_OPENSEARCH {
+	if remoteType == OPENSEARCH {
 		dest, err1 = getMergedOpensearchInterface(rawOutput, fileName, remoteType)
 	} else {
 		dest, err1 = getMergedPostgresqlInterface(rawOutput, fileName, remoteType)
diff --git a/components/automate-cli/cmd/chef-automate/cmdUtils.go b/components/automate-cli/cmd/chef-automate/cmdUtils.go
index 1823041e8..084e2bd14 100644
--- a/components/automate-cli/cmd/chef-automate/cmdUtils.go
+++ b/components/automate-cli/cmd/chef-automate/cmdUtils.go
@@ -52,6 +52,8 @@ type CmdResult struct {
 
 type RemoteCmdExecutor interface {
 	Execute() (map[string][]*CmdResult, error)
+	ExecuteWithNodeMap(nodeMap *NodeTypeAndCmd) (map[string][]*CmdResult, error)
+	GetSshUtil() SSHUtil
 }
 
 type remoteCmdExecutor struct {
@@ -60,6 +62,24 @@ type remoteCmdExecutor struct {
 	Output  *cli.Writer
 }
 
+type MockRemoteCmdExecutor struct {
+	ExecuteFunc            func() (map[string][]*CmdResult, error)
+	ExecuteWithNodeMapFunc func(nodeMap *NodeTypeAndCmd) (map[string][]*CmdResult, error)
+	GetSshUtilFunc         func() SSHUtil
+}
+
+func (m *MockRemoteCmdExecutor) Execute() (map[string][]*CmdResult, error) {
+	return m.ExecuteFunc()
+}
+
+func (m *MockRemoteCmdExecutor) ExecuteWithNodeMap(nodeMap *NodeTypeAndCmd) (map[string][]*CmdResult, error) {
+	return m.ExecuteWithNodeMapFunc(nodeMap)
+}
+
+func (m *MockRemoteCmdExecutor) GetSshUtil() SSHUtil {
+	return m.GetSshUtilFunc()
+}
+
 func NewRemoteCmdExecutor(nodeMap *NodeTypeAndCmd, sshUtil SSHUtil, writer *cli.Writer) RemoteCmdExecutor {
 	return &remoteCmdExecutor{
 		NodeMap: nodeMap,
@@ -68,57 +88,76 @@ func NewRemoteCmdExecutor(nodeMap *NodeTypeAndCmd, sshUtil SSHUtil, writer *cli.
 	}
 }
 
+func NewRemoteCmdExecutorWithoutNodeMap(sshUtil SSHUtil, writer *cli.Writer) RemoteCmdExecutor {
+	return &remoteCmdExecutor{
+		SshUtil: sshUtil,
+		Output:  writer,
+	}
+}
+
+func (c *remoteCmdExecutor) GetSshUtil() SSHUtil {
+	return c.SshUtil
+}
+
+func (c *remoteCmdExecutor) ExecuteWithNodeMap(nodeMap *NodeTypeAndCmd) (map[string][]*CmdResult, error) {
+	return c.execute(nodeMap)
+}
+
 func (c *remoteCmdExecutor) Execute() (map[string][]*CmdResult, error) {
+	return c.execute(c.NodeMap)
+}
+
+func (c *remoteCmdExecutor) execute(nodeMap *NodeTypeAndCmd) (map[string][]*CmdResult, error) {
 	timestamp := time.Now().Format("20060102150405")
 	cmdResult := map[string][]*CmdResult{}
 
-	sshConfig := getSshDetails(c.NodeMap.Infra)
+	sshConfig := getSshDetails(nodeMap.Infra)
 	c.SshUtil.setSSHConfig(sshConfig)
 
 	switch true {
-	case c.NodeMap.Frontend.CmdInputs.NodeType:
-		const remoteService string = CONST_FRONTEND
-		nodeIps, err := preCmdExecCheck(c.NodeMap.Frontend, c.SshUtil, c.NodeMap.Infra, remoteService, timestamp, writer)
+	case nodeMap.Frontend.CmdInputs.NodeType:
+		const remoteService string = FRONTEND
+		nodeIps, err := preCmdExecCheck(nodeMap.Frontend, c.SshUtil, nodeMap.Infra, remoteService, timestamp, writer)
 		if err != nil {
 			return cmdResult, err
 		}
-		output := c.executeCmdOnGivenNodes(c.NodeMap.Frontend.CmdInputs, nodeIps, remoteService, timestamp, writer)
+		output := c.executeCmdOnGivenNodes(nodeMap.Frontend.CmdInputs, nodeIps, remoteService, timestamp, writer)
 		return output, nil
-	case c.NodeMap.Automate.CmdInputs.NodeType:
-		const remoteService string = CONST_AUTOMATE
-		nodeIps, err := preCmdExecCheck(c.NodeMap.Automate, c.SshUtil, c.NodeMap.Infra, remoteService, timestamp, writer)
+	case nodeMap.Automate.CmdInputs.NodeType:
+		const remoteService string = AUTOMATE
+		nodeIps, err := preCmdExecCheck(nodeMap.Automate, c.SshUtil, nodeMap.Infra, remoteService, timestamp, writer)
 		if err != nil {
 			return cmdResult, err
 		}
 
-		output := c.executeCmdOnGivenNodes(c.NodeMap.Automate.CmdInputs, nodeIps, remoteService, timestamp, writer)
+		output := c.executeCmdOnGivenNodes(nodeMap.Automate.CmdInputs, nodeIps, remoteService, timestamp, writer)
 		return output, nil
-	case c.NodeMap.ChefServer.CmdInputs.NodeType:
-		const remoteService string = CONST_CHEF_SERVER
-		nodeIps, err := preCmdExecCheck(c.NodeMap.ChefServer, c.SshUtil, c.NodeMap.Infra, remoteService, timestamp, writer)
+	case nodeMap.ChefServer.CmdInputs.NodeType:
+		const remoteService string = CHEF_SERVER
+		nodeIps, err := preCmdExecCheck(nodeMap.ChefServer, c.SshUtil, nodeMap.Infra, remoteService, timestamp, writer)
 		if err != nil {
 			return cmdResult, err
 		}
 
-		output := c.executeCmdOnGivenNodes(c.NodeMap.ChefServer.CmdInputs, nodeIps, remoteService, timestamp, writer)
+		output := c.executeCmdOnGivenNodes(nodeMap.ChefServer.CmdInputs, nodeIps, remoteService, timestamp, writer)
 		return output, nil
-	case c.NodeMap.Postgresql.CmdInputs.NodeType:
-		const remoteService string = CONST_POSTGRESQL
-		nodeIps, err := preCmdExecCheck(c.NodeMap.Postgresql, c.SshUtil, c.NodeMap.Infra, remoteService, timestamp, writer)
+	case nodeMap.Postgresql.CmdInputs.NodeType:
+		const remoteService string = POSTGRESQL
+		nodeIps, err := preCmdExecCheck(nodeMap.Postgresql, c.SshUtil, nodeMap.Infra, remoteService, timestamp, writer)
 		if err != nil {
 			return cmdResult, err
 		}
 
-		output := c.executeCmdOnGivenNodes(c.NodeMap.Postgresql.CmdInputs, nodeIps, remoteService, timestamp, writer)
+		output := c.executeCmdOnGivenNodes(nodeMap.Postgresql.CmdInputs, nodeIps, remoteService, timestamp, writer)
 		return output, nil
-	case c.NodeMap.Opensearch.CmdInputs.NodeType:
-		const remoteService string = CONST_OPENSEARCH
-		nodeIps, err := preCmdExecCheck(c.NodeMap.Opensearch, c.SshUtil, c.NodeMap.Infra, remoteService, timestamp, writer)
+	case nodeMap.Opensearch.CmdInputs.NodeType:
+		const remoteService string = OPENSEARCH
+		nodeIps, err := preCmdExecCheck(nodeMap.Opensearch, c.SshUtil, nodeMap.Infra, remoteService, timestamp, writer)
 		if err != nil {
 			return cmdResult, err
 		}
 
-		output := c.executeCmdOnGivenNodes(c.NodeMap.Opensearch.CmdInputs, nodeIps, remoteService, timestamp, writer)
+		output := c.executeCmdOnGivenNodes(nodeMap.Opensearch.CmdInputs, nodeIps, remoteService, timestamp, writer)
 		return output, nil
 	default:
 		return cmdResult, errors.New("Missing or Unsupported flag")
@@ -296,15 +335,15 @@ func getSshDetails(infra *AutomateHAInfraDetails) *SSHConfig {
 func getNodeIPs(single bool, ip string, infra *AutomateHAInfraDetails, remoteService string) ([]string, error) {
 	nodeIps := []string{}
 	switch {
-	case remoteService == CONST_OPENSEARCH:
+	case remoteService == OPENSEARCH:
 		nodeIps = infra.Outputs.OpensearchPrivateIps.Value
-	case remoteService == CONST_POSTGRESQL:
+	case remoteService == POSTGRESQL:
 		nodeIps = infra.Outputs.PostgresqlPrivateIps.Value
-	case remoteService == CONST_CHEF_SERVER:
+	case remoteService == CHEF_SERVER:
 		nodeIps = infra.Outputs.ChefServerPrivateIps.Value
-	case remoteService == CONST_AUTOMATE:
+	case remoteService == AUTOMATE:
 		nodeIps = infra.Outputs.AutomatePrivateIps.Value
-	case remoteService == CONST_FRONTEND:
+	case remoteService == FRONTEND:
 		nodeIps = append(infra.Outputs.AutomatePrivateIps.Value, infra.Outputs.ChefServerPrivateIps.Value...)
 	}
 	if ip != "" {
@@ -329,9 +368,7 @@ func GetSingleIp(ips []string) (string, error) {
 
 // isValidIP will check whether the given ip is in the given remoteservice ips set or not.
 func isValidIP(ip string, ips []string) bool {
-	ipv4Regex := `^(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4})`
-	match, _ := regexp.MatchString(ipv4Regex, ip)
-	if !match {
+	if !isValidIPFormat(ip) {
 		return false
 	}
 	for _, clusterIP := range ips {
@@ -342,6 +379,14 @@ func isValidIP(ip string, ips []string) bool {
 	return false
 }
 
+func isValidIPFormat(ip string) bool {
+	if strings.TrimSpace(ip) == "" {
+		return false
+	}
+	match, _ := regexp.MatchString(IPV4REGEX, ip)
+	return match
+}
+
 // printOutput of the remote jobs
 func printOutput(remoteService string, result CmdResult, outputFiles []string, cliWriter *cli.Writer) {
 	var resultFiles []string
diff --git a/components/automate-cli/cmd/chef-automate/constants.go b/components/automate-cli/cmd/chef-automate/constants.go
index f7bdead09..48d27e28d 100644
--- a/components/automate-cli/cmd/chef-automate/constants.go
+++ b/components/automate-cli/cmd/chef-automate/constants.go
@@ -1,12 +1,13 @@
 package main
 
 const (
-	CONST_FRONTEND    = "frontend"
-	CONST_AUTOMATE    = "automate"
-	CONST_CHEF_SERVER = "chef_server"
-	CONST_POSTGRESQL  = "postgresql"
-	CONST_OPENSEARCH  = "opensearch"
-	SET               = "set"
-	PATCH             = "patch"
-	SUDO_PASSWORD     = "sudo_password"
+	FRONTEND      = "frontend"
+	AUTOMATE      = "automate"
+	CHEF_SERVER   = "chef_server"
+	POSTGRESQL    = "postgresql"
+	OPENSEARCH    = "opensearch"
+	SET           = "set"
+	PATCH         = "patch"
+	SUDO_PASSWORD = "sudo_password"
+	IPV4REGEX     = `^(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4})`
 )
diff --git a/components/automate-cli/cmd/chef-automate/ha_cert_show.go b/components/automate-cli/cmd/chef-automate/ha_cert_show.go
index 35e2aefd7..4fdab8076 100644
--- a/components/automate-cli/cmd/chef-automate/ha_cert_show.go
+++ b/components/automate-cli/cmd/chef-automate/ha_cert_show.go
@@ -7,6 +7,7 @@ import (
 	"github.com/chef/automate/components/automate-cli/pkg/docs"
 	"github.com/chef/automate/components/automate-cli/pkg/status"
 	"github.com/chef/automate/components/automate-deployment/pkg/cli"
+	"github.com/chef/automate/lib/platform/command"
 	"github.com/chef/automate/lib/stringutils"
 	"github.com/spf13/cobra"
 )
@@ -68,16 +69,16 @@ func init() {
 		},
 	}
 
-	certShowCmd.PersistentFlags().BoolVarP(&flagsObj.automate, CONST_AUTOMATE, "a", false, "Show Automate Certificates")
+	certShowCmd.PersistentFlags().BoolVarP(&flagsObj.automate, AUTOMATE, "a", false, "Show Automate Certificates")
 	certShowCmd.PersistentFlags().BoolVar(&flagsObj.automate, "a2", false, "Show Automate Certificates")
 
-	certShowCmd.PersistentFlags().BoolVarP(&flagsObj.chefserver, CONST_CHEF_SERVER, "c", false, "Show Chef Server Certificates")
+	certShowCmd.PersistentFlags().BoolVarP(&flagsObj.chefserver, CHEF_SERVER, "c", false, "Show Chef Server Certificates")
 	certShowCmd.PersistentFlags().BoolVar(&flagsObj.chefserver, "cs", false, "Show Chef Server Certificates")
 
-	certShowCmd.PersistentFlags().BoolVarP(&flagsObj.postgresql, CONST_POSTGRESQL, "p", false, "Show Postgres Certificates")
+	certShowCmd.PersistentFlags().BoolVarP(&flagsObj.postgresql, POSTGRESQL, "p", false, "Show Postgres Certificates")
 	certShowCmd.PersistentFlags().BoolVar(&flagsObj.postgresql, "pg", false, "Show Postgres Certificates")
 
-	certShowCmd.PersistentFlags().BoolVarP(&flagsObj.opensearch, CONST_OPENSEARCH, "o", false, "Show Opensearch Certificates")
+	certShowCmd.PersistentFlags().BoolVarP(&flagsObj.opensearch, OPENSEARCH, "o", false, "Show Opensearch Certificates")
 	certShowCmd.PersistentFlags().BoolVar(&flagsObj.opensearch, "os", false, "Show Opensearch Certificates")
 
 	certShowCmd.PersistentFlags().StringVarP(&flagsObj.node, "node", "n", "", "Service cluster's node IP address to show certificates, if not provided then all nodes certificates will be shown")
@@ -98,7 +99,7 @@ func NewCertShowImpl(flags certShowFlags, nodeUtils NodeOpUtils, sshUtil SSHUtil
 // certShowCmdFunc is the main function for the cert show command
 func certShowCmdFunc(flagsObj *certShowFlags) func(cmd *cobra.Command, args []string) error {
 	return func(cmd *cobra.Command, args []string) error {
-		cs := NewCertShowImpl(*flagsObj, NewNodeUtils(), NewSSHUtil(&SSHConfig{}), writer)
+		cs := NewCertShowImpl(*flagsObj, NewNodeUtils(NewRemoteCmdExecutorWithoutNodeMap(NewSSHUtil(&SSHConfig{}), writer), command.NewExecExecutor(), writer), NewSSHUtil(&SSHConfig{}), writer)
 		return cs.certShow(cmd, args)
 	}
 }
@@ -155,32 +156,32 @@ func (c *certShowImpl) fetchCurrentCerts() (*certShowCertificates, error) {
 
 func (c *certShowImpl) validateAndPrintCertificates(remoteService string, certInfo *certShowCertificates) error {
 	switch remoteService {
-	case CONST_AUTOMATE:
-		if err := c.validateNode(certInfo.AutomateCertsByIP, CONST_AUTOMATE); err != nil {
+	case AUTOMATE:
+		if err := c.validateNode(certInfo.AutomateCertsByIP, AUTOMATE); err != nil {
 			return err
 		}
-		c.printAutomateAndCSCertificates(certInfo.AutomateRootCert, certInfo.AutomateCertsByIP, stringutils.Title(CONST_AUTOMATE))
-	case CONST_CHEF_SERVER:
-		if err := c.validateNode(certInfo.ChefServerCertsByIP, CONST_CHEF_SERVER); err != nil {
+		c.printAutomateAndCSCertificates(certInfo.AutomateRootCert, certInfo.AutomateCertsByIP, stringutils.Title(AUTOMATE))
+	case CHEF_SERVER:
+		if err := c.validateNode(certInfo.ChefServerCertsByIP, CHEF_SERVER); err != nil {
 			return err
 		}
-		c.printAutomateAndCSCertificates("", certInfo.ChefServerCertsByIP, stringutils.TitleSplit(CONST_CHEF_SERVER, "_"))
-	case CONST_POSTGRESQL:
+		c.printAutomateAndCSCertificates("", certInfo.ChefServerCertsByIP, stringutils.TitleSplit(CHEF_SERVER, "_"))
+	case POSTGRESQL:
 		if c.nodeUtils.isManagedServicesOn() {
 			return status.New(status.InvalidCommandArgsError, "This command is not supported in Managed Services")
 		}
-		if err := c.validateNode(certInfo.PostgresqlCertsByIP, CONST_POSTGRESQL); err != nil {
+		if err := c.validateNode(certInfo.PostgresqlCertsByIP, POSTGRESQL); err != nil {
 			return err
 		}
-		c.printPostgresqlAndOSCertificates(certInfo.PostgresqlRootCert, "", "", certInfo.PostgresqlCertsByIP, stringutils.Title(CONST_POSTGRESQL))
-	case CONST_OPENSEARCH:
+		c.printPostgresqlAndOSCertificates(certInfo.PostgresqlRootCert, "", "", certInfo.PostgresqlCertsByIP, stringutils.Title(POSTGRESQL))
+	case OPENSEARCH:
 		if c.nodeUtils.isManagedServicesOn() {
 			return status.New(status.InvalidCommandArgsError, "This command is not supported in Managed Services")
 		}
-		if err := c.validateNode(certInfo.OpensearchCertsByIP, CONST_OPENSEARCH); err != nil {
+		if err := c.validateNode(certInfo.OpensearchCertsByIP, OPENSEARCH); err != nil {
 			return err
 		}
-		c.printPostgresqlAndOSCertificates(certInfo.OpensearchRootCert, certInfo.OpensearchAdminKey, certInfo.OpensearchAdminCert, certInfo.OpensearchCertsByIP, stringutils.Title(CONST_OPENSEARCH))
+		c.printPostgresqlAndOSCertificates(certInfo.OpensearchRootCert, certInfo.OpensearchAdminKey, certInfo.OpensearchAdminCert, certInfo.OpensearchCertsByIP, stringutils.Title(OPENSEARCH))
 	default:
 		c.printAllCertificates(certInfo)
 	}
@@ -225,12 +226,12 @@ func (c *certShowImpl) getCerts(config interface{}) (*certShowCertificates, erro
 
 // printAllCertificates prints all certificates
 func (c *certShowImpl) printAllCertificates(certInfo *certShowCertificates) {
-	c.printAutomateAndCSCertificates(certInfo.AutomateRootCert, certInfo.AutomateCertsByIP, stringutils.Title(CONST_AUTOMATE))
-	c.printAutomateAndCSCertificates("", certInfo.ChefServerCertsByIP, stringutils.TitleSplit(CONST_CHEF_SERVER, "_"))
+	c.printAutomateAndCSCertificates(certInfo.AutomateRootCert, certInfo.AutomateCertsByIP, stringutils.Title(AUTOMATE))
+	c.printAutomateAndCSCertificates("", certInfo.ChefServerCertsByIP, stringutils.TitleSplit(CHEF_SERVER, "_"))
 
 	if !c.nodeUtils.isManagedServicesOn() {
-		c.printPostgresqlAndOSCertificates(certInfo.PostgresqlRootCert, "", "", certInfo.PostgresqlCertsByIP, stringutils.Title(CONST_POSTGRESQL))
-		c.printPostgresqlAndOSCertificates(certInfo.OpensearchRootCert, certInfo.OpensearchAdminKey, certInfo.OpensearchAdminCert, certInfo.OpensearchCertsByIP, stringutils.Title(CONST_OPENSEARCH))
+		c.printPostgresqlAndOSCertificates(certInfo.PostgresqlRootCert, "", "", certInfo.PostgresqlCertsByIP, stringutils.Title(POSTGRESQL))
+		c.printPostgresqlAndOSCertificates(certInfo.OpensearchRootCert, certInfo.OpensearchAdminKey, certInfo.OpensearchAdminCert, certInfo.OpensearchCertsByIP, stringutils.Title(OPENSEARCH))
 	}
 }
 
@@ -239,7 +240,7 @@ func (c *certShowImpl) printAutomateAndCSCertificates(rootCA string, certsByIP [
 	c.writer.Title(fmt.Sprintf("%s Certificates", remoteServiceName))
 	c.writer.HR()
 
-	if remoteServiceName == stringutils.Title(CONST_AUTOMATE) {
+	if remoteServiceName == stringutils.Title(AUTOMATE) {
 		c.writer.Println("========================Automate Root CA========================")
 		if len(strings.TrimSpace(rootCA)) == 0 {
 			c.writer.Println("No Automate root certificate found")
@@ -277,7 +278,7 @@ func (c *certShowImpl) printPostgresqlAndOSCertificates(rootCA, adminKey, adminC
 		c.writer.Println(rootCA)
 	}
 
-	if remoteServiceName == stringutils.Title(CONST_OPENSEARCH) {
+	if remoteServiceName == stringutils.Title(OPENSEARCH) {
 		c.writer.Println("\n======================Opensearch Admin Key======================")
 		if len(strings.TrimSpace(adminKey)) == 0 {
 			c.writer.Println("No admin key found")
@@ -372,13 +373,13 @@ func (c *certShowImpl) isCommonCerts(certs []CertByIP) bool {
 // getRemoteService returns the remote service name based on the flags
 func (c *certShowImpl) getRemoteService() string {
 	if c.flags.automate {
-		return CONST_AUTOMATE
+		return AUTOMATE
 	} else if c.flags.chefserver {
-		return CONST_CHEF_SERVER
+		return CHEF_SERVER
 	} else if c.flags.postgresql {
-		return CONST_POSTGRESQL
+		return POSTGRESQL
 	} else if c.flags.opensearch {
-		return CONST_OPENSEARCH
+		return OPENSEARCH
 	} else {
 		return ""
 	}
diff --git a/components/automate-cli/cmd/chef-automate/ha_node_add.go b/components/automate-cli/cmd/chef-automate/ha_node_add.go
index 2109e7b9f..de90457eb 100644
--- a/components/automate-cli/cmd/chef-automate/ha_node_add.go
+++ b/components/automate-cli/cmd/chef-automate/ha_node_add.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/chef/automate/components/automate-cli/pkg/docs"
 	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/platform/command"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 )
@@ -64,13 +65,13 @@ func haAddNodeFactory(addDeleteNodeHACmdFlags *AddDeleteNodeHACmdFlags, deployer
 	switch deployerType {
 	case EXISTING_INFRA_MODE:
 		if !addDeleteNodeHACmdFlags.awsMode {
-			hamd = NewAddNodeOnPrem(writer, *addDeleteNodeHACmdFlags, NewNodeUtils(), initConfigHabA2HAPathFlag.a2haDirPath, &fileutils.FileSystemUtils{}, NewSSHUtil(&SSHConfig{}))
+			hamd = NewAddNodeOnPrem(writer, *addDeleteNodeHACmdFlags, NewNodeUtils(NewRemoteCmdExecutorWithoutNodeMap(NewSSHUtil(&SSHConfig{}), writer), command.NewExecExecutor(), writer), initConfigHabA2HAPathFlag.a2haDirPath, &fileutils.FileSystemUtils{}, NewSSHUtil(&SSHConfig{}))
 		} else {
 			err = fmt.Errorf("Flag given does not match with the current deployment type %s. Try with --onprem-mode flag", deployerType)
 		}
 	case AWS_MODE:
 		if !addDeleteNodeHACmdFlags.onPremMode {
-			hamd = NewAddNodeAWS(writer, *addDeleteNodeHACmdFlags, NewNodeUtils(), initConfigHabA2HAPathFlag.a2haDirPath, &fileutils.FileSystemUtils{}, NewSSHUtil(&SSHConfig{}))
+			hamd = NewAddNodeAWS(writer, *addDeleteNodeHACmdFlags, NewNodeUtils(NewRemoteCmdExecutorWithoutNodeMap(NewSSHUtil(&SSHConfig{}), writer), command.NewExecExecutor(), writer), initConfigHabA2HAPathFlag.a2haDirPath, &fileutils.FileSystemUtils{}, NewSSHUtil(&SSHConfig{}))
 		} else {
 			err = fmt.Errorf("Flag given does not match with the current deployment type %s. Try with --aws-mode flag", deployerType)
 		}
diff --git a/components/automate-cli/cmd/chef-automate/ha_node_delete.go b/components/automate-cli/cmd/chef-automate/ha_node_delete.go
index b7ed5896c..392f781d4 100644
--- a/components/automate-cli/cmd/chef-automate/ha_node_delete.go
+++ b/components/automate-cli/cmd/chef-automate/ha_node_delete.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/chef/automate/components/automate-cli/pkg/docs"
 	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/platform/command"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 )
@@ -51,13 +52,13 @@ func haDeleteNodeFactory(addDeleteNodeHACmdFlags *AddDeleteNodeHACmdFlags, deplo
 	switch deployerType {
 	case EXISTING_INFRA_MODE:
 		if !addDeleteNodeHACmdFlags.awsMode {
-			hamd = NewDeleteNodeOnPrem(writer, *addDeleteNodeHACmdFlags, NewNodeUtils(), initConfigHabA2HAPathFlag.a2haDirPath, &fileutils.FileSystemUtils{}, NewSSHUtil(&SSHConfig{}))
+			hamd = NewDeleteNodeOnPrem(writer, *addDeleteNodeHACmdFlags, NewNodeUtils(NewRemoteCmdExecutorWithoutNodeMap(NewSSHUtil(&SSHConfig{}), writer), command.NewExecExecutor(), writer), initConfigHabA2HAPathFlag.a2haDirPath, &fileutils.FileSystemUtils{}, NewSSHUtil(&SSHConfig{}))
 		} else {
 			err = fmt.Errorf("flag given does not match with the current deployment type %s. Try with --onprem-mode flag", deployerType)
 		}
 	case AWS_MODE:
 		if !addDeleteNodeHACmdFlags.onPremMode {
-			hamd = NewDeleteNodeAWS(writer, *addDeleteNodeHACmdFlags, NewNodeUtils(), initConfigHabA2HAPathFlag.a2haDirPath, &fileutils.FileSystemUtils{}, NewSSHUtil(&SSHConfig{}))
+			hamd = NewDeleteNodeAWS(writer, *addDeleteNodeHACmdFlags, NewNodeUtils(NewRemoteCmdExecutorWithoutNodeMap(NewSSHUtil(&SSHConfig{}), writer), command.NewExecExecutor(), writer), initConfigHabA2HAPathFlag.a2haDirPath, &fileutils.FileSystemUtils{}, NewSSHUtil(&SSHConfig{}))
 		} else {
 			err = fmt.Errorf("flag given does not match with the current deployment type %s. Try with --aws-mode flag", deployerType)
 		}
diff --git a/components/automate-cli/cmd/chef-automate/ha_node_delete_aws.go b/components/automate-cli/cmd/chef-automate/ha_node_delete_aws.go
index 6eb9c5232..a4e62eb60 100644
--- a/components/automate-cli/cmd/chef-automate/ha_node_delete_aws.go
+++ b/components/automate-cli/cmd/chef-automate/ha_node_delete_aws.go
@@ -9,30 +9,22 @@ import (
 	"github.com/chef/automate/components/automate-cli/pkg/status"
 	"github.com/chef/automate/components/automate-deployment/pkg/cli"
 	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/stringutils"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 )
 
-type SvcDetails struct {
-	ipList         []string
-	configIpList   []string
-	instanceCount  string
-	minInstanceCnt int
-	name           string
-}
 type DeleteNodeAWSImpl struct {
-	config           AwsConfigToml
-	automateIpList   []string
-	chefServerIpList []string
-	opensearchIpList []string
-	postgresqlIpList []string
-	nodeUtils        NodeOpUtils
-	flags            AddDeleteNodeHACmdFlags
-	configpath       string
-	terraformPath    string
-	writer           *cli.Writer
-	fileUtils        fileutils.FileUtils
-	sshUtil          SSHUtil
+	config        AwsConfigToml
+	ipToDelete    string
+	nodeType      string
+	nodeUtils     NodeOpUtils
+	flags         AddDeleteNodeHACmdFlags
+	configpath    string
+	terraformPath string
+	writer        *cli.Writer
+	fileUtils     fileutils.FileUtils
+	sshUtil       SSHUtil
 	AWSConfigIp
 }
 
@@ -57,18 +49,14 @@ var (
 
 func NewDeleteNodeAWS(writer *cli.Writer, flags AddDeleteNodeHACmdFlags, nodeUtils NodeOpUtils, haDirPath string, fileutils fileutils.FileUtils, sshUtil SSHUtil) HAModifyAndDeploy {
 	return &DeleteNodeAWSImpl{
-		config:           AwsConfigToml{},
-		automateIpList:   []string{},
-		chefServerIpList: []string{},
-		opensearchIpList: []string{},
-		postgresqlIpList: []string{},
-		nodeUtils:        nodeUtils,
-		flags:            flags,
-		configpath:       filepath.Join(haDirPath, "config.toml"),
-		terraformPath:    filepath.Join(haDirPath, "terraform"),
-		writer:           writer,
-		fileUtils:        fileutils,
-		sshUtil:          sshUtil,
+		config:        AwsConfigToml{},
+		nodeUtils:     nodeUtils,
+		flags:         flags,
+		configpath:    filepath.Join(haDirPath, "config.toml"),
+		terraformPath: filepath.Join(haDirPath, "terraform"),
+		writer:        writer,
+		fileUtils:     fileutils,
+		sshUtil:       sshUtil,
 	}
 }
 
@@ -101,40 +89,82 @@ func (dna *DeleteNodeAWSImpl) Execute(c *cobra.Command, args []string) error {
 			return nil
 		}
 	}
-	dna.prepare()
 
-	return dna.runDeploy()
-}
-func (dna *DeleteNodeAWSImpl) modifyConfig() error {
-	dna.config.Architecture.ConfigInitials.Architecture = "aws"
-	err := modifyConfigForDeleteNodeForAWS(
-		&dna.config.Automate.Config.InstanceCount,
-		dna.automateIpList,
-	)
+	previousCount, err := dna.nodeUtils.calculateTotalInstanceCount()
 	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error modifying automate instance count")
+		return err
 	}
-	err = modifyConfigForDeleteNodeForAWS(
-		&dna.config.ChefServer.Config.InstanceCount,
-		dna.chefServerIpList,
-	)
+
+	err = dna.prepare()
 	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error modifying chef-server instance count")
+		return err
 	}
-	err = modifyConfigForDeleteNodeForAWS(
-		&dna.config.Opensearch.Config.InstanceCount,
-		dna.opensearchIpList,
-	)
-	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error modifying opensearch instance count")
+
+	err = dna.runDeploy()
+	currentCount, newErr := dna.nodeUtils.calculateTotalInstanceCount()
+	if newErr != nil {
+		if err != nil {
+			return errors.Wrap(err, newErr.Error())
+		}
+		return newErr
 	}
-	err = modifyConfigForDeleteNodeForAWS(
-		&dna.config.Postgresql.Config.InstanceCount,
-		dna.postgresqlIpList,
-	)
+
+	if currentCount == previousCount-1 {
+		stopErr := dna.stopNodes()
+		if stopErr != nil {
+			if err != nil {
+				return errors.Wrap(err, stopErr.Error())
+			}
+			return stopErr
+		}
+	} else {
+		if err != nil {
+			return errors.Wrap(err, "Error in deleting node")
+		}
+		return errors.New("Error in deleting node")
+	}
+
+	if err == nil {
+		dna.writer.Println("Node is successfully deleted from the cluster.")
+	}
+
+	return err
+}
+
+func (dna *DeleteNodeAWSImpl) modifyConfig() error {
+	dna.config.Architecture.ConfigInitials.Architecture = "aws"
+
+	var err error
+
+	switch dna.nodeType {
+	case AUTOMATE:
+		err = modifyConfigForDeleteNodeForAWS(
+			&dna.config.Automate.Config.InstanceCount,
+			[]string{dna.ipToDelete},
+		)
+	case CHEF_SERVER:
+		err = modifyConfigForDeleteNodeForAWS(
+			&dna.config.ChefServer.Config.InstanceCount,
+			[]string{dna.ipToDelete},
+		)
+	case POSTGRESQL:
+		err = modifyConfigForDeleteNodeForAWS(
+			&dna.config.Postgresql.Config.InstanceCount,
+			[]string{dna.ipToDelete},
+		)
+	case OPENSEARCH:
+		err = modifyConfigForDeleteNodeForAWS(
+			&dna.config.Opensearch.Config.InstanceCount,
+			[]string{dna.ipToDelete},
+		)
+	default:
+		return errors.New("Invalid node type")
+	}
+
 	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error modifying postgresql instance count")
+		return status.Wrap(err, status.ConfigError, "Error modifying "+stringutils.TitleReplace(dna.nodeType, "_", "-")+" instance count")
 	}
+
 	return nil
 }
 
@@ -150,22 +180,13 @@ func (dna *DeleteNodeAWSImpl) promptUserConfirmation() (bool, error) {
 	dna.writer.Println("OpenSearch => " + strings.Join(dna.AWSConfigIp.configOpensearchIpList, ", "))
 	dna.writer.Println("Postgresql => " + strings.Join(dna.AWSConfigIp.configPostgresqlIpList, ", "))
 	dna.writer.Println("")
-	dna.writer.Println("Nodes to be deleted:")
+	dna.writer.Println("Node to be deleted:")
 	dna.writer.Println("================================================")
-	if len(dna.automateIpList) > 0 {
-		dna.writer.Println("Automate => " + strings.Join(dna.automateIpList, ", "))
-	}
-	if len(dna.chefServerIpList) > 0 {
-		dna.writer.Println("Chef-Server => " + strings.Join(dna.chefServerIpList, ", "))
-	}
-	if len(dna.opensearchIpList) > 0 {
-		dna.writer.Println("OpenSearch => " + strings.Join(dna.opensearchIpList, ", "))
-	}
-	if len(dna.postgresqlIpList) > 0 {
-		dna.writer.Println("Postgresql => " + strings.Join(dna.postgresqlIpList, ", "))
-	}
-	dna.writer.Println("Removal of nodes for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch nodes.")
-	return dna.writer.Confirm("This will delete the above nodes from your existing setup. It might take a while. Are you sure you want to continue?")
+
+	dna.writer.Println(fmt.Sprintf("%s => %s", stringutils.TitleReplace(dna.nodeType, "_", "-"), dna.ipToDelete))
+
+	dna.writer.Println("Removal of node for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch node.")
+	return dna.writer.Confirm("This will delete the above node from your existing setup. It might take a while. Are you sure you want to continue?")
 }
 
 func (dna *DeleteNodeAWSImpl) runDeploy() error {
@@ -198,34 +219,38 @@ func (dna *DeleteNodeAWSImpl) runDeploy() error {
 }
 
 func (dna *DeleteNodeAWSImpl) runRemoveNodeFromAws() error {
-	err := dna.removeNodeIfExists("chef_automate", dna.automateIpList, dna.configAutomateIpList)
-	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error removing automate node")
-	}
 
-	err = dna.removeNodeIfExists("chef_server", dna.chefServerIpList, dna.configChefServerIpList)
-	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error removing chef server node")
+	var instanceType string
+	var configNodeIpList []string
+	switch dna.nodeType {
+	case AUTOMATE:
+		instanceType = "chef_automate"
+		configNodeIpList = dna.configAutomateIpList
+	case CHEF_SERVER:
+		instanceType = "chef_server"
+		configNodeIpList = dna.configChefServerIpList
+	case POSTGRESQL:
+		instanceType = "chef_automate_postgresql"
+		configNodeIpList = dna.configPostgresqlIpList
+	case OPENSEARCH:
+		instanceType = "chef_automate_opensearch"
+		configNodeIpList = dna.configOpensearchIpList
+	default:
+		status.New(status.InvalidCommandArgsError, "Invalid node type")
 	}
 
-	err = dna.removeNodeIfExists("chef_automate_opensearch", dna.opensearchIpList, dna.configOpensearchIpList)
+	err := dna.removeNodeIfExists(instanceType, dna.ipToDelete, configNodeIpList)
 	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error removing opensearch node")
+		return status.Wrap(err, status.ConfigError, "Error removing "+stringutils.TitleReplace(dna.nodeType, "_", "-")+" node")
 	}
 
-	err = dna.removeNodeIfExists("chef_automate_postgresql", dna.postgresqlIpList, dna.configPostgresqlIpList)
-	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error removing postgresql node")
-	}
 	return nil
 }
 
-func (dna *DeleteNodeAWSImpl) removeNodeIfExists(nodeType string, nodeIpList, configNodeIpList []string) error {
-	if len(nodeIpList) == 1 {
-		for i := 0; i < len(configNodeIpList); i++ {
-			if configNodeIpList[i] == nodeIpList[0] {
-				return dna.removeNodeFromAws(nodeType, i, len(configNodeIpList)-1)
-			}
+func (dna *DeleteNodeAWSImpl) removeNodeIfExists(nodeType, ipToDelete string, configNodeIpList []string) error {
+	for i := 0; i < len(configNodeIpList); i++ {
+		if configNodeIpList[i] == ipToDelete {
+			return dna.removeNodeFromAws(nodeType, i, len(configNodeIpList)-1)
 		}
 	}
 	return nil
@@ -236,25 +261,47 @@ func (dna *DeleteNodeAWSImpl) validate() error {
 	if err != nil {
 		return status.Wrap(err, status.ConfigError, "Error getting AWS instance Ip")
 	}
-	dna.automateIpList, dna.chefServerIpList, dna.opensearchIpList, dna.postgresqlIpList = splitIPCSV(
+
+	automateIpList, chefServerIpList, opensearchIpList, postgresqlIpList := splitIPCSV(
 		dna.flags.automateIp,
 		dna.flags.chefServerIp,
 		dna.flags.opensearchIp,
 		dna.flags.postgresqlIp,
 	)
-	var exceptionIps []string
-	exceptionIps = append(exceptionIps, dna.automateIpList...)
-	exceptionIps = append(exceptionIps, dna.chefServerIpList...)
-	exceptionIps = append(exceptionIps, dna.opensearchIpList...)
-	exceptionIps = append(exceptionIps, dna.postgresqlIpList...)
 
-	updatedConfig, err := dna.nodeUtils.pullAndUpdateConfigAws(&dna.sshUtil, exceptionIps)
+	// Check if only one node is being deleted
+	if (len(automateIpList) + len(chefServerIpList) + len(opensearchIpList) + len(postgresqlIpList)) != 1 {
+		return status.New(status.InvalidCommandArgsError, "Only one node can be deleted at a time")
+	}
+
+	// Get the node type and ip address of the node to be deleted
+	if len(automateIpList) > 0 {
+		dna.ipToDelete = automateIpList[0]
+		dna.nodeType = AUTOMATE
+	} else if len(chefServerIpList) > 0 {
+		dna.ipToDelete = chefServerIpList[0]
+		dna.nodeType = CHEF_SERVER
+	} else if len(postgresqlIpList) > 0 {
+		dna.ipToDelete = postgresqlIpList[0]
+		dna.nodeType = POSTGRESQL
+	} else if len(opensearchIpList) > 0 {
+		dna.ipToDelete = opensearchIpList[0]
+		dna.nodeType = OPENSEARCH
+	} else {
+		return status.New(status.InvalidCommandArgsError, "Please provide service name and ip address of the node which you want to delete")
+	}
+
+	if !isValidIPFormat(dna.ipToDelete) {
+		return status.New(status.InvalidCommandArgsError, "Invalid IP address "+dna.ipToDelete)
+	}
+
+	updatedConfig, err := dna.nodeUtils.pullAndUpdateConfigAws(&dna.sshUtil, []string{dna.ipToDelete})
 	if err != nil {
 		return err
 	}
 	dna.config = *updatedConfig
 	if dna.nodeUtils.isManagedServicesOn() {
-		if len(dna.opensearchIpList) > 0 || len(dna.postgresqlIpList) > 0 {
+		if dna.nodeType == POSTGRESQL || dna.nodeType == OPENSEARCH {
 			return status.New(status.ConfigError, fmt.Sprintf(TYPE_ERROR, "remove"))
 		}
 	}
@@ -267,31 +314,41 @@ func (dna *DeleteNodeAWSImpl) validate() error {
 
 func (dna *DeleteNodeAWSImpl) validateCmdArgs() *list.List {
 	errorList := list.New()
-	services := []SvcDetails{
-		{dna.automateIpList, dna.configAutomateIpList, dna.config.Automate.Config.InstanceCount, AUTOMATE_MIN_INSTANCE_COUNT, "Automate"},
-		{dna.chefServerIpList, dna.configChefServerIpList, dna.config.ChefServer.Config.InstanceCount, CHEF_SERVER_MIN_INSTANCE_COUNT, "Chef-Server"},
-	}
-	if !dna.nodeUtils.isManagedServicesOn() {
-		services = append(services, []SvcDetails{
-			{dna.opensearchIpList, dna.configOpensearchIpList, dna.config.Opensearch.Config.InstanceCount, OPENSEARCH_MIN_INSTANCE_COUNT, "OpenSearch"},
-			{dna.postgresqlIpList, dna.configPostgresqlIpList, dna.config.Postgresql.Config.InstanceCount, POSTGRESQL_MIN_INSTANCE_COUNT, "Postgresql"},
-		}...)
-	}
-	for _, service := range services {
-		if len(service.ipList) > 1 {
-			errorList.PushBack(fmt.Sprintf("Only one %s is allowed to delete for AWS deployment type", service.name))
-		} else if len(service.ipList) == 1 {
-			allowed, finalCount, err := isFinalInstanceCountAllowed(service.instanceCount, -len(service.ipList), service.minInstanceCnt)
-			if err != nil {
-				errorList.PushBack(fmt.Sprintf("Error occurred in calculating %s final instance count", service.name))
-				continue
-			}
-			if !allowed {
-				errorList.PushBack(fmt.Sprintf("Unable to remove node. %s instance count cannot be less than %d. Final count %d not allowed.", service.name, service.minInstanceCnt, finalCount))
-			}
-			errorList.PushBackList(checkIfPresentInPrivateIPList(service.configIpList, service.ipList, service.name))
-		}
+
+	var minCount int
+	var instanceCount string
+	var configIpList []string
+
+	switch dna.nodeType {
+	case AUTOMATE:
+		instanceCount = dna.config.Automate.Config.InstanceCount
+		minCount = AUTOMATE_MIN_INSTANCE_COUNT
+		configIpList = dna.configAutomateIpList
+	case CHEF_SERVER:
+		instanceCount = dna.config.ChefServer.Config.InstanceCount
+		minCount = CHEF_SERVER_MIN_INSTANCE_COUNT
+		configIpList = dna.configChefServerIpList
+	case POSTGRESQL:
+		instanceCount = dna.config.Postgresql.Config.InstanceCount
+		minCount = POSTGRESQL_MIN_INSTANCE_COUNT
+		configIpList = dna.configPostgresqlIpList
+	case OPENSEARCH:
+		instanceCount = dna.config.Opensearch.Config.InstanceCount
+		minCount = OPENSEARCH_MIN_INSTANCE_COUNT
+		configIpList = dna.configOpensearchIpList
+	default:
+		errorList.PushBack("Invalid node type")
 	}
+
+	allowed, finalCount, err := isFinalInstanceCountAllowed(instanceCount, -1, minCount)
+	if err != nil {
+		errorList.PushBack(fmt.Sprintf("Error occurred in calculating %s final instance count", stringutils.TitleReplace(dna.nodeType, "_", "-")))
+	}
+	if !allowed {
+		errorList.PushBack(fmt.Sprintf("Unable to remove node. %s instance count cannot be less than %d. Final count %d not allowed.", stringutils.TitleReplace(dna.nodeType, "_", "-"), minCount, finalCount))
+	}
+	errorList.PushBackList(checkIfPresentInPrivateIPList(configIpList, []string{dna.ipToDelete}, stringutils.TitleReplace(dna.nodeType, "_", "-")))
+
 	return errorList
 }
 
@@ -329,3 +386,18 @@ func (dna *DeleteNodeAWSImpl) getAwsHAIp() error {
 	dna.AWSConfigIp = *ConfigIp
 	return nil
 }
+
+// Stop all services on the node to be deleted
+func (dna *DeleteNodeAWSImpl) stopNodes() error {
+
+	infra, _, err := dna.nodeUtils.getHaInfraDetails()
+	if err != nil {
+		return err
+	}
+
+	err = dna.nodeUtils.stopServicesOnNode(dna.ipToDelete, dna.nodeType, AWS_MODE, infra)
+	if err != nil {
+		return status.Wrap(err, status.CommandExecutionError, "Error stopping services on node")
+	}
+	return nil
+}
diff --git a/components/automate-cli/cmd/chef-automate/ha_node_delete_onprem.go b/components/automate-cli/cmd/chef-automate/ha_node_delete_onprem.go
index 2f980be85..50edc7878 100644
--- a/components/automate-cli/cmd/chef-automate/ha_node_delete_onprem.go
+++ b/components/automate-cli/cmd/chef-automate/ha_node_delete_onprem.go
@@ -9,6 +9,7 @@ import (
 	"github.com/chef/automate/components/automate-cli/pkg/status"
 	"github.com/chef/automate/components/automate-deployment/pkg/cli"
 	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/stringutils"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 )
@@ -20,21 +21,11 @@ const (
 	OPENSEARCH_MIN_INSTANCE_COUNT  = 3
 )
 
-type SvcDetailOnPrem struct {
-	ipList         []string
-	configKey      string
-	minCount       int
-	instanceCount  string
-	existingIplist []string
-}
-
 type DeleteNodeOnPremImpl struct {
 	config                  ExistingInfraConfigToml
 	copyConfigForUserPrompt ExistingInfraConfigToml
-	automateIpList          []string
-	chefServerIpList        []string
-	opensearchIpList        []string
-	postgresqlIp            []string
+	ipToDelete              string
+	nodeType                string
 	nodeUtils               NodeOpUtils
 	flags                   AddDeleteNodeHACmdFlags
 	configpath              string
@@ -84,8 +75,46 @@ func (dni *DeleteNodeOnPremImpl) Execute(c *cobra.Command, args []string) error
 			return nil
 		}
 	}
-	dni.prepare()
-	return dni.runDeploy()
+
+	previousCount, err := dni.nodeUtils.calculateTotalInstanceCount()
+	if err != nil {
+		return err
+	}
+
+	err = dni.prepare()
+	if err != nil {
+		return err
+	}
+
+	err = dni.runDeploy()
+	currentCount, newErr := dni.nodeUtils.calculateTotalInstanceCount()
+	if newErr != nil {
+		if err != nil {
+			return errors.Wrap(err, newErr.Error())
+		}
+		return newErr
+	}
+
+	if currentCount == previousCount-1 {
+		stopErr := dni.stopNodes()
+		if stopErr != nil {
+			if err != nil {
+				return errors.Wrap(err, stopErr.Error())
+			}
+			return stopErr
+		}
+	} else {
+		if err != nil {
+			return errors.Wrap(err, "Error in deleting node")
+		}
+		return errors.New("Error in deleting node")
+	}
+
+	if err == nil {
+		dni.writer.Println("Node is successfully removed from the cluster. Please delete the machine manually.")
+	}
+
+	return err
 }
 
 func (dni *DeleteNodeOnPremImpl) prepare() error {
@@ -93,25 +122,47 @@ func (dni *DeleteNodeOnPremImpl) prepare() error {
 }
 
 func (dni *DeleteNodeOnPremImpl) validate() error {
-	dni.automateIpList, dni.chefServerIpList, dni.opensearchIpList, dni.postgresqlIp = splitIPCSV(
+	automateIpList, chefServerIpList, opensearchIpList, postgresqlIpList := splitIPCSV(
 		dni.flags.automateIp,
 		dni.flags.chefServerIp,
 		dni.flags.opensearchIp,
 		dni.flags.postgresqlIp,
 	)
-	var exceptionIps []string
-	exceptionIps = append(exceptionIps, dni.automateIpList...)
-	exceptionIps = append(exceptionIps, dni.chefServerIpList...)
-	exceptionIps = append(exceptionIps, dni.opensearchIpList...)
-	exceptionIps = append(exceptionIps, dni.postgresqlIp...)
-	updatedConfig, err := dni.nodeUtils.pullAndUpdateConfig(&dni.sshUtil, exceptionIps)
+
+	// Check if only one node is being deleted
+	if (len(automateIpList) + len(chefServerIpList) + len(opensearchIpList) + len(postgresqlIpList)) != 1 {
+		return status.New(status.InvalidCommandArgsError, "Only one node can be deleted at a time")
+	}
+
+	// Get the node type and ip address of the node to be deleted
+	if len(automateIpList) > 0 {
+		dni.ipToDelete = automateIpList[0]
+		dni.nodeType = AUTOMATE
+	} else if len(chefServerIpList) > 0 {
+		dni.ipToDelete = chefServerIpList[0]
+		dni.nodeType = CHEF_SERVER
+	} else if len(postgresqlIpList) > 0 {
+		dni.ipToDelete = postgresqlIpList[0]
+		dni.nodeType = POSTGRESQL
+	} else if len(opensearchIpList) > 0 {
+		dni.ipToDelete = opensearchIpList[0]
+		dni.nodeType = OPENSEARCH
+	} else {
+		return status.New(status.InvalidCommandArgsError, "Please provide service name and ip address of the node which you want to delete")
+	}
+
+	if !isValidIPFormat(dni.ipToDelete) {
+		return status.New(status.InvalidCommandArgsError, "Invalid IP address "+dni.ipToDelete)
+	}
+
+	updatedConfig, err := dni.nodeUtils.pullAndUpdateConfig(&dni.sshUtil, []string{dni.ipToDelete})
 	if err != nil {
 		return err
 	}
 	dni.config = *updatedConfig
 	dni.copyConfigForUserPrompt = dni.config
 	if dni.nodeUtils.isManagedServicesOn() {
-		if len(dni.opensearchIpList) > 0 || len(dni.postgresqlIp) > 0 {
+		if dni.nodeType == POSTGRESQL || dni.nodeType == OPENSEARCH {
 			return status.New(status.ConfigError, fmt.Sprintf(TYPE_ERROR, "remove"))
 		}
 	}
@@ -121,45 +172,50 @@ func (dni *DeleteNodeOnPremImpl) validate() error {
 	}
 	return nil
 }
+
 func (dni *DeleteNodeOnPremImpl) modifyConfig() error {
-	err := modifyConfigForDeleteNode(
-		&dni.config.Automate.Config.InstanceCount,
-		&dni.config.ExistingInfra.Config.AutomatePrivateIps,
-		dni.automateIpList,
-		&dni.config.Automate.Config.CertsByIP,
-	)
-	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error modifying automate instance count")
-	}
-	err = modifyConfigForDeleteNode(
-		&dni.config.ChefServer.Config.InstanceCount,
-		&dni.config.ExistingInfra.Config.ChefServerPrivateIps,
-		dni.chefServerIpList,
-		&dni.config.ChefServer.Config.CertsByIP,
-	)
-	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error modifying chef-server instance count")
-	}
-	err = modifyConfigForDeleteNode(
-		&dni.config.Opensearch.Config.InstanceCount,
-		&dni.config.ExistingInfra.Config.OpensearchPrivateIps,
-		dni.opensearchIpList,
-		&dni.config.Opensearch.Config.CertsByIP,
-	)
-	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error modifying opensearch instance count")
+	var err error
+
+	switch dni.nodeType {
+	case AUTOMATE:
+		err = modifyConfigForDeleteNode(
+			&dni.config.Automate.Config.InstanceCount,
+			&dni.config.ExistingInfra.Config.AutomatePrivateIps,
+			[]string{dni.ipToDelete},
+			&dni.config.Automate.Config.CertsByIP,
+		)
+	case CHEF_SERVER:
+		err = modifyConfigForDeleteNode(
+			&dni.config.ChefServer.Config.InstanceCount,
+			&dni.config.ExistingInfra.Config.ChefServerPrivateIps,
+			[]string{dni.ipToDelete},
+			&dni.config.ChefServer.Config.CertsByIP,
+		)
+	case POSTGRESQL:
+		err = modifyConfigForDeleteNode(
+			&dni.config.Postgresql.Config.InstanceCount,
+			&dni.config.ExistingInfra.Config.PostgresqlPrivateIps,
+			[]string{dni.ipToDelete},
+			&dni.config.Postgresql.Config.CertsByIP,
+		)
+	case OPENSEARCH:
+		err = modifyConfigForDeleteNode(
+			&dni.config.Opensearch.Config.InstanceCount,
+			&dni.config.ExistingInfra.Config.OpensearchPrivateIps,
+			[]string{dni.ipToDelete},
+			&dni.config.Opensearch.Config.CertsByIP,
+		)
+	default:
+		return errors.New("Invalid node type")
 	}
-	err = modifyConfigForDeleteNode(
-		&dni.config.Postgresql.Config.InstanceCount,
-		&dni.config.ExistingInfra.Config.PostgresqlPrivateIps,
-		dni.postgresqlIp,
-		&dni.config.Postgresql.Config.CertsByIP,
-	)
+
 	if err != nil {
-		return status.Wrap(err, status.ConfigError, "Error modifying postgresql instance count")
+		return status.Wrap(err, status.ConfigError, "Error modifying "+stringutils.TitleReplace(dni.nodeType, "_", "-")+" instance count")
 	}
+
 	return nil
 }
+
 func (dni *DeleteNodeOnPremImpl) promptUserConfirmation() (bool, error) {
 	dni.writer.Println("Existing nodes:")
 	dni.writer.Println("================================================")
@@ -168,22 +224,13 @@ func (dni *DeleteNodeOnPremImpl) promptUserConfirmation() (bool, error) {
 	dni.writer.Println("OpenSearch => " + strings.Join(dni.copyConfigForUserPrompt.ExistingInfra.Config.OpensearchPrivateIps, ", "))
 	dni.writer.Println("Postgresql => " + strings.Join(dni.copyConfigForUserPrompt.ExistingInfra.Config.PostgresqlPrivateIps, ", "))
 	dni.writer.Println("")
-	dni.writer.Println("Nodes to be deleted:")
+	dni.writer.Println("Node to be deleted:")
 	dni.writer.Println("================================================")
-	if len(dni.automateIpList) > 0 {
-		dni.writer.Println("Automate => " + strings.Join(dni.automateIpList, ", "))
-	}
-	if len(dni.chefServerIpList) > 0 {
-		dni.writer.Println("Chef-Server => " + strings.Join(dni.chefServerIpList, ", "))
-	}
-	if len(dni.opensearchIpList) > 0 {
-		dni.writer.Println("OpenSearch => " + strings.Join(dni.opensearchIpList, ", "))
-	}
-	if len(dni.postgresqlIp) > 0 {
-		dni.writer.Println("Postgresql => " + strings.Join(dni.postgresqlIp, ", "))
-	}
-	dni.writer.Println("Removal of nodes for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch nodes.")
-	return dni.writer.Confirm("This will delete the above nodes from your existing setup. It might take a while. Are you sure you want to continue?")
+
+	dni.writer.Println(fmt.Sprintf("%s => %s", stringutils.TitleReplace(dni.nodeType, "_", "-"), dni.ipToDelete))
+
+	dni.writer.Println("Removal of node for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch node.")
+	return dni.writer.Confirm("This will delete the above node from your existing setup. It might take a while. Are you sure you want to continue?")
 }
 
 func (dni *DeleteNodeOnPremImpl) runDeploy() error {
@@ -197,29 +244,56 @@ func (dni *DeleteNodeOnPremImpl) runDeploy() error {
 
 func (dni *DeleteNodeOnPremImpl) validateCmdArgs() *list.List {
 	errorList := list.New()
-	var validations = []SvcDetailOnPrem{
-		{dni.automateIpList, "Automate", AUTOMATE_MIN_INSTANCE_COUNT, dni.config.Automate.Config.InstanceCount, dni.config.ExistingInfra.Config.AutomatePrivateIps},
-		{dni.chefServerIpList, "Chef-Server", CHEF_SERVER_MIN_INSTANCE_COUNT, dni.config.ChefServer.Config.InstanceCount, dni.config.ExistingInfra.Config.ChefServerPrivateIps},
-	}
-	if !dni.nodeUtils.isManagedServicesOn() {
-		validations = append(validations, []SvcDetailOnPrem{
-			{dni.opensearchIpList, "OpenSearch", OPENSEARCH_MIN_INSTANCE_COUNT, dni.config.Opensearch.Config.InstanceCount, dni.config.ExistingInfra.Config.OpensearchPrivateIps},
-			{dni.postgresqlIp, "Postgresql", POSTGRESQL_MIN_INSTANCE_COUNT, dni.config.Postgresql.Config.InstanceCount, dni.config.ExistingInfra.Config.PostgresqlPrivateIps},
-		}...)
-	}
-	for _, v := range validations {
-		if len(v.ipList) == 0 {
-			continue
-		}
-		allowed, finalCount, err := isFinalInstanceCountAllowed(v.instanceCount, -len(v.ipList), v.minCount)
-		if err != nil {
-			errorList.PushBack("Error occurred in calculating " + v.configKey + " final instance count")
-		}
-		if !allowed {
-			errorList.PushBack(fmt.Sprintf("Unable to remove node. %s instance count cannot be less than %d. Final count %d not allowed.", v.configKey, v.minCount, finalCount))
-			continue
-		}
-		errorList.PushBackList(checkIfPresentInPrivateIPList(v.existingIplist, v.ipList, v.configKey))
+
+	var minCount int
+	var instanceCount string
+	var existingIplist []string
+
+	switch dni.nodeType {
+	case AUTOMATE:
+		instanceCount = dni.config.Automate.Config.InstanceCount
+		minCount = AUTOMATE_MIN_INSTANCE_COUNT
+		existingIplist = dni.config.ExistingInfra.Config.AutomatePrivateIps
+	case CHEF_SERVER:
+		instanceCount = dni.config.ChefServer.Config.InstanceCount
+		minCount = CHEF_SERVER_MIN_INSTANCE_COUNT
+		existingIplist = dni.config.ExistingInfra.Config.ChefServerPrivateIps
+	case POSTGRESQL:
+		instanceCount = dni.config.Postgresql.Config.InstanceCount
+		minCount = POSTGRESQL_MIN_INSTANCE_COUNT
+		existingIplist = dni.config.ExistingInfra.Config.PostgresqlPrivateIps
+	case OPENSEARCH:
+		instanceCount = dni.config.Opensearch.Config.InstanceCount
+		minCount = OPENSEARCH_MIN_INSTANCE_COUNT
+		existingIplist = dni.config.ExistingInfra.Config.OpensearchPrivateIps
+	default:
+		errorList.PushBack("Invalid node type")
+	}
+
+	allowed, finalCount, err := isFinalInstanceCountAllowed(instanceCount, -1, minCount)
+	if err != nil {
+		errorList.PushBack("Error occurred in calculating " + stringutils.TitleReplace(dni.nodeType, "_", "-") + " final instance count")
 	}
+	if !allowed {
+		errorList.PushBack(fmt.Sprintf("Unable to remove node. %s instance count cannot be less than %d. Final count %d not allowed.", stringutils.TitleReplace(dni.nodeType, "_", "-"), minCount, finalCount))
+		return errorList
+	}
+	errorList.PushBackList(checkIfPresentInPrivateIPList(existingIplist, []string{dni.ipToDelete}, stringutils.TitleReplace(dni.nodeType, "_", "-")))
+
 	return errorList
 }
+
+// Stop all services on the node to be deleted
+func (dni *DeleteNodeOnPremImpl) stopNodes() error {
+
+	infra, _, err := dni.nodeUtils.getHaInfraDetails()
+	if err != nil {
+		return err
+	}
+
+	err = dni.nodeUtils.stopServicesOnNode(dni.ipToDelete, dni.nodeType, EXISTING_INFRA_MODE, infra)
+	if err != nil {
+		return status.Wrap(err, status.CommandExecutionError, "Error stopping services on node")
+	}
+	return nil
+}
diff --git a/components/automate-cli/cmd/chef-automate/ha_node_operations_utils.go b/components/automate-cli/cmd/chef-automate/ha_node_operations_utils.go
index 39dbfe038..08829604e 100644
--- a/components/automate-cli/cmd/chef-automate/ha_node_operations_utils.go
+++ b/components/automate-cli/cmd/chef-automate/ha_node_operations_utils.go
@@ -5,11 +5,14 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strconv"
 	"strings"
 
 	"github.com/chef/automate/components/automate-cli/pkg/status"
+	"github.com/chef/automate/components/automate-deployment/pkg/cli"
 	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/platform/command"
 	"github.com/chef/automate/lib/stringutils"
 	ptoml "github.com/pelletier/go-toml"
 	"github.com/pkg/errors"
@@ -56,6 +59,10 @@ type MockNodeUtilsImpl struct {
 	moveAWSAutoTfvarsFileFunc                 func(path string) error
 	modifyTfArchFileFunc                      func(path string) error
 	getAWSConfigIpFunc                        func() (*AWSConfigIp, error)
+	stopServicesOnNodeFunc                    func(ip, nodeType, deploymentType string, infra *AutomateHAInfraDetails) error
+	excludeOpenSearchNodeFunc                 func(ipToDelete string, infra *AutomateHAInfraDetails) error
+	checkExistingExcludedOSNodesFunc          func(automateIp string, infra *AutomateHAInfraDetails) (string, error)
+	calculateTotalInstanceCountFunc           func() (int, error)
 }
 
 func (mnu *MockNodeUtilsImpl) executeAutomateClusterCtlCommandAsync(command string, args []string, helpDocs string) error {
@@ -114,6 +121,18 @@ func (mnu *MockNodeUtilsImpl) modifyTfArchFile(path string) error {
 func (mnu *MockNodeUtilsImpl) pullAndUpdateConfigAws(sshUtil *SSHUtil, exceptionIps []string) (*AwsConfigToml, error) {
 	return mnu.pullAndUpdateConfigAwsFunc(sshUtil, exceptionIps)
 }
+func (mnu *MockNodeUtilsImpl) stopServicesOnNode(ip, nodeType, deploymentType string, infra *AutomateHAInfraDetails) error {
+	return mnu.stopServicesOnNodeFunc(ip, nodeType, deploymentType, infra)
+}
+func (mnu *MockNodeUtilsImpl) excludeOpenSearchNode(ipToDelete string, infra *AutomateHAInfraDetails) error {
+	return mnu.excludeOpenSearchNodeFunc(ipToDelete, infra)
+}
+func (mnu *MockNodeUtilsImpl) checkExistingExcludedOSNodes(automateIp string, infra *AutomateHAInfraDetails) (string, error) {
+	return mnu.checkExistingExcludedOSNodesFunc(automateIp, infra)
+}
+func (mnu *MockNodeUtilsImpl) calculateTotalInstanceCount() (int, error) {
+	return mnu.calculateTotalInstanceCountFunc()
+}
 
 type NodeOpUtils interface {
 	executeAutomateClusterCtlCommandAsync(command string, args []string, helpDocs string) error
@@ -134,12 +153,24 @@ type NodeOpUtils interface {
 	moveAWSAutoTfvarsFile(string) error
 	modifyTfArchFile(string) error
 	getAWSConfigIp() (*AWSConfigIp, error)
+	stopServicesOnNode(ip, nodeType, deploymentType string, infra *AutomateHAInfraDetails) error
+	excludeOpenSearchNode(ipToDelete string, infra *AutomateHAInfraDetails) error
+	checkExistingExcludedOSNodes(automateIp string, infra *AutomateHAInfraDetails) (string, error)
+	calculateTotalInstanceCount() (int, error)
 }
 
-type NodeUtilsImpl struct{}
+type NodeUtilsImpl struct {
+	cmdUtil RemoteCmdExecutor
+	exec    command.Executor
+	writer  *cli.Writer
+}
 
-func NewNodeUtils() NodeOpUtils {
-	return &NodeUtilsImpl{}
+func NewNodeUtils(cmdUtil RemoteCmdExecutor, exec command.Executor, writer *cli.Writer) NodeOpUtils {
+	return &NodeUtilsImpl{
+		cmdUtil: cmdUtil,
+		exec:    exec,
+		writer:  writer,
+	}
 }
 
 func (nu *NodeUtilsImpl) getAWSConfigIp() (*AWSConfigIp, error) {
@@ -226,9 +257,6 @@ func (nu *NodeUtilsImpl) taintTerraform(path string) error {
 	return executeShellCommand("/bin/sh", []string{"-c", TAINT_TERRAFORM}, path)
 }
 
-func (nu *NodeUtilsImpl) readConfig(path string) (ExistingInfraConfigToml, error) {
-	return readConfig(path)
-}
 func (nu *NodeUtilsImpl) executeAutomateClusterCtlCommandAsync(command string, args []string, helpDocs string) error {
 	return executeAutomateClusterCtlCommandAsync(command, args, helpDocs, true)
 }
@@ -321,6 +349,174 @@ func (nu *NodeUtilsImpl) executeShellCommand(command, path string) error {
 	}, path)
 }
 
+// Stop services on node
+func (nu *NodeUtilsImpl) stopServicesOnNode(ip, nodeType, deploymentType string, infra *AutomateHAInfraDetails) error {
+
+	if nodeType == OPENSEARCH {
+		err := nu.excludeOpenSearchNode(ip, infra)
+		if err != nil {
+			return err
+		}
+		nu.writer.Println(fmt.Sprintf("OpenSearch node %s excluded from the cluster.", ip))
+	}
+
+	// If deployment type is AWS, then we don't need to stop services on node
+	if deploymentType == AWS_MODE {
+		return nil
+	}
+
+	var cmd string
+	switch nodeType {
+	case AUTOMATE:
+		cmd = STOP_FE_SERVICES_CMD
+	case CHEF_SERVER:
+		cmd = STOP_FE_SERVICES_CMD
+	case POSTGRESQL:
+		cmd = STOP_BE_SERVICES_CMD
+	case OPENSEARCH:
+		cmd = STOP_BE_SERVICES_CMD
+	default:
+		return errors.New("Invalid node type")
+	}
+
+	sshUtil := nu.cmdUtil.GetSshUtil()
+
+	sshUtil.setSSHConfig(&SSHConfig{
+		sshUser:    infra.Outputs.SSHUser.Value,
+		sshPort:    infra.Outputs.SSHPort.Value,
+		sshKeyFile: infra.Outputs.SSHKeyFile.Value,
+		hostIP:     ip,
+	})
+
+	_, err := sshUtil.connectAndExecuteCommandOnRemote(cmd, false)
+	if err != nil {
+		return err
+	}
+
+	nu.writer.Println(fmt.Sprintf("Services stopped on the node %s.", ip))
+
+	return nil
+}
+
+func (nu *NodeUtilsImpl) excludeOpenSearchNode(ipToDelete string, infra *AutomateHAInfraDetails) error {
+
+	automateIp := infra.Outputs.AutomatePrivateIps.Value[0]
+
+	existingIps, err := nu.checkExistingExcludedOSNodes(automateIp, infra)
+	if err != nil {
+		return err
+	}
+
+	if strings.Contains(existingIps, ipToDelete) {
+		return errors.New("Node is already excluded from the cluster")
+	}
+
+	if existingIps != "" {
+		ipToDelete = existingIps + "," + ipToDelete
+	}
+
+	nodeMap := &NodeTypeAndCmd{
+		Frontend:   &Cmd{CmdInputs: &CmdInputs{NodeType: false}},
+		Automate:   createCmdInputs(automateIp, fmt.Sprintf(EXCLUDE_OPENSEARCH_NODE_REQUEST, ipToDelete)),
+		ChefServer: &Cmd{CmdInputs: &CmdInputs{NodeType: false}},
+		Postgresql: &Cmd{CmdInputs: &CmdInputs{NodeType: false}},
+		Opensearch: &Cmd{CmdInputs: &CmdInputs{NodeType: false}},
+		Infra:      infra,
+	}
+
+	_, err = nu.cmdUtil.ExecuteWithNodeMap(nodeMap)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (nu *NodeUtilsImpl) checkExistingExcludedOSNodes(automateIp string, infra *AutomateHAInfraDetails) (string, error) {
+	nodeMap := &NodeTypeAndCmd{
+		Frontend: &Cmd{CmdInputs: &CmdInputs{NodeType: false}},
+		Automate: &Cmd{CmdInputs: &CmdInputs{
+			Cmd:                      GET_OPENSEARCH_CLUSTER_SETTINGS,
+			NodeIps:                  []string{automateIp},
+			NodeType:                 true,
+			SkipPrintOutput:          true,
+			HideSSHConnectionMessage: true}},
+		ChefServer: &Cmd{CmdInputs: &CmdInputs{NodeType: false}},
+		Postgresql: &Cmd{CmdInputs: &CmdInputs{NodeType: false}},
+		Opensearch: &Cmd{CmdInputs: &CmdInputs{NodeType: false}},
+		Infra:      infra,
+	}
+
+	out, err := nu.cmdUtil.ExecuteWithNodeMap(nodeMap)
+	if err != nil {
+		return "", err
+	}
+
+	var res *CmdResult
+	val, ok := out[automateIp]
+	if !ok {
+		return "", errors.New("Failed to get the response from the node")
+	}
+	if len(val) > 0 {
+		res = val[0]
+	}
+
+	if res.Error != nil {
+		return "", res.Error
+	}
+
+	return getIPsFromOSClusterResponse(res.Output), nil
+}
+
+// Calculate total instance count
+func (nu *NodeUtilsImpl) calculateTotalInstanceCount() (int, error) {
+	cmd := `
+        a2csCount=$(terraform state list -state=/hab/a2_deploy_workspace/terraform/terraform.tfstate | grep -E "module.automate|module.bootstrap_automate|module.chef_server" | wc -l)
+        pgosCount=$(terraform state list -state=/hab/a2_deploy_workspace/terraform/terraform.tfstate | grep -E "module.opensearch|module.postgresql" | wc -l)
+        echo $(($a2csCount/2 + $pgosCount))
+    `
+
+	out, err := nu.exec.CombinedOutput("/bin/sh", command.Args("-c", cmd))
+	if err != nil {
+		return -1, err
+	}
+
+	count, err := strconv.Atoi(strings.TrimSuffix(out, "\n"))
+	if err != nil {
+		return -1, err
+	}
+
+	return count, nil
+}
+
+func getIPsFromOSClusterResponse(output string) string {
+	// Regular expression pattern to match the IP addresses
+	pattern := `"_ip":"((?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:,\s*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})*)"`
+
+	// Compile the regular expression pattern
+	reg := regexp.MustCompile(pattern)
+
+	// Find all matches of the pattern in the string
+	matches := reg.FindAllStringSubmatch(output, -1)
+
+	if len(matches) > 0 {
+		// Extract the IP addresses from the first match
+		return strings.TrimSpace(matches[0][1])
+	}
+	return ""
+}
+
+func createCmdInputs(ip string, cmd string) *Cmd {
+	return &Cmd{
+		CmdInputs: &CmdInputs{
+			Cmd:                      cmd,
+			NodeIps:                  []string{ip},
+			NodeType:                 true,
+			SkipPrintOutput:          true,
+			HideSSHConnectionMessage: true,
+		},
+	}
+}
+
 func trimSliceSpace(slc []string) []string {
 	for i := range slc {
 		slc[i] = strings.TrimSpace(slc[i])
diff --git a/components/automate-cluster-ctl/templates/terraform.tfvars.erb b/components/automate-cluster-ctl/templates/terraform.tfvars.erb
index 41d813b5d..5e7c244f3 100644
--- a/components/automate-cluster-ctl/templates/terraform.tfvars.erb
+++ b/components/automate-cluster-ctl/templates/terraform.tfvars.erb
@@ -93,7 +93,9 @@ chef_ebs_volume_size = "<%= config.aws.chef_ebs_volume_size %>"
 chef_ebs_volume_type = "<%= config.aws.chef_ebs_volume_type %>"
 <% end %>
 
+<% if config.aws.opensearch_server_instance_type %>
 opensearch_server_instance_type = "<%= config.aws.opensearch_server_instance_type %>"
+<% end %>
 
 <% if config.aws.opensearch_ebs_volume_iops %>
 opensearch_ebs_volume_iops = "<%= config.aws.opensearch_ebs_volume_iops %>"
@@ -107,7 +109,9 @@ opensearch_ebs_volume_size = "<%= config.aws.opensearch_ebs_volume_size %>"
 opensearch_ebs_volume_type = "<%= config.aws.opensearch_ebs_volume_type %>"
 <% end %>
 
+<% if config.aws.postgresql_server_instance_type %>
 postgresql_server_instance_type = "<%= config.aws.postgresql_server_instance_type %>"
+<% end %>
 
 <% if config.aws.postgresql_ebs_volume_iops %>
 postgresql_ebs_volume_iops = "<%= config.aws.postgresql_ebs_volume_iops %>"
diff --git a/components/docs-chef-io/content/automate/backup.md b/components/docs-chef-io/content/automate/backup.md
index 7330e481a..adf34f6d4 100644
--- a/components/docs-chef-io/content/automate/backup.md
+++ b/components/docs-chef-io/content/automate/backup.md
@@ -91,7 +91,7 @@ To store backups in an existing AWS S3 bucket, use the supported S3-related sett
   # AWS environment variables or through the shared AWS config files.
   access_key = "<access_key>"
   secret_key = "<secret_key>"
-  session_key = "<session_key>"
+  session_token = "<session_token>"
 
 [global.v1.backups.s3.ssl]
   # root_cert (optional): The root certificate used for SSL validation.
diff --git a/components/docs-chef-io/content/automate/ha_automate_to_automate_ha.md b/components/docs-chef-io/content/automate/ha_automate_to_automate_ha.md
index 595b25981..775f35e6a 100644
--- a/components/docs-chef-io/content/automate/ha_automate_to_automate_ha.md
+++ b/components/docs-chef-io/content/automate/ha_automate_to_automate_ha.md
@@ -273,7 +273,7 @@ For AWS managed services, map the snapshot role to the OpenSearch dashboard. It
     # AWS environment variables or through the shared AWS config files.
     access_key = "<access_key>"
     secret_key = "<secret_key>"
-    session_key = "<session_key>"
+    session_token = "<session_token>"
     [global.v1.backups.s3.ssl]
     # root_cert (optional): The root certificate used for SSL validation.
     # For S3 compatible APIs, you can set the SSL root cert if needed
diff --git a/components/docs-chef-io/content/automate/ha_aws_deploy_steps.md b/components/docs-chef-io/content/automate/ha_aws_deploy_steps.md
index 21f9a00dc..bb6814773 100644
--- a/components/docs-chef-io/content/automate/ha_aws_deploy_steps.md
+++ b/components/docs-chef-io/content/automate/ha_aws_deploy_steps.md
@@ -105,7 +105,7 @@ Run the following steps on Bastion Host Machine:
    - Give `ssh_key_file` path, this should have been download from AWS SSH Key Pair which we want to use to create all the VM's. Thus, we will be able to access all VM's using this.
    - We support only private key authentication.
    - Set `backup_config` to `"efs"` or `"s3"`
-   - If `backup_config` is `s3` then set `s3_bucketName` to a Unique Value.
+   - If `backup_config` is `s3` then uncomment and set the value for following `s3_bucketName` attribute to your bucket name. If the bucket name does not exist, it will be created for you automatically.
    - Set `admin_password` which you can use to access Chef Automate UI for user `admin`.
    - If you don't have a custom FQDN leave `fqdn` as empty for this AWS deployment. By default, AWS Application load balancer will be used as `fqdn`.
    - Set `instance_count` for Chef Automate, Chef Infra Server, Postgresql, OpenSearch.
@@ -113,7 +113,7 @@ Run the following steps on Bastion Host Machine:
      - Set `profile`, by default `profile` is `"default"`
      - Set `region`, by default `region` is `"us-east-1"`
      - Set `aws_vpc_id`, which you had created as Prerequisite step. Example: `"vpc12318h"`
-     - Set `private_custom_subnets` and `public_custom_subnets`: example : `["subnet-07e469d218301533","subnet-07e469d218041534","subnet-07e469d283041535"]`
+     - Set `private_custom_subnets` to the 3 private subnets we created in prerequisites step and set `public_custom_subnets` to the 3 public subnets we created in prerequisites step: example : `["subnet-07e469d218301533","subnet-07e469d218041534","subnet-07e469d283041535"]`
      - Set `ssh_key_pair_name`, this is the SSH Key Pair we created as Prerequisite. This value should be just name of the AWS SSH Key Pair, not having `.pem` extention. The ssh key content should be same as content of `ssh_key_file`.
      - Set `setup_managed_services` as `false`, As these deployment steps are for Non-Managed Services AWS Deployment. Default value is `false`.
      - Set `ami_id`, this value depends on your AWS Region and the Operating System Image you want to use.
@@ -387,7 +387,7 @@ For example, if you have patched any external configurations like SAML or LDAP,
   Downgrading the number of instance_count for the backend nodes will result in data loss. We do not recommend downgrading the backend nodes.
 {{< /warning >}}
 
-## Delete single node In AWS Deployment post deployment
+## Remove Single Node From Cluster on AWS Deployment
 
 {{< warning >}}
 
@@ -399,10 +399,10 @@ For example, if you have patched any external configurations like SAML or LDAP,
 
 {{< /warning >}}
 
-The commands require some arguments so that it can determine which types of nodes you want to remove to your HA setup from your bastion host. It needs the ip address of the node you want to remove as as argument when you run the command.
+The command requires some arguments so that it can determine which types of nodes you want to remove from your HA setup from your bastion host. It needs the IP address of the node you want to remove as an argument when you run the command.
 For example,
 
-- if you want to remove node of automate, you have to run the:
+- If you want to remove node of automate, you have to run the:
 
     ```sh
     chef-automate node remove --automate-ip "<automate-ip-address>"
@@ -426,7 +426,7 @@ For example,
     chef-automate node remove --postgresql-ip "<postgresql-ip-address>"
     ```
 
-Once the command executes, it will remove nodes to your HA setup
+Once the command executes, it will remove the supplied node from your HA setup. The changes might take a while.
 
 ## Uninstall chef automate HA
 
diff --git a/components/docs-chef-io/content/automate/ha_aws_managed_deploy_steps.md b/components/docs-chef-io/content/automate/ha_aws_managed_deploy_steps.md
index 233093a82..f08cb27a4 100644
--- a/components/docs-chef-io/content/automate/ha_aws_managed_deploy_steps.md
+++ b/components/docs-chef-io/content/automate/ha_aws_managed_deploy_steps.md
@@ -107,7 +107,8 @@ Set the above prerequisites in `~/.aws/credentials` in Bastion Host:
    - Give `ssh_port` if your AMI runs on custom **ssh port**. The default value is 22.
    - Give `ssh_key_file` path, downloaded from **AWS SSH Key Pair**, which you want to use to create all the VMs. This will let you access all the VMs.
    - We support only private key authentication.
-   - Set `backup_config` to `"s3"`. If `backup_config` is `s3`, set `s3_bucketName`.
+   - Set `backup_config` to `"s3"` or `efs`.
+   - If `backup_config` is `s3`, uncomment and set the value for following `s3_bucketName` attribute to your bucket name. If the bucket name does not exist, it will be created for you automatically.
    - Set `admin_password` to access Chef Automate UI for user `admin`.
    - Don't set `fqdn` for the AWS deployment.
    - Set `instance_count` for *Chef Automate*, *Chef Infra Server*, *Postgresql*, *OpenSearch*.
@@ -115,7 +116,7 @@ Set the above prerequisites in `~/.aws/credentials` in Bastion Host:
       - Set `profile`. The default value of `profile` is `"default"`.
       - Set `region`. The  default value of `region` is `"us-east-1"`.
       - Set `aws_vpc_id`, created in the Prerequisites step. Example: `"vpc12318h"`.
-      - If AWS VPC uses Subnet, set `private_custom_subnets` and `public_custom_subnets`. For example : `["subnet-07e469d218301533","subnet-07e469d218041534","subnet-07e469d283041535"]`.
+      - If AWS VPC uses Subnet, set `private_custom_subnets` to the 3 private subnets we created in prerequisites step and `public_custom_subnets` to the 3 public subnets we created in prerequisites step. For example : `["subnet-07e469d218301533","subnet-07e469d218041534","subnet-07e469d283041535"]`.
       - Set `ssh_key_pair_name`, an SSH Key Pair created as a Prerequisite. The pair value should be the name of the AWS SSH Key Pair without a `.pem` extension. The ssh key content should be the same as the content of `ssh_key_file`.
       - Set `setup_managed_services` as `true`, As these deployment steps are for Managed Services AWS Deployment. The default value is `false`, which should be changed.
         - Set `managed_opensearch_domain_name`, `managed_opensearch_domain_url`, `managed_opensearch_username`, `managed_opensearch_user_password` from the **Managed AWS OpenSearch** created in the Prerequisite steps.
diff --git a/components/docs-chef-io/content/automate/ha_onprim_deployment_procedure.md b/components/docs-chef-io/content/automate/ha_onprim_deployment_procedure.md
index 8a2289e1d..6ae4b7a5c 100644
--- a/components/docs-chef-io/content/automate/ha_onprim_deployment_procedure.md
+++ b/components/docs-chef-io/content/automate/ha_onprim_deployment_procedure.md
@@ -67,8 +67,8 @@ sudo sed -i 's/SELINUX=enforcing/SELINUX=permissive/g' /etc/selinux/config
     #Run commands as sudo.
     sudo -- sh -c "
     #Download Chef Automate CLI.
-    curl https://packages.chef.io/files/current/latest/chef-automate-cli/chef-automate_linux_amd64.zip
-    | gunzip - > chef-automate && chmod +x chef-automate
+    curl https://packages.chef.io/files/current/latest/chef-automate-cli/chef-automate_linux_amd64.zip \
+    | gunzip - > chef-automate && chmod +x chef-automate \
     | cp -f chef-automate /usr/bin/chef-automate
 
     #Download the latest Airgapped Bundle.
@@ -115,7 +115,7 @@ sudo sed -i 's/SELINUX=enforcing/SELINUX=permissive/g' /etc/selinux/config
    - Give the `ssh_key_file` path; this key should have access to all the Machines or VMs.
    - We support only private key authentication.
    - Provide `backup_config` based on the type of backup storage you have. This field can be optionally left empty during deployment and can be patched at later point. Allowed values are `object_storage` and `file_system`.
-   - If `backup_config` is `object_storage`, make sure to fill values under `[object_storage.config]`
+   - If `backup_config` is `object_storage`, make sure to fill values under `[object_storage.config]`. User must create the bucket themselves and make sure to assign correct [IAM policy for bucket access](/automate/backup/#aws-s3-permissions) if you are using AWS s3.
    - Give `fqdn` as the DNS entry of Chef Automate, which LoadBalancer redirects to Chef Automate Machines or VM's. Example: `chefautomate.example.com`
    - Set the `admin_password` to what you want to use to login to Chef Automate, when you open up `chefautomate.example.com` in the Browser, for the username `admin`.
 
@@ -503,7 +503,7 @@ For example, if you have patched any external configurations like SAML or LDAP,
 It's essential to ensure that the IP address of the nodes you are trying to add has sufficient resources and is reachable from the bastion host.
 {{< /warning >}}
 
-## Remove Any Nodes From Frontend Cluster OnPrem Deployment
+## Remove Single Node From Cluster on OnPrem Deployment
 
 {{< warning >}}
 
@@ -513,51 +513,39 @@ It's essential to ensure that the IP address of the nodes you are trying to add
 
 - Below process can be done for `chef-server` and `automate`.
 
+- Only one node can be removed at a time irrespective of node type.
+
 {{< /warning >}}
 
-The commands require some arguments so that it can determine which types of nodes you want to remove from your HA setup from your bastion host. It needs the IP addresses of the nodes you want to remove as comma-separate values with no spaces in between.
+The command requires some arguments so that it can determine which types of nodes you want to remove from your HA setup from your bastion host. It needs the IP address of the node you want to remove.
 
 For example,
 
-- if you want to remove nodes with IP 10.1.2.23 to automate, you have to run the:
-
-    ```sh
-    chef-automate node remove --automate-ip 10.1.2.23
-    ```
-
-- If you want to remove nodes with IP 10.1.2.23 and 10.0.1.42 to chef-server you have to run the:
+- If you want to remove node of automate, you have to run the:
 
     ```sh
-    chef-automate node remove --chef-server-ip 10.1.2.23,10.0.1.42
+    chef-automate node remove --automate-ip "<automate-ip-address>"
     ```
 
-- If you want to remove nodes with IP 10.1.2.23 and 10.0.1.42 to OpenSearch, you have to run:
+- If you want to remove node of chef-server, you have to run the:
 
     ```sh
-    chef-automate node remove --opensearch-ip 10.1.2.23,10.0.1.42
+    chef-automate node remove --chef-server-ip "<chef-server-ip-address>"
     ```
 
-  - If you want to remove nodes with IP 10.1.2.23, 10.0.1.54 and 10.0.1.42 to PostgreSQL you have to run:
-
-    ```sh
-    chef-automate node remove --postgresql-ip 10.1.2.23,10.0.1.42,10.0.1.54
-    ```
-
-You can mix and match different services to remove nodes across various services.
-
-- If you want to remove nodes with IP 10.1.2.23 to automate and nodes with IP 10.0.1.54 and 10.0.1.42 to PostgreSQL, you have to run:
+- If you want to remove node of OpenSearch, you have to run the:
 
     ```sh
-    chef-automate node remove --automate-ip 10.1.2.23 --postgresql-ip 10.0.1.42,10.0.1.54
+    chef-automate node remove --opensearch-ip "<opensearch-ip-address>"
     ```
 
-- If you want to remove nodes with IP 10.1.2.23 to automate, nodes with IP 10.1.0.36 and 10.0.1.233 to chef-server, and nodes with IP 10.0.1.54 and 10.0.1.42 to PostgreSQL you have to run:
+- If you want to remove node of PostgreSQL you have to run:
 
     ```sh
-    chef-automate node remove --automate-ip 10.1.2.23 --chef-server-ip 10.1.0.36,10.0.1.233  --postgresql-ip 10.0.1.42,10.0.1.54
+    chef-automate node remove --postgresql-ip "<postgresql-ip-address>"
     ```
 
-Once the command executes, it will remove the supplied nodes from your automate setup. The changes might take a while.
+Once the command executes, it will remove the supplied node from your HA setup. The changes might take a while.
 
 - Make sure to remove the ip address of the deleted node from your loadbalancer configuration. For reference check [Load Balancer Configuration page](/automate/loadbalancer_configuration/)
 
@@ -575,7 +563,7 @@ Once the command executes, it will remove the supplied nodes from your automate
 - First Add a New Node follow [this](#add-more-nodes-to-the-onprem-deployment).
 - Stop the Habitat Supervisior on the node .i.e going to be removed, use `systemctl stop hab-sup` command to stop the
   habitat supervisior.
-- Remove a Existing Node follow [this](#remove-any-nodes-from-frontend-cluster-onprem-deployment).
+- Remove a Existing Node follow [this](#remove-single-node-from-cluster-on-onprem-deployment).
 
 ## Uninstall chef automate HA
 
diff --git a/components/docs-chef-io/content/automate/managed_services.md b/components/docs-chef-io/content/automate/managed_services.md
index 68feeadc4..2284ca2f8 100644
--- a/components/docs-chef-io/content/automate/managed_services.md
+++ b/components/docs-chef-io/content/automate/managed_services.md
@@ -561,8 +561,8 @@ To store backups in an existing AWS S3 bucket, add the following to your `config
   # AWS environment variables or through the shared AWS config files.
   access_key = "<access_key>"
   secret_key = "<secret_key>"
-  # session_key (optional)
-  session_key = "<session_key>"
+  # session_token (optional)
+  session_token = "<session_token>"
 
 [global.v1.backups.s3.ssl]
   # root_cert (optional): The root certificate used for SSL validation.
diff --git a/lib/platform/command/mock.go b/lib/platform/command/mock.go
index bc0bef974..8ba25ea45 100644
--- a/lib/platform/command/mock.go
+++ b/lib/platform/command/mock.go
@@ -49,6 +49,29 @@ type ExpectedCommand struct {
 	Stdout           io.Writer
 }
 
+type MockExecutorImpl struct {
+	RunFunc            func(string, ...Opt) error
+	StartFunc          func(string, ...Opt) (WaitFunc, error)
+	OutputFunc         func(string, ...Opt) (string, error)
+	CombinedOutputFunc func(string, ...Opt) (string, error)
+}
+
+func (m *MockExecutorImpl) Run(cmd string, opts ...Opt) error {
+	return m.RunFunc(cmd, opts...)
+}
+
+func (m *MockExecutorImpl) Start(cmd string, opts ...Opt) (WaitFunc, error) {
+	return m.StartFunc(cmd, opts...)
+}
+
+func (m *MockExecutorImpl) Output(cmd string, opts ...Opt) (string, error) {
+	return m.OutputFunc(cmd, opts...)
+}
+
+func (m *MockExecutorImpl) CombinedOutput(cmd string, opts ...Opt) (string, error) {
+	return m.CombinedOutputFunc(cmd, opts...)
+}
+
 // Return sets the return value for an expectation
 func (e *Expectation) Return(vals ...interface{}) *Expectation {
 	e.retVals = vals
diff --git a/lib/stringutils/strings.go b/lib/stringutils/strings.go
index d56374cc3..4a19ce794 100644
--- a/lib/stringutils/strings.go
+++ b/lib/stringutils/strings.go
@@ -14,11 +14,15 @@ func Title(s string) string {
 
 // TitleSplit splits the string s by sep and returns a copy of the string s with all Unicode letters that begin words mapped to their title case.
 func TitleSplit(s string, sep string) string {
+	return TitleReplace(s, sep, " ")
+}
+
+func TitleReplace(s string, sep string, rep string) string {
 	split := strings.Split(s, sep)
 	for i, v := range split {
 		split[i] = Title(v)
 	}
-	return strings.Join(split, " ")
+	return strings.Join(split, rep)
 }
 
 func IsNumeric(word string) bool {


Test patch:
diff --git a/components/automate-cli/cmd/chef-automate/certRotate_test.go b/components/automate-cli/cmd/chef-automate/certRotate_test.go
index 6c826c2a6..4c3b58ec9 100644
--- a/components/automate-cli/cmd/chef-automate/certRotate_test.go
+++ b/components/automate-cli/cmd/chef-automate/certRotate_test.go
@@ -458,7 +458,7 @@ func TestCompareCurrentCertsWithNewCerts(t *testing.T) {
 	testCases := []testCaseInfo{
 		{
 			testCaseDescription: "Automate new certs are equal | No node flag",
-			remoteService:       CONST_AUTOMATE,
+			remoteService:       AUTOMATE,
 			newCerts: &certificates{
 				publicCert:  FileContent,
 				privateCert: FileContent,
@@ -484,7 +484,7 @@ func TestCompareCurrentCertsWithNewCerts(t *testing.T) {
 		},
 		{
 			testCaseDescription: "Automate new certs are different | No node flag",
-			remoteService:       CONST_AUTOMATE,
+			remoteService:       AUTOMATE,
 			newCerts: &certificates{
 				publicCert:  FileContent + "a",
 				privateCert: FileContent,
@@ -510,7 +510,7 @@ func TestCompareCurrentCertsWithNewCerts(t *testing.T) {
 		},
 		{
 			testCaseDescription: "Automate new certs are eqaul | Node flag",
-			remoteService:       CONST_AUTOMATE,
+			remoteService:       AUTOMATE,
 			newCerts: &certificates{
 				publicCert:  FileContent,
 				privateCert: FileContent,
@@ -536,7 +536,7 @@ func TestCompareCurrentCertsWithNewCerts(t *testing.T) {
 		},
 		{
 			testCaseDescription: "Chef-server new certs are equal | No node flag",
-			remoteService:       CONST_CHEF_SERVER,
+			remoteService:       CHEF_SERVER,
 			newCerts: &certificates{
 				publicCert:  FileContent,
 				privateCert: FileContent,
@@ -560,7 +560,7 @@ func TestCompareCurrentCertsWithNewCerts(t *testing.T) {
 		},
 		{
 			testCaseDescription: "Chef-server new certs are different | No node flag",
-			remoteService:       CONST_CHEF_SERVER,
+			remoteService:       CHEF_SERVER,
 			newCerts: &certificates{
 				publicCert:  FileContent + "a",
 				privateCert: FileContent,
@@ -584,7 +584,7 @@ func TestCompareCurrentCertsWithNewCerts(t *testing.T) {
 		},
 		{
 			testCaseDescription: "Opensearch new certs are equals | No node flag",
-			remoteService:       CONST_OPENSEARCH,
+			remoteService:       OPENSEARCH,
 			newCerts: &certificates{
 				publicCert:  FileContent,
 				privateCert: FileContent,
@@ -619,7 +619,7 @@ func TestCompareCurrentCertsWithNewCerts(t *testing.T) {
 		},
 		{
 			testCaseDescription: "Opensearch new certs are different | No node flag",
-			remoteService:       CONST_OPENSEARCH,
+			remoteService:       OPENSEARCH,
 			newCerts: &certificates{
 				publicCert:  FileContent + "a",
 				privateCert: FileContent,
@@ -654,7 +654,7 @@ func TestCompareCurrentCertsWithNewCerts(t *testing.T) {
 		},
 		{
 			testCaseDescription: "Postgresql new certs are eqaul | No node flag",
-			remoteService:       CONST_POSTGRESQL,
+			remoteService:       POSTGRESQL,
 			newCerts: &certificates{
 				publicCert:  FileContent,
 				privateCert: FileContent,
@@ -685,7 +685,7 @@ func TestCompareCurrentCertsWithNewCerts(t *testing.T) {
 		},
 		{
 			testCaseDescription: "Postgresql new certs are different | No node flag",
-			remoteService:       CONST_POSTGRESQL,
+			remoteService:       POSTGRESQL,
 			newCerts: &certificates{
 				publicCert:  FileContent,
 				privateCert: FileContent + "a",
@@ -737,16 +737,16 @@ func TestGetFrontEndIpsForSkippingRootCAPatching(t *testing.T) {
 	testCases := []testCaseInfo{
 		{
 			testCaseDescription: "Automate root-ca patching in chef-server | same root-ca",
-			remoteService:       CONST_AUTOMATE,
+			remoteService:       AUTOMATE,
 			newRootCA:           FileContent,
 			currentCertsInfo: &certShowCertificates{
 				AutomateRootCert: FileContent,
 			},
-			skipIpsList: c.getIps(CONST_CHEF_SERVER, infra),
+			skipIpsList: c.getIps(CHEF_SERVER, infra),
 		},
 		{
 			testCaseDescription: "Automate root-ca patching in chef-server | different root-ca",
-			remoteService:       CONST_AUTOMATE,
+			remoteService:       AUTOMATE,
 			newRootCA:           FileContent + "a",
 			currentCertsInfo: &certShowCertificates{
 				AutomateRootCert: FileContent,
@@ -755,7 +755,7 @@ func TestGetFrontEndIpsForSkippingRootCAPatching(t *testing.T) {
 		},
 		{
 			testCaseDescription: "Postgresql root-ca patching in frontend | same root-ca",
-			remoteService:       CONST_POSTGRESQL,
+			remoteService:       POSTGRESQL,
 			newRootCA:           FileContent,
 			currentCertsInfo: &certShowCertificates{
 				PostgresqlRootCert: FileContent,
@@ -764,7 +764,7 @@ func TestGetFrontEndIpsForSkippingRootCAPatching(t *testing.T) {
 		},
 		{
 			testCaseDescription: "Postgresql root-ca patching in frontEnd | different root-ca",
-			remoteService:       CONST_POSTGRESQL,
+			remoteService:       POSTGRESQL,
 			newRootCA:           FileContent + "a",
 			currentCertsInfo: &certShowCertificates{
 				PostgresqlRootCert: FileContent,
@@ -939,8 +939,7 @@ func TestGetOldCn(t *testing.T) {
 					V1: &shared.V1{
 						External: &shared.External{
 							Opensearch: &shared.External_Opensearch{
-								Ssl: &shared.External_Opensearch_SSL{
-								},
+								Ssl: &shared.External_Opensearch_SSL{},
 							},
 						},
 					},
@@ -963,11 +962,11 @@ func TestGetOldCn(t *testing.T) {
 
 	for _, testCase := range testCases {
 		t.Run("GetOldCn", func(t *testing.T) {
-			configMap:= map[string]*deployment.AutomateConfig {
+			configMap := map[string]*deployment.AutomateConfig{
 				ValidIP: testCase.automatesConfig,
 			}
-			oldCn:=getOldCn(configMap)
-			assert.Equal(t, testCase.OldCnExpected,oldCn)
+			oldCn := getOldCn(configMap)
+			assert.Equal(t, testCase.OldCnExpected, oldCn)
 		})
 	}
 }
diff --git a/components/automate-cli/cmd/chef-automate/cmdUtils_test.go b/components/automate-cli/cmd/chef-automate/cmdUtils_test.go
index 1fef16f3b..b6d3cda0e 100644
--- a/components/automate-cli/cmd/chef-automate/cmdUtils_test.go
+++ b/components/automate-cli/cmd/chef-automate/cmdUtils_test.go
@@ -43,7 +43,7 @@ func TestExecute(t *testing.T) {
 
 	nodeFlagFalse := &Cmd{
 		CmdInputs: &CmdInputs{
-			NodeType: false,
+			NodeType:        false,
 			SkipPrintOutput: true,
 		},
 	}
@@ -52,7 +52,7 @@ func TestExecute(t *testing.T) {
 			return errors.New(errorForPreExec)
 		},
 		CmdInputs: &CmdInputs{
-			NodeType: true,
+			NodeType:        true,
 			SkipPrintOutput: true,
 		},
 	}
@@ -616,7 +616,7 @@ func TestPrintOutput(t *testing.T) {
 		{
 			name: "Print Success message and combine output files in one",
 			args: args{
-				remoteService: CONST_AUTOMATE,
+				remoteService: AUTOMATE,
 				result: CmdResult{
 					HostIP:      ip7,
 					OutputFiles: []string{out},
@@ -630,7 +630,7 @@ func TestPrintOutput(t *testing.T) {
 		{
 			name: "Print Success message and file combinations failed",
 			args: args{
-				remoteService: CONST_AUTOMATE,
+				remoteService: AUTOMATE,
 				result: CmdResult{
 					HostIP:      ip7,
 					OutputFiles: []string{fail},
@@ -644,7 +644,7 @@ func TestPrintOutput(t *testing.T) {
 		{
 			name: "Print Error message",
 			args: args{
-				remoteService: CONST_AUTOMATE,
+				remoteService: AUTOMATE,
 				result: CmdResult{
 					HostIP:      ip7,
 					OutputFiles: []string{},
diff --git a/components/automate-cli/cmd/chef-automate/ha_node_add_aws_test.go b/components/automate-cli/cmd/chef-automate/ha_node_add_aws_test.go
index 9ce6837fa..69913b89c 100644
--- a/components/automate-cli/cmd/chef-automate/ha_node_add_aws_test.go
+++ b/components/automate-cli/cmd/chef-automate/ha_node_add_aws_test.go
@@ -13,7 +13,7 @@ import (
 const CONFIG_TOML_PATH_AWS = "../../pkg/testfiles/aws"
 
 func TestAddnodeAWSValidateError(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{automateCount: 0}
 	nodeAdd := NewAddNodeAWS(w.CliWriter, flags, &MockNodeUtilsImpl{
 		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
@@ -37,7 +37,7 @@ func TestAddnodeAWSValidateError(t *testing.T) {
 }
 
 func TestAddnodeAWSValidate(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{opensearchCount: 1}
 	nodeAdd := NewAddNodeAWS(w.CliWriter, flags, &MockNodeUtilsImpl{
 		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
@@ -57,11 +57,11 @@ func TestAddnodeAWSValidate(t *testing.T) {
 	})
 	err := nodeAdd.validate()
 	assert.NoError(t, err)
-	assert.Equal(t, "3", nodeAdd.(*AddNodeAWSImpl).config.Opensearch.Config.InstanceCount)
+	assert.Equal(t, "6", nodeAdd.(*AddNodeAWSImpl).config.Opensearch.Config.InstanceCount)
 }
 
 func TestAddnodeAWSValidateErrorIsManagedServicesOn(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{opensearchCount: 1}
 	nodeAdd := NewAddNodeAWS(w.CliWriter, flags, &MockNodeUtilsImpl{
 		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
@@ -85,7 +85,7 @@ func TestAddnodeAWSValidateErrorIsManagedServicesOn(t *testing.T) {
 }
 
 func TestAddnodeModifyAws(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{
 		chefServerCount: 1,
 	}
@@ -141,15 +141,15 @@ func TestAddAwsnodePrompt(t *testing.T) {
 	assert.Contains(t, w.Output(), `Existing node count:
 ================================================
 Automate => 5
-Chef-Server => 1
-OpenSearch => 3
+Chef-Server => 2
+OpenSearch => 6
 Postgresql => 4
 
 New node count:
 ================================================
 Automate => 6
-Chef-Server => 1
-OpenSearch => 3
+Chef-Server => 2
+OpenSearch => 6
 Postgresql => 4
 This will add the new nodes to your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
 }
@@ -194,22 +194,22 @@ func TestAddnodeDeployWithNewOSNodeInAws(t *testing.T) {
 	assert.NoError(t, err)
 	err = nodeAdd.modifyConfig()
 	assert.NoError(t, err)
-	assert.Equal(t, "4", nodeAdd.(*AddNodeAWSImpl).config.Opensearch.Config.InstanceCount)
+	assert.Equal(t, "7", nodeAdd.(*AddNodeAWSImpl).config.Opensearch.Config.InstanceCount)
 	res, err := nodeAdd.promptUserConfirmation()
 	assert.Equal(t, true, res)
 	assert.NoError(t, err)
 	assert.Contains(t, w.Output(), `Existing node count:
 ================================================
 Automate => 5
-Chef-Server => 1
-OpenSearch => 3
+Chef-Server => 2
+OpenSearch => 6
 Postgresql => 4
 
 New node count:
 ================================================
 Automate => 5
-Chef-Server => 1
-OpenSearch => 4
+Chef-Server => 2
+OpenSearch => 7
 Postgresql => 4
 This will add the new nodes to your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
 	err = nodeAdd.runDeploy()
@@ -282,15 +282,15 @@ func TestAddnodeWithExecuteFuncGenConfigErr(t *testing.T) {
 	assert.Contains(t, w.Output(), `Existing node count:
 ================================================
 Automate => 5
-Chef-Server => 1
-OpenSearch => 3
+Chef-Server => 2
+OpenSearch => 6
 Postgresql => 4
 
 New node count:
 ================================================
 Automate => 5
-Chef-Server => 1
-OpenSearch => 4
+Chef-Server => 2
+OpenSearch => 7
 Postgresql => 4
 This will add the new nodes to your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
 	assert.Equal(t, true, autoFileMoved)
@@ -346,15 +346,15 @@ func TestAddnodeWithExecuteFunc(t *testing.T) {
 	assert.Contains(t, w.Output(), `Existing node count:
 ================================================
 Automate => 5
-Chef-Server => 1
-OpenSearch => 3
+Chef-Server => 2
+OpenSearch => 6
 Postgresql => 4
 
 New node count:
 ================================================
 Automate => 5
-Chef-Server => 1
-OpenSearch => 4
+Chef-Server => 2
+OpenSearch => 7
 Postgresql => 4
 This will add the new nodes to your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
 	assert.Equal(t, true, autoFileMoved)
diff --git a/components/automate-cli/cmd/chef-automate/ha_node_delete_aws_test.go b/components/automate-cli/cmd/chef-automate/ha_node_delete_aws_test.go
index 5044e2a17..a1553534e 100644
--- a/components/automate-cli/cmd/chef-automate/ha_node_delete_aws_test.go
+++ b/components/automate-cli/cmd/chef-automate/ha_node_delete_aws_test.go
@@ -17,11 +17,10 @@ Chef-Server Ip 193.0.0.1 is not present in existing list of ip addresses. Please
 Unable to remove node. OpenSearch instance count cannot be less than 3. Final count 2 not allowed.
 OpenSearch Ip 193.0.0.1 is not present in existing list of ip addresses. Please use a different private ip.
 Postgresql Ip 193.0.0.1 is not present in existing list of ip addresses. Please use a different private ip.`
+	automateIpNotPresent = `IP address validation failed: 
+Automate Ip 193.0.0.1 is not present in existing list of ip addresses. Please use a different private ip.`
 	multipleIpAddressError = `IP address validation failed: 
-Only one Automate is allowed to delete for AWS deployment type
-Only one Chef-Server is allowed to delete for AWS deployment type
-Only one OpenSearch is allowed to delete for AWS deployment type
-Only one Postgresql is allowed to delete for AWS deployment type`
+Only one node can be deleted at a time`
 	isManagedServicesError = `Cannot remove OpenSearch or Postgresql nodes if external.database.type is either aws or self-managed.
 Please set external.database.type to empty if you want to add OpenSearch or Postgresql nodes`
 )
@@ -80,10 +79,7 @@ func TestDeletenodeAWSValidateIsManagedServicesOnError(t *testing.T) {
 func TestDeletenodeAWSValidateErrorIpAddressNotMatched(t *testing.T) {
 	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
 	flags := AddDeleteNodeHACmdFlags{
-		automateIp:   "193.0.0.1",
-		chefServerIp: "193.0.0.1",
-		opensearchIp: "193.0.0.1",
-		postgresqlIp: "193.0.0.1",
+		automateIp: "193.0.0.1",
 	}
 	nodeDelete := NewDeleteNodeAWS(
 		w.CliWriter,
@@ -127,7 +123,7 @@ func TestDeletenodeAWSValidateErrorIpAddressNotMatched(t *testing.T) {
 	err := nodeDelete.(*DeleteNodeAWSImpl).getAwsHAIp()
 	assert.NoError(t, err)
 	err = nodeDelete.validate()
-	assert.Equal(t, err.Error(), ipAddressNotPresent)
+	assert.Equal(t, automateIpNotPresent, err.Error())
 }
 
 func TestDeletenodeAWSValidateErrorMoreThenOneIpAddress(t *testing.T) {
@@ -180,10 +176,10 @@ func TestDeletenodeAWSValidateErrorMoreThenOneIpAddress(t *testing.T) {
 	err := nodeDelete.(*DeleteNodeAWSImpl).getAwsHAIp()
 	assert.NoError(t, err)
 	err = nodeDelete.validate()
-	assert.Contains(t, err.Error(), multipleIpAddressError)
+	assert.Contains(t, err.Error(), multipleNodeError)
 }
 
-func TestDeletenodeAWSModify(t *testing.T) {
+func TestDeletenodeAWSModifyA2(t *testing.T) {
 	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
 	flags := AddDeleteNodeHACmdFlags{
 		automateIp: "192.0.0.1",
@@ -236,6 +232,166 @@ func TestDeletenodeAWSModify(t *testing.T) {
 	assert.Equal(t, "4", nodeDelete.(*DeleteNodeAWSImpl).config.Automate.Config.InstanceCount)
 }
 
+func TestDeletenodeAWSModifyCS(t *testing.T) {
+	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	flags := AddDeleteNodeHACmdFlags{
+		chefServerIp: "192.0.1.2",
+	}
+	nodeDelete := NewDeleteNodeAWS(
+		w.CliWriter,
+		flags,
+		&MockNodeUtilsImpl{
+			getAWSConfigIpFunc: func() (*AWSConfigIp, error) {
+				return &AWSConfigIp{
+					configAutomateIpList:   []string{"192.0.0.1", "192.0.0.2", "192.0.0.3", "192.0.0.4"},
+					configChefServerIpList: []string{"192.0.1.1", "192.0.1.2", "192.0.1.3", "192.0.1.4"},
+					configOpensearchIpList: []string{"192.0.2.1", "192.0.2.2", "192.0.2.3", "192.0.2.4"},
+					configPostgresqlIpList: []string{"192.0.3.1", "192.0.3.2", "192.0.3.3", "192.0.3.4"},
+				}, nil
+			},
+			getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
+				return nil, &SSHConfig{}, nil
+			},
+			executeAutomateClusterCtlCommandAsyncfunc: func(command string, args []string, helpDocs string) error {
+				return nil
+			},
+			writeHAConfigFilesFunc: func(templateName string, data interface{}) error {
+				return nil
+			},
+			getModeFromConfigFunc: func(path string) (string, error) {
+				return EXISTING_INFRA_MODE, nil
+			},
+			isManagedServicesOnFunc: func() bool {
+				return false
+			},
+			pullAndUpdateConfigAwsFunc: PullAwsConfFunc,
+			isA2HARBFileExistFunc: func() bool {
+				return true
+			},
+		},
+		CONFIG_TOML_PATH_AWS,
+		&fileutils.MockFileSystemUtils{},
+		&MockSSHUtilsImpl{
+			connectAndExecuteCommandOnRemoteFunc: func(remoteCommands string, spinner bool) (string, error) {
+				return "", nil
+			},
+		})
+	err := nodeDelete.(*DeleteNodeAWSImpl).getAwsHAIp()
+	assert.NoError(t, err)
+
+	err = nodeDelete.validate()
+	assert.NoError(t, err)
+	err = nodeDelete.modifyConfig()
+	assert.NoError(t, err)
+	assert.Equal(t, "1", nodeDelete.(*DeleteNodeAWSImpl).config.ChefServer.Config.InstanceCount)
+}
+
+func TestDeletenodeAWSModifyPG(t *testing.T) {
+	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	flags := AddDeleteNodeHACmdFlags{
+		postgresqlIp: "192.0.3.3",
+	}
+	nodeDelete := NewDeleteNodeAWS(
+		w.CliWriter,
+		flags,
+		&MockNodeUtilsImpl{
+			getAWSConfigIpFunc: func() (*AWSConfigIp, error) {
+				return &AWSConfigIp{
+					configAutomateIpList:   []string{"192.0.0.1", "192.0.0.2", "192.0.0.3", "192.0.0.4"},
+					configChefServerIpList: []string{"192.0.1.1", "192.0.1.2", "192.0.1.3", "192.0.1.4"},
+					configOpensearchIpList: []string{"192.0.2.1", "192.0.2.2", "192.0.2.3", "192.0.2.4"},
+					configPostgresqlIpList: []string{"192.0.3.1", "192.0.3.2", "192.0.3.3", "192.0.3.4"},
+				}, nil
+			},
+			getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
+				return nil, &SSHConfig{}, nil
+			},
+			executeAutomateClusterCtlCommandAsyncfunc: func(command string, args []string, helpDocs string) error {
+				return nil
+			},
+			writeHAConfigFilesFunc: func(templateName string, data interface{}) error {
+				return nil
+			},
+			getModeFromConfigFunc: func(path string) (string, error) {
+				return EXISTING_INFRA_MODE, nil
+			},
+			isManagedServicesOnFunc: func() bool {
+				return false
+			},
+			pullAndUpdateConfigAwsFunc: PullAwsConfFunc,
+			isA2HARBFileExistFunc: func() bool {
+				return true
+			},
+		},
+		CONFIG_TOML_PATH_AWS,
+		&fileutils.MockFileSystemUtils{},
+		&MockSSHUtilsImpl{
+			connectAndExecuteCommandOnRemoteFunc: func(remoteCommands string, spinner bool) (string, error) {
+				return "", nil
+			},
+		})
+	err := nodeDelete.(*DeleteNodeAWSImpl).getAwsHAIp()
+	assert.NoError(t, err)
+	err = nodeDelete.validate()
+	assert.NoError(t, err)
+	err = nodeDelete.modifyConfig()
+	assert.NoError(t, err)
+	assert.Equal(t, "3", nodeDelete.(*DeleteNodeAWSImpl).config.Postgresql.Config.InstanceCount)
+}
+
+func TestDeletenodeAWSModifyOS(t *testing.T) {
+	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	flags := AddDeleteNodeHACmdFlags{
+		opensearchIp: "192.0.2.4",
+	}
+	nodeDelete := NewDeleteNodeAWS(
+		w.CliWriter,
+		flags,
+		&MockNodeUtilsImpl{
+			getAWSConfigIpFunc: func() (*AWSConfigIp, error) {
+				return &AWSConfigIp{
+					configAutomateIpList:   []string{"192.0.0.1", "192.0.0.2", "192.0.0.3", "192.0.0.4"},
+					configChefServerIpList: []string{"192.0.1.1", "192.0.1.2", "192.0.1.3", "192.0.1.4"},
+					configOpensearchIpList: []string{"192.0.2.1", "192.0.2.2", "192.0.2.3", "192.0.2.4"},
+					configPostgresqlIpList: []string{"192.0.3.1", "192.0.3.2", "192.0.3.3", "192.0.3.4"},
+				}, nil
+			},
+			getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
+				return nil, &SSHConfig{}, nil
+			},
+			executeAutomateClusterCtlCommandAsyncfunc: func(command string, args []string, helpDocs string) error {
+				return nil
+			},
+			writeHAConfigFilesFunc: func(templateName string, data interface{}) error {
+				return nil
+			},
+			getModeFromConfigFunc: func(path string) (string, error) {
+				return EXISTING_INFRA_MODE, nil
+			},
+			isManagedServicesOnFunc: func() bool {
+				return false
+			},
+			pullAndUpdateConfigAwsFunc: PullAwsConfFunc,
+			isA2HARBFileExistFunc: func() bool {
+				return true
+			},
+		},
+		CONFIG_TOML_PATH_AWS,
+		&fileutils.MockFileSystemUtils{},
+		&MockSSHUtilsImpl{
+			connectAndExecuteCommandOnRemoteFunc: func(remoteCommands string, spinner bool) (string, error) {
+				return "", nil
+			},
+		})
+	err := nodeDelete.(*DeleteNodeAWSImpl).getAwsHAIp()
+	assert.NoError(t, err)
+	err = nodeDelete.validate()
+	assert.NoError(t, err)
+	err = nodeDelete.modifyConfig()
+	assert.NoError(t, err)
+	assert.Equal(t, "5", nodeDelete.(*DeleteNodeAWSImpl).config.Opensearch.Config.InstanceCount)
+}
+
 func TestDeleteAwsnodePrompt(t *testing.T) {
 	w := majorupgrade_utils.NewCustomWriterWithInputs("y")
 	flags := AddDeleteNodeHACmdFlags{
@@ -295,11 +451,11 @@ Chef-Server => 192.0.1.1, 192.0.1.2, 192.0.1.3, 192.0.1.4
 OpenSearch => 192.0.2.1, 192.0.2.2, 192.0.2.3, 192.0.2.4
 Postgresql => 192.0.3.1, 192.0.3.2, 192.0.3.3, 192.0.3.4
 
-Nodes to be deleted:
+Node to be deleted:
 ================================================
 Automate => 192.0.0.1
-Removal of nodes for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch nodes.
-This will delete the above nodes from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
+Removal of node for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch node.
+This will delete the above node from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
 }
 
 func TestDeletenodeDeployWithNewOSNodeInAws(t *testing.T) {
@@ -379,11 +535,11 @@ Chef-Server => 192.0.1.1, 192.0.1.2, 192.0.1.3, 192.0.1.4
 OpenSearch => 192.0.2.1, 192.0.2.2, 192.0.2.3, 192.0.2.4
 Postgresql => 192.0.3.1, 192.0.3.2, 192.0.3.3, 192.0.3.4
 
-Nodes to be deleted:
+Node to be deleted:
 ================================================
 Automate => 192.0.0.1
-Removal of nodes for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch nodes.
-This will delete the above nodes from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
+Removal of node for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch node.
+This will delete the above node from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
 	err = nodeDelete.runDeploy()
 	assert.NoError(t, err)
 	assert.Equal(t, true, autoFileMoved)
@@ -467,6 +623,12 @@ func TestDeletenodeAWSExecuteWithError(t *testing.T) {
 				tfArchModified = true
 				return nil
 			},
+			stopServicesOnNodeFunc: func(ip, nodeType, deploymentType string, infra *AutomateHAInfraDetails) error {
+				return nil
+			},
+			calculateTotalInstanceCountFunc: func() (int, error) {
+				return 0, nil
+			},
 		},
 		CONFIG_TOML_PATH_AWS,
 		&fileutils.MockFileSystemUtils{},
@@ -486,73 +648,96 @@ func TestDeletenodeAWSExecuteWithError(t *testing.T) {
 }
 
 func TestDeletenodeAWSExecuteNoError(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("y")
-	flags := AddDeleteNodeHACmdFlags{
-		automateIp: "192.0.0.1",
-	}
+	count := 10
 	var ipAddres, filewritten, executeCommands, autoFileMoved, tfArchModified bool
-
-	nodeDelete := NewDeleteNodeAWS(
-		w.CliWriter,
-		flags,
-		&MockNodeUtilsImpl{
-			getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
-				return nil, &SSHConfig{}, nil
-			},
-			executeAutomateClusterCtlCommandAsyncfunc: func(command string, args []string, helpDocs string) error {
-				return nil
-			},
-			writeHAConfigFilesFunc: func(templateName string, data interface{}) error {
-				filewritten = true
-				return nil
-			},
-			getModeFromConfigFunc: func(path string) (string, error) {
-				return EXISTING_INFRA_MODE, nil
-			},
-			isManagedServicesOnFunc: func() bool {
-				return false
-			},
-			pullAndUpdateConfigAwsFunc: PullAwsConfFunc,
-			isA2HARBFileExistFunc: func() bool {
-				return true
-			},
-			taintTerraformFunc: func(path string) error {
-				return nil
-			},
-			getAWSConfigIpFunc: func() (*AWSConfigIp, error) {
-				ipAddres = true
-				return &AWSConfigIp{
-					configAutomateIpList:   []string{"192.0.0.1", "192.0.0.2", "192.0.0.3", "192.0.0.4"},
-					configChefServerIpList: []string{"192.0.1.1", "192.0.1.2", "192.0.1.3", "192.0.1.4"},
-					configOpensearchIpList: []string{"192.0.2.1", "192.0.2.2", "192.0.2.3", "192.0.2.4"},
-					configPostgresqlIpList: []string{"192.0.3.1", "192.0.3.2", "192.0.3.3", "192.0.3.4"},
-				}, nil
-			},
-			executeShellCommandFunc: func() error {
-				executeCommands = true
-				return nil
-			},
-			moveAWSAutoTfvarsFileFunc: func(path string) error {
-				autoFileMoved = true
-				return nil
-			},
-			modifyTfArchFileFunc: func(path string) error {
-				tfArchModified = true
-				return nil
-			},
+	mnu := &MockNodeUtilsImpl{
+		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
+			return nil, &SSHConfig{}, nil
 		},
-		CONFIG_TOML_PATH_AWS,
-		&fileutils.MockFileSystemUtils{},
-		&MockSSHUtilsImpl{
-			connectAndExecuteCommandOnRemoteFunc: func(remoteCommands string, spinner bool) (string, error) {
-				return "", nil
-			},
-		})
-	err := nodeDelete.Execute(nil, nil)
-	assert.NoError(t, err)
-	assert.True(t, tfArchModified)
-	assert.True(t, ipAddres)
-	assert.True(t, filewritten)
-	assert.True(t, executeCommands)
-	assert.True(t, autoFileMoved)
+		executeAutomateClusterCtlCommandAsyncfunc: func(command string, args []string, helpDocs string) error {
+			return nil
+		},
+		writeHAConfigFilesFunc: func(templateName string, data interface{}) error {
+			filewritten = true
+			return nil
+		},
+		getModeFromConfigFunc: func(path string) (string, error) {
+			return EXISTING_INFRA_MODE, nil
+		},
+		isManagedServicesOnFunc: func() bool {
+			return false
+		},
+		pullAndUpdateConfigAwsFunc: PullAwsConfFunc,
+		isA2HARBFileExistFunc: func() bool {
+			return true
+		},
+		taintTerraformFunc: func(path string) error {
+			return nil
+		},
+		getAWSConfigIpFunc: func() (*AWSConfigIp, error) {
+			ipAddres = true
+			return &AWSConfigIp{
+				configAutomateIpList:   []string{"192.0.0.1", "192.0.0.2", "192.0.0.3", "192.0.0.4"},
+				configChefServerIpList: []string{"192.0.1.1", "192.0.1.2", "192.0.1.3", "192.0.1.4"},
+				configOpensearchIpList: []string{"192.0.2.1", "192.0.2.2", "192.0.2.3", "192.0.2.4"},
+				configPostgresqlIpList: []string{"192.0.3.1", "192.0.3.2", "192.0.3.3", "192.0.3.4"},
+			}, nil
+		},
+		executeShellCommandFunc: func() error {
+			executeCommands = true
+			return nil
+		},
+		moveAWSAutoTfvarsFileFunc: func(path string) error {
+			autoFileMoved = true
+			return nil
+		},
+		modifyTfArchFileFunc: func(path string) error {
+			tfArchModified = true
+			return nil
+		},
+		stopServicesOnNodeFunc: func(ip, nodeType, deploymentType string, infra *AutomateHAInfraDetails) error {
+			return nil
+		},
+		calculateTotalInstanceCountFunc: func() (int, error) {
+			count = count - 1
+			return count, nil
+		},
+	}
+	flagsArr := []AddDeleteNodeHACmdFlags{
+		{
+			automateIp: "192.0.0.1",
+		},
+		{
+			chefServerIp: "192.0.1.1",
+		},
+		{
+			postgresqlIp: "192.0.3.1",
+		},
+		{
+			opensearchIp: "192.0.2.1",
+		},
+	}
+
+	for _, flags := range flagsArr {
+		w := majorupgrade_utils.NewCustomWriterWithInputs("y")
+		nodeDelete := NewDeleteNodeAWS(
+			w.CliWriter,
+			flags,
+			mnu,
+			CONFIG_TOML_PATH_AWS,
+			&fileutils.MockFileSystemUtils{},
+			&MockSSHUtilsImpl{
+				connectAndExecuteCommandOnRemoteFunc: func(remoteCommands string, spinner bool) (string, error) {
+					return "", nil
+				},
+			})
+		err := nodeDelete.Execute(nil, nil)
+		assert.NoError(t, err)
+		assert.True(t, tfArchModified)
+		assert.True(t, ipAddres)
+		assert.True(t, filewritten)
+		assert.True(t, executeCommands)
+		assert.True(t, autoFileMoved)
+	}
+
 }
diff --git a/components/automate-cli/cmd/chef-automate/ha_node_delete_onprem_test.go b/components/automate-cli/cmd/chef-automate/ha_node_delete_onprem_test.go
index 37294ba45..4c78dcc15 100644
--- a/components/automate-cli/cmd/chef-automate/ha_node_delete_onprem_test.go
+++ b/components/automate-cli/cmd/chef-automate/ha_node_delete_onprem_test.go
@@ -10,6 +10,10 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+const (
+	multipleNodeError = `Only one node can be deleted at a time`
+)
+
 func PullConfFunc(sshUtil *SSHUtil, ex []string) (*ExistingInfraConfigToml, error) {
 	cfg, err := readConfig(CONFIG_TOML_PATH + "/config.toml")
 	if err != nil {
@@ -27,9 +31,9 @@ func PullAwsConfFunc(sshUtil *SSHUtil, ex []string) (*AwsConfigToml, error) {
 }
 
 func TestDeleteNodeValidateError(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{
-		automateIp: "10.2.1.67,ewewedw",
+		automateIp: "192.0.2.2",
 	}
 	nodedelete := NewDeleteNodeOnPrem(w.CliWriter, flags, &MockNodeUtilsImpl{
 		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
@@ -49,17 +53,14 @@ func TestDeleteNodeValidateError(t *testing.T) {
 	})
 	err := nodedelete.validate()
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(),
-		`Unable to remove node. Automate instance count cannot be less than 1. Final count 0 not allowed.`)
+	assert.Contains(t, err.Error(), `
+Automate Ip 192.0.2.2 is not present in existing list of ip addresses. Please use a different private ip.`)
 }
 
 func TestDeleteNodeValidateErrorMultiple(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{
-		automateIp:   "10.2.1.67,ewewedw",
-		chefServerIp: "10.2.1.637,ewewedw",
-		postgresqlIp: "10.2.1.657,ewewedw",
-		opensearchIp: "10.2.1.61,ewewedw",
+		automateIp: "10.2.1.67,10.2.1.637",
 	}
 	nodedelete := NewDeleteNodeOnPrem(w.CliWriter, flags, &MockNodeUtilsImpl{
 		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
@@ -80,14 +81,11 @@ func TestDeleteNodeValidateErrorMultiple(t *testing.T) {
 	err := nodedelete.validate()
 	assert.Error(t, err)
 	assert.Contains(t, err.Error(),
-		`Unable to remove node. Automate instance count cannot be less than 1. Final count 0 not allowed.
-Unable to remove node. Chef-Server instance count cannot be less than 1. Final count -1 not allowed.
-Unable to remove node. OpenSearch instance count cannot be less than 3. Final count 2 not allowed.
-Unable to remove node. Postgresql instance count cannot be less than 3. Final count 1 not allowed.`)
+		multipleNodeError)
 }
 
 func TestDeleteNodeModifyAutomate(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{
 		automateIp: "192.0.2.0",
 	}
@@ -111,14 +109,14 @@ func TestDeleteNodeModifyAutomate(t *testing.T) {
 	assert.NoError(t, err)
 	err = nodedelete.modifyConfig()
 	assert.NoError(t, err)
-	assert.Equal(t, flags.automateIp, nodedelete.(*DeleteNodeOnPremImpl).automateIpList[0])
+	assert.Equal(t, flags.automateIp, nodedelete.(*DeleteNodeOnPremImpl).ipToDelete)
 	assert.Equal(t, "1", nodedelete.(*DeleteNodeOnPremImpl).config.Automate.Config.InstanceCount)
 	assert.Equal(t, 1, len(nodedelete.(*DeleteNodeOnPremImpl).config.Automate.Config.CertsByIP))
 	assert.Equal(t, 1, len(nodedelete.(*DeleteNodeOnPremImpl).config.ExistingInfra.Config.AutomatePrivateIps))
 }
 
 func TestRemovenodeValidateTypeAwsOrSelfManaged(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{
 		postgresqlIp: TEST_IP_1,
 	}
@@ -150,7 +148,7 @@ func TestRemovenodeValidateTypeAwsOrSelfManaged(t *testing.T) {
 }
 
 func TestRemovenodeValidateTypeAwsOrSelfManaged2(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{
 		opensearchIp: TEST_IP_1,
 		automateIp:   TEST_IP_2,
@@ -179,11 +177,11 @@ func TestRemovenodeValidateTypeAwsOrSelfManaged2(t *testing.T) {
 	})
 	err := nodeAdd.validate()
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), fmt.Sprintf(TYPE_ERROR, "remove"))
+	assert.Contains(t, err.Error(), multipleNodeError)
 }
 
 func TestDeleteNodeModifyInfra(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{
 		chefServerIp: "192.0.2.2",
 	}
@@ -209,14 +207,14 @@ func TestDeleteNodeModifyInfra(t *testing.T) {
 	// even though validation will fail still we check if modify config is working as expected or not
 	err = nodedelete.modifyConfig()
 	assert.NoError(t, err)
-	assert.Equal(t, flags.chefServerIp, nodedelete.(*DeleteNodeOnPremImpl).chefServerIpList[0])
+	assert.Equal(t, flags.chefServerIp, nodedelete.(*DeleteNodeOnPremImpl).ipToDelete)
 	assert.Equal(t, "0", nodedelete.(*DeleteNodeOnPremImpl).config.ChefServer.Config.InstanceCount)
 	assert.Equal(t, 0, len(nodedelete.(*DeleteNodeOnPremImpl).config.ChefServer.Config.CertsByIP))
 	assert.Equal(t, 0, len(nodedelete.(*DeleteNodeOnPremImpl).config.ExistingInfra.Config.ChefServerPrivateIps))
 }
 
 func TestDeletenodeModifyOpensearch(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{
 		opensearchIp: "192.0.2.6",
 	}
@@ -240,14 +238,14 @@ func TestDeletenodeModifyOpensearch(t *testing.T) {
 	assert.NoError(t, err)
 	err = nodedelete.modifyConfig()
 	assert.NoError(t, err)
-	assert.Equal(t, flags.opensearchIp, nodedelete.(*DeleteNodeOnPremImpl).opensearchIpList[0])
+	assert.Equal(t, flags.opensearchIp, nodedelete.(*DeleteNodeOnPremImpl).ipToDelete)
 	assert.Equal(t, "3", nodedelete.(*DeleteNodeOnPremImpl).config.Opensearch.Config.InstanceCount)
 	assert.Equal(t, 3, len(nodedelete.(*DeleteNodeOnPremImpl).config.Opensearch.Config.CertsByIP))
 	assert.Equal(t, 3, len(nodedelete.(*DeleteNodeOnPremImpl).config.ExistingInfra.Config.OpensearchPrivateIps))
 }
 
 func TestDeletenodeModifyPostgresql(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("x")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{
 		postgresqlIp: "192.0.2.9",
 	}
@@ -273,7 +271,7 @@ func TestDeletenodeModifyPostgresql(t *testing.T) {
 	// even though validation will fail still we check if modify config is working as expected or not
 	err = nodedelete.modifyConfig()
 	assert.NoError(t, err)
-	assert.Equal(t, flags.postgresqlIp, nodedelete.(*DeleteNodeOnPremImpl).postgresqlIp[0])
+	assert.Equal(t, flags.postgresqlIp, nodedelete.(*DeleteNodeOnPremImpl).ipToDelete)
 	assert.Equal(t, "2", nodedelete.(*DeleteNodeOnPremImpl).config.Postgresql.Config.InstanceCount)
 	assert.Equal(t, 2, len(nodedelete.(*DeleteNodeOnPremImpl).config.Postgresql.Config.CertsByIP))
 	assert.Equal(t, 2, len(nodedelete.(*DeleteNodeOnPremImpl).config.ExistingInfra.Config.PostgresqlPrivateIps))
@@ -304,7 +302,7 @@ func TestDeleteNodePrompt(t *testing.T) {
 	assert.NoError(t, err)
 	err = nodedelete.modifyConfig()
 	assert.NoError(t, err)
-	assert.Equal(t, flags.automateIp, nodedelete.(*DeleteNodeOnPremImpl).automateIpList[0])
+	assert.Equal(t, flags.automateIp, nodedelete.(*DeleteNodeOnPremImpl).ipToDelete)
 	res, err := nodedelete.promptUserConfirmation()
 	assert.Equal(t, true, res)
 	assert.NoError(t, err)
@@ -315,11 +313,11 @@ Chef-Server => 192.0.2.2
 OpenSearch => 192.0.2.3, 192.0.2.4, 192.0.2.5, 192.0.2.6
 Postgresql => 192.0.2.7, 192.0.2.8, 192.0.2.9
 
-Nodes to be deleted:
+Node to be deleted:
 ================================================
 Automate => 192.0.2.0
-Removal of nodes for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch nodes.
-This will delete the above nodes from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
+Removal of node for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch node.
+This will delete the above node from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
 }
 
 func TestDeleteNodeDeployWithNewOSNode(t *testing.T) {
@@ -356,7 +354,7 @@ func TestDeleteNodeDeployWithNewOSNode(t *testing.T) {
 	assert.NoError(t, err)
 	err = nodedelete.modifyConfig()
 	assert.NoError(t, err)
-	assert.Equal(t, flags.opensearchIp, nodedelete.(*DeleteNodeOnPremImpl).opensearchIpList[0])
+	assert.Equal(t, flags.opensearchIp, nodedelete.(*DeleteNodeOnPremImpl).ipToDelete)
 	res, err := nodedelete.promptUserConfirmation()
 	assert.Equal(t, true, res)
 	assert.NoError(t, err)
@@ -367,11 +365,11 @@ Chef-Server => 192.0.2.2
 OpenSearch => 192.0.2.3, 192.0.2.4, 192.0.2.5, 192.0.2.6
 Postgresql => 192.0.2.7, 192.0.2.8, 192.0.2.9
 
-Nodes to be deleted:
+Node to be deleted:
 ================================================
-OpenSearch => 192.0.2.3
-Removal of nodes for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch nodes.
-This will delete the above nodes from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
+Opensearch => 192.0.2.3
+Removal of node for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch node.
+This will delete the above node from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
 	err = nodedelete.runDeploy()
 	assert.NoError(t, err)
 	assert.Equal(t, true, filewritten)
@@ -379,7 +377,7 @@ This will delete the above nodes from your existing setup. It might take a while
 }
 
 func TestDeleteNodeDeployWithNewOSMinCountError(t *testing.T) {
-	w := majorupgrade_utils.NewCustomWriterWithInputs("y")
+	w := majorupgrade_utils.NewCustomWriter()
 	flags := AddDeleteNodeHACmdFlags{
 		opensearchIp: "192.0.2.3,192.0.2.5",
 	}
@@ -408,7 +406,7 @@ func TestDeleteNodeDeployWithNewOSMinCountError(t *testing.T) {
 	err := nodedelete.validate()
 	assert.Error(t, err)
 	assert.Contains(t, err.Error(),
-		`Unable to remove node. OpenSearch instance count cannot be less than 3. Final count 2 not allowed.`)
+		multipleNodeError)
 }
 
 func TestDeleteNodeDeployWithNewOSNodeError(t *testing.T) {
@@ -442,7 +440,7 @@ func TestDeleteNodeDeployWithNewOSNodeError(t *testing.T) {
 	assert.NoError(t, err)
 	err = nodedelete.modifyConfig()
 	assert.NoError(t, err)
-	assert.Equal(t, flags.opensearchIp, nodedelete.(*DeleteNodeOnPremImpl).opensearchIpList[0])
+	assert.Equal(t, flags.opensearchIp, nodedelete.(*DeleteNodeOnPremImpl).ipToDelete)
 	res, err := nodedelete.promptUserConfirmation()
 	assert.Equal(t, true, res)
 	assert.NoError(t, err)
@@ -453,17 +451,18 @@ Chef-Server => 192.0.2.2
 OpenSearch => 192.0.2.3, 192.0.2.4, 192.0.2.5, 192.0.2.6
 Postgresql => 192.0.2.7, 192.0.2.8, 192.0.2.9
 
-Nodes to be deleted:
+Node to be deleted:
 ================================================
-OpenSearch => 192.0.2.3
-Removal of nodes for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch nodes.
-This will delete the above nodes from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
+Opensearch => 192.0.2.3
+Removal of node for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch node.
+This will delete the above node from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
 	err = nodedelete.runDeploy()
 	assert.Error(t, err)
 	assert.Contains(t, err.Error(), "random")
 }
 
 func TestRemovenodeExecuteWithNewOSNodeNoCertsByIP(t *testing.T) {
+	count := 10
 	w := majorupgrade_utils.NewCustomWriterWithInputs("y")
 	flags := AddDeleteNodeHACmdFlags{
 		opensearchIp: "192.0.2.6",
@@ -496,6 +495,13 @@ func TestRemovenodeExecuteWithNewOSNodeNoCertsByIP(t *testing.T) {
 		isManagedServicesOnFunc: func() bool {
 			return false
 		},
+		stopServicesOnNodeFunc: func(ip, nodeType, deploymentType string, infra *AutomateHAInfraDetails) error {
+			return nil
+		},
+		calculateTotalInstanceCountFunc: func() (int, error) {
+			count = count - 1
+			return count, nil
+		},
 		pullAndUpdateConfigFunc: func(sshUtil *SSHUtil, exceptionIps []string) (*ExistingInfraConfigToml, error) {
 			cfg, err := readConfig(CONFIG_TOML_PATH + "/config.toml")
 			if err != nil {
@@ -521,16 +527,17 @@ Chef-Server => 192.0.2.2
 OpenSearch => 192.0.2.3, 192.0.2.4, 192.0.2.5, 192.0.2.6
 Postgresql => 192.0.2.7, 192.0.2.8, 192.0.2.9
 
-Nodes to be deleted:
+Node to be deleted:
 ================================================
-OpenSearch => 192.0.2.6
-Removal of nodes for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch nodes.
-This will delete the above nodes from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
+Opensearch => 192.0.2.6
+Removal of node for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch node.
+This will delete the above node from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
 	assert.Equal(t, true, filewritten)
 	assert.Equal(t, true, deployed)
 }
 
 func TestRemovenodeExecuteWithNewOSNode(t *testing.T) {
+	count := 10
 	w := majorupgrade_utils.NewCustomWriterWithInputs("y")
 	flags := AddDeleteNodeHACmdFlags{
 		opensearchIp: "192.0.2.6",
@@ -563,6 +570,13 @@ func TestRemovenodeExecuteWithNewOSNode(t *testing.T) {
 		isManagedServicesOnFunc: func() bool {
 			return false
 		},
+		stopServicesOnNodeFunc: func(ip, nodeType, deploymentType string, infra *AutomateHAInfraDetails) error {
+			return nil
+		},
+		calculateTotalInstanceCountFunc: func() (int, error) {
+			count = count - 1
+			return count, nil
+		},
 		pullAndUpdateConfigFunc: PullConfFunc,
 	}, CONFIG_TOML_PATH, &fileutils.MockFileSystemUtils{}, &MockSSHUtilsImpl{
 		connectAndExecuteCommandOnRemoteFunc: func(remoteCommands string, spinner bool) (string, error) {
@@ -578,11 +592,11 @@ Chef-Server => 192.0.2.2
 OpenSearch => 192.0.2.3, 192.0.2.4, 192.0.2.5, 192.0.2.6
 Postgresql => 192.0.2.7, 192.0.2.8, 192.0.2.9
 
-Nodes to be deleted:
+Node to be deleted:
 ================================================
-OpenSearch => 192.0.2.6
-Removal of nodes for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch nodes.
-This will delete the above nodes from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
+Opensearch => 192.0.2.6
+Removal of node for Postgresql or OpenSearch is at your own risk and may result to data loss. Consult your database administrator before trying to delete Postgresql or OpenSearch node.
+This will delete the above node from your existing setup. It might take a while. Are you sure you want to continue? (y/n)`)
 	assert.Equal(t, true, filewritten)
 	assert.Equal(t, true, deployed)
 }
diff --git a/components/automate-cli/cmd/chef-automate/ha_node_operations_utils_test.go b/components/automate-cli/cmd/chef-automate/ha_node_operations_utils_test.go
index 9e2a7a98a..2c1981531 100644
--- a/components/automate-cli/cmd/chef-automate/ha_node_operations_utils_test.go
+++ b/components/automate-cli/cmd/chef-automate/ha_node_operations_utils_test.go
@@ -1,19 +1,26 @@
 package main
 
 import (
+	"errors"
 	"os"
 	"path/filepath"
 	"testing"
 
 	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/majorupgrade_utils"
+	"github.com/chef/automate/lib/platform/command"
 	"github.com/stretchr/testify/assert"
 )
 
-const TEST_IP_2 = "192.0.2.12"
-const TEST_IP_3 = "192.0.2.13"
-const TEST_IP_4 = "192.0.2.14"
-const TEST_IP_5 = "192.0.2.15"
-const TEST_IP_6 = "192.0.2.16"
+const (
+	TEST_IP_2 = "192.0.2.12"
+	TEST_IP_3 = "192.0.2.13"
+	TEST_IP_4 = "192.0.2.14"
+	TEST_IP_5 = "192.0.2.15"
+	TEST_IP_6 = "192.0.2.16"
+)
+
+var MockWriter = majorupgrade_utils.NewCustomWriterWithInputs("y")
 
 func TestTrimSliceSpace(t *testing.T) {
 	testArr := []string{TEST_IP_1 + " ", " " + TEST_IP_3 + " "}
@@ -183,7 +190,8 @@ func TestIsFinalInstanceCountAllowed(t *testing.T) {
 }
 
 func TestMoveAWSAutoTfvarsFileAllExist(t *testing.T) {
-	nodeUtil := NewNodeUtils()
+
+	nodeUtil := NewNodeUtils(NewRemoteCmdExecutorWithoutNodeMap(NewSSHUtil(&SSHConfig{}), MockWriter.CliWriter), command.NewMockExecutor(t), MockWriter.CliWriter)
 	dir := t.TempDir()
 	_, err := os.Create(filepath.Join(dir, AWS_AUTO_TFVARS))
 	assert.NoError(t, err)
@@ -195,7 +203,7 @@ func TestMoveAWSAutoTfvarsFileAllExist(t *testing.T) {
 }
 
 func TestMoveAWSAutoTfvarsFileNotExist(t *testing.T) {
-	nodeUtil := NewNodeUtils()
+	nodeUtil := NewNodeUtils(NewRemoteCmdExecutorWithoutNodeMap(NewSSHUtil(&SSHConfig{}), MockWriter.CliWriter), command.NewMockExecutor(t), MockWriter.CliWriter)
 	dir := t.TempDir()
 
 	err := os.MkdirAll(filepath.Join(dir, DESTROY_AWS_FOLDER), os.ModePerm)
@@ -207,7 +215,7 @@ func TestMoveAWSAutoTfvarsFileNotExist(t *testing.T) {
 }
 
 func TestMoveAWSAutoTfvarsDestroyFolderNotExist(t *testing.T) {
-	nodeUtil := NewNodeUtils()
+	nodeUtil := NewNodeUtils(NewRemoteCmdExecutorWithoutNodeMap(NewSSHUtil(&SSHConfig{}), MockWriter.CliWriter), command.NewMockExecutor(t), MockWriter.CliWriter)
 	dir := t.TempDir()
 
 	_, err := os.Create(filepath.Join(dir, AWS_AUTO_TFVARS))
@@ -220,7 +228,7 @@ func TestMoveAWSAutoTfvarsDestroyFolderNotExist(t *testing.T) {
 }
 
 func TestModifyTfArchFile(t *testing.T) {
-	nodeUtil := NewNodeUtils()
+	nodeUtil := NewNodeUtils(NewRemoteCmdExecutorWithoutNodeMap(NewSSHUtil(&SSHConfig{}), MockWriter.CliWriter), command.NewMockExecutor(t), MockWriter.CliWriter)
 	dir := t.TempDir()
 	_, err := os.Create(filepath.Join(dir, TF_ARCH_FILE))
 	assert.NoError(t, err)
@@ -233,8 +241,312 @@ func TestModifyTfArchFile(t *testing.T) {
 }
 
 func TestModifyTfArchFileNotExist(t *testing.T) {
-	nodeUtil := NewNodeUtils()
+	nodeUtil := NewNodeUtils(NewRemoteCmdExecutorWithoutNodeMap(NewSSHUtil(&SSHConfig{}), MockWriter.CliWriter), command.NewMockExecutor(t), MockWriter.CliWriter)
 	dir := t.TempDir()
 	err := nodeUtil.modifyTfArchFile(dir)
 	assert.Error(t, err)
 }
+
+func TestStopServicesOnNodeA2(t *testing.T) {
+	mockUtil := &MockNodeUtilsImpl{
+		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
+			infra := &AutomateHAInfraDetails{}
+			infra.Outputs.AutomatePrivateIps.Value = []string{TEST_IP_1}
+			return infra, &SSHConfig{}, nil
+		},
+		excludeOpenSearchNodeFunc: func(ipToDelete string, infra *AutomateHAInfraDetails) error {
+			return nil
+		},
+		checkExistingExcludedOSNodesFunc: func(automateIp string, infra *AutomateHAInfraDetails) (string, error) {
+			return "", nil
+		},
+	}
+
+	infra, _, err := mockUtil.getHaInfraDetails()
+	assert.NoError(t, err)
+
+	nodeUtil := NewNodeUtils(&MockRemoteCmdExecutor{
+		ExecuteFunc: func() (map[string][]*CmdResult, error) {
+			return nil, nil
+		},
+		ExecuteWithNodeMapFunc: func(nodeMap *NodeTypeAndCmd) (map[string][]*CmdResult, error) {
+			return nil, nil
+		},
+		GetSshUtilFunc: func() SSHUtil {
+			return &MockSSHUtilsImpl{
+				connectAndExecuteCommandOnRemoteFunc: func(remoteCommands string, spinner bool) (string, error) {
+					return "", nil
+				},
+			}
+		},
+	}, command.NewMockExecutor(t), MockWriter.CliWriter)
+	err = nodeUtil.stopServicesOnNode(TEST_IP_1, AUTOMATE, EXISTING_INFRA_MODE, infra)
+	assert.NoError(t, err)
+}
+
+func TestStopServicesOnNodeA2AWS(t *testing.T) {
+	mockUtil := &MockNodeUtilsImpl{
+		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
+			infra := &AutomateHAInfraDetails{}
+			infra.Outputs.AutomatePrivateIps.Value = []string{TEST_IP_1}
+			return infra, &SSHConfig{}, nil
+		},
+		excludeOpenSearchNodeFunc: func(ipToDelete string, infra *AutomateHAInfraDetails) error {
+			return nil
+		},
+		checkExistingExcludedOSNodesFunc: func(automateIp string, infra *AutomateHAInfraDetails) (string, error) {
+			return "", nil
+		},
+	}
+
+	infra, _, err := mockUtil.getHaInfraDetails()
+	assert.NoError(t, err)
+
+	nodeUtil := NewNodeUtils(&MockRemoteCmdExecutor{
+		ExecuteFunc: func() (map[string][]*CmdResult, error) {
+			return nil, nil
+		},
+		ExecuteWithNodeMapFunc: func(nodeMap *NodeTypeAndCmd) (map[string][]*CmdResult, error) {
+			return nil, nil
+		},
+		GetSshUtilFunc: func() SSHUtil {
+			return &MockSSHUtilsImpl{
+				connectAndExecuteCommandOnRemoteFunc: func(remoteCommands string, spinner bool) (string, error) {
+					return "", nil
+				},
+			}
+		},
+	}, command.NewMockExecutor(t), MockWriter.CliWriter)
+	err = nodeUtil.stopServicesOnNode(TEST_IP_1, AUTOMATE, AWS_MODE, infra)
+	assert.NoError(t, err)
+}
+
+func TestStopServicesOnNodeCS(t *testing.T) {
+	mockUtil := &MockNodeUtilsImpl{
+		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
+			infra := &AutomateHAInfraDetails{}
+			infra.Outputs.AutomatePrivateIps.Value = []string{TEST_IP_1}
+			return infra, &SSHConfig{}, nil
+		},
+		excludeOpenSearchNodeFunc: func(ipToDelete string, infra *AutomateHAInfraDetails) error {
+			return nil
+		},
+		checkExistingExcludedOSNodesFunc: func(automateIp string, infra *AutomateHAInfraDetails) (string, error) {
+			return "", nil
+		},
+	}
+
+	infra, _, err := mockUtil.getHaInfraDetails()
+	assert.NoError(t, err)
+
+	nodeUtil := NewNodeUtils(&MockRemoteCmdExecutor{
+		ExecuteFunc: func() (map[string][]*CmdResult, error) {
+			return nil, nil
+		},
+		ExecuteWithNodeMapFunc: func(nodeMap *NodeTypeAndCmd) (map[string][]*CmdResult, error) {
+			return nil, nil
+		},
+		GetSshUtilFunc: func() SSHUtil {
+			return &MockSSHUtilsImpl{
+				connectAndExecuteCommandOnRemoteFunc: func(remoteCommands string, spinner bool) (string, error) {
+					return "", nil
+				},
+			}
+		},
+	}, command.NewMockExecutor(t), MockWriter.CliWriter)
+	err = nodeUtil.stopServicesOnNode(TEST_IP_1, CHEF_SERVER, EXISTING_INFRA_MODE, infra)
+	assert.NoError(t, err)
+}
+func TestStopServicesOnNodePG(t *testing.T) {
+	mockUtil := &MockNodeUtilsImpl{
+		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
+			infra := &AutomateHAInfraDetails{}
+			infra.Outputs.AutomatePrivateIps.Value = []string{TEST_IP_1}
+			return infra, &SSHConfig{}, nil
+		},
+		excludeOpenSearchNodeFunc: func(ipToDelete string, infra *AutomateHAInfraDetails) error {
+			return nil
+		},
+		checkExistingExcludedOSNodesFunc: func(automateIp string, infra *AutomateHAInfraDetails) (string, error) {
+			return "", nil
+		},
+	}
+
+	infra, _, err := mockUtil.getHaInfraDetails()
+	assert.NoError(t, err)
+
+	nodeUtil := NewNodeUtils(&MockRemoteCmdExecutor{
+		ExecuteFunc: func() (map[string][]*CmdResult, error) {
+			return nil, nil
+		},
+		ExecuteWithNodeMapFunc: func(nodeMap *NodeTypeAndCmd) (map[string][]*CmdResult, error) {
+			return nil, nil
+		},
+		GetSshUtilFunc: func() SSHUtil {
+			return &MockSSHUtilsImpl{
+				connectAndExecuteCommandOnRemoteFunc: func(remoteCommands string, spinner bool) (string, error) {
+					return "", nil
+				},
+			}
+		},
+	}, command.NewMockExecutor(t), MockWriter.CliWriter)
+	err = nodeUtil.stopServicesOnNode(TEST_IP_1, POSTGRESQL, EXISTING_INFRA_MODE, infra)
+	assert.NoError(t, err)
+}
+func TestStopServicesOnNodeOS(t *testing.T) {
+	mockUtil := &MockNodeUtilsImpl{
+		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
+			infra := &AutomateHAInfraDetails{}
+			infra.Outputs.AutomatePrivateIps.Value = []string{TEST_IP_1}
+			return infra, &SSHConfig{}, nil
+		},
+		excludeOpenSearchNodeFunc: func(ipToDelete string, infra *AutomateHAInfraDetails) error {
+			return nil
+		},
+		checkExistingExcludedOSNodesFunc: func(automateIp string, infra *AutomateHAInfraDetails) (string, error) {
+			return "", nil
+		},
+	}
+
+	infra, _, err := mockUtil.getHaInfraDetails()
+	assert.NoError(t, err)
+
+	nodeUtil := NewNodeUtils(&MockRemoteCmdExecutor{
+		ExecuteFunc: func() (map[string][]*CmdResult, error) {
+			return nil, nil
+		},
+		ExecuteWithNodeMapFunc: func(nodeMap *NodeTypeAndCmd) (map[string][]*CmdResult, error) {
+			//return dummy result
+			return map[string][]*CmdResult{
+				TEST_IP_1: {
+					{
+						ScriptName:  "",
+						HostIP:      "",
+						OutputFiles: []string{},
+						Output:      "",
+						Error:       nil,
+					},
+				},
+			}, nil
+		},
+		GetSshUtilFunc: func() SSHUtil {
+			return &MockSSHUtilsImpl{
+				connectAndExecuteCommandOnRemoteFunc: func(remoteCommands string, spinner bool) (string, error) {
+					return "", nil
+				},
+			}
+		},
+	}, command.NewMockExecutor(t), MockWriter.CliWriter)
+	err = nodeUtil.stopServicesOnNode(TEST_IP_1, OPENSEARCH, EXISTING_INFRA_MODE, infra)
+	assert.NoError(t, err)
+}
+
+func TestStopServicesOnNodeOSAWS(t *testing.T) {
+	mockUtil := &MockNodeUtilsImpl{
+		getHaInfraDetailsfunc: func() (*AutomateHAInfraDetails, *SSHConfig, error) {
+			infra := &AutomateHAInfraDetails{}
+			infra.Outputs.AutomatePrivateIps.Value = []string{TEST_IP_1}
+			return infra, &SSHConfig{}, nil
+		},
+		excludeOpenSearchNodeFunc: func(ipToDelete string, infra *AutomateHAInfraDetails) error {
+			return nil
+		},
+		checkExistingExcludedOSNodesFunc: func(automateIp string, infra *AutomateHAInfraDetails) (string, error) {
+			return "", nil
+		},
+	}
+
+	infra, _, err := mockUtil.getHaInfraDetails()
+	assert.NoError(t, err)
+
+	nodeUtil := NewNodeUtils(&MockRemoteCmdExecutor{
+		ExecuteFunc: func() (map[string][]*CmdResult, error) {
+			return nil, nil
+		},
+		ExecuteWithNodeMapFunc: func(nodeMap *NodeTypeAndCmd) (map[string][]*CmdResult, error) {
+			//return dummy result
+			return map[string][]*CmdResult{
+				TEST_IP_1: {
+					{
+						ScriptName:  "",
+						HostIP:      "",
+						OutputFiles: []string{},
+						Output:      "",
+						Error:       nil,
+					},
+				},
+			}, nil
+		}}, command.NewMockExecutor(t), MockWriter.CliWriter)
+	err = nodeUtil.stopServicesOnNode(TEST_IP_1, OPENSEARCH, AWS_MODE, infra)
+	assert.NoError(t, err)
+}
+
+func TestStopServicesOnNodeInvalidNodeType(t *testing.T) {
+	nodeUtil := NewNodeUtils(nil, command.NewMockExecutor(t), MockWriter.CliWriter)
+	err := nodeUtil.stopServicesOnNode(TEST_IP_1, "invalid", EXISTING_INFRA_MODE, &AutomateHAInfraDetails{})
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "Invalid node type")
+}
+
+func TestCalculateTotalInstanceCount(t *testing.T) {
+	nodeUtil := NewNodeUtils(nil, &command.MockExecutorImpl{
+		CombinedOutputFunc: func(cmd string, opts ...command.Opt) (string, error) {
+			return "8\n", nil
+		},
+	}, MockWriter.CliWriter)
+	count, err := nodeUtil.calculateTotalInstanceCount()
+	assert.NoError(t, err)
+	assert.Equal(t, 8, count)
+}
+
+func TestCalculateTotalInstanceCountCombineOutputError(t *testing.T) {
+	nodeUtil := NewNodeUtils(nil, &command.MockExecutorImpl{
+		CombinedOutputFunc: func(cmd string, opts ...command.Opt) (string, error) {
+			return "", errors.New("random error")
+		},
+	}, MockWriter.CliWriter)
+	count, err := nodeUtil.calculateTotalInstanceCount()
+	assert.ErrorContains(t, err, "error")
+	assert.Equal(t, -1, count)
+}
+
+func TestCalculateTotalInstanceCountAtoiError(t *testing.T) {
+	nodeUtil := NewNodeUtils(nil, &command.MockExecutorImpl{
+		CombinedOutputFunc: func(cmd string, opts ...command.Opt) (string, error) {
+			return "abc", nil
+		},
+	}, MockWriter.CliWriter)
+	count, err := nodeUtil.calculateTotalInstanceCount()
+	assert.ErrorContains(t, err, "invalid syntax")
+	assert.Equal(t, -1, count)
+}
+
+func TestGetIPsFromOSClusterResponsePersistent(t *testing.T) {
+	input := `{"persistent":{"cluster":{"routing":{"allocation":{"exclude":{"_ip":"192.0.2.11"}}}},"plugins":{"index_state_management":{"template_migration":{"control":"-1"}}}},"transient":{}}`
+	out := getIPsFromOSClusterResponse(input)
+	assert.Equal(t, TEST_IP_1, out)
+}
+
+func TestGetIPsFromOSClusterResponsePersistentMultiple(t *testing.T) {
+	input := `{"persistent":{"cluster":{"routing":{"allocation":{"exclude":{"_ip":"192.0.2.11,192.0.2.12,192.0.2.13"}}}},"plugins":{"index_state_management":{"template_migration":{"control":"-1"}}}},"transient":{}}`
+	out := getIPsFromOSClusterResponse(input)
+	assert.Equal(t, "192.0.2.11,192.0.2.12,192.0.2.13", out)
+}
+
+func TestGetIPsFromOSClusterResponseTransient(t *testing.T) {
+	input := `{"persistent":{"plugins":{"index_state_management":{"template_migration":{"control":"-1"}}}},"transient":{"cluster":{"routing":{"allocation":{"exclude":{"_ip":"192.0.2.11"}}}}}}`
+	out := getIPsFromOSClusterResponse(input)
+	assert.Equal(t, TEST_IP_1, out)
+}
+
+func TestGetIPsFromOSClusterResponseTransientEmpty(t *testing.T) {
+	input := `{"persistent":{"plugins":{"index_state_management":{"template_migration":{"control":"-1"}}}},"transient":{"cluster":{"routing":{"allocation":{"exclude":{"_ip":""}}}}}}`
+	out := getIPsFromOSClusterResponse(input)
+	assert.Equal(t, "", out)
+}
+
+func TestGetIPsFromOSClusterResponseNotFound(t *testing.T) {
+	input := `{"persistent":{"plugins":{"index_state_management":{"template_migration":{"control":"-1"}}}},"transient":{}}`
+	out := getIPsFromOSClusterResponse(input)
+	assert.Equal(t, "", out)
+}
diff --git a/components/automate-cli/pkg/testfiles/aws/config.toml b/components/automate-cli/pkg/testfiles/aws/config.toml
index 819aef778..5874c9cbd 100644
--- a/components/automate-cli/pkg/testfiles/aws/config.toml
+++ b/components/automate-cli/pkg/testfiles/aws/config.toml
@@ -44,7 +44,7 @@ private_key = "test_pri_key"
 [chef_server.config]
 ## === INPUT NEEDED ===
 # No. of Chef Server Frontend Machine or VM eg.: instance_count = "2"
-instance_count = "1"
+instance_count = "2"
 # Add Chef Server load balancer root-ca and keys
 enable_custom_certs = true
 public_key = "test_pub_key"
@@ -56,7 +56,7 @@ private_key = "test_pri_key"
 [opensearch.config]
 ## === INPUT NEEDED ===
 # No. of OpenSearch DB Backend Machine or VM eg.: instance_count = "3"
-instance_count = "3"
+instance_count = "6"
 # Add OpenSearch load balancer root-ca and keys
 enable_custom_certs = true
 root_ca = "test_root_ca"
diff --git a/lib/stringutils/strings_test.go b/lib/stringutils/strings_test.go
index 60b9dced2..cb5687a55 100644
--- a/lib/stringutils/strings_test.go
+++ b/lib/stringutils/strings_test.go
@@ -25,3 +25,11 @@ func TestTitleSplit(t *testing.T) {
 	assert.Equal(t, "Foo Bar", stringutils.TitleSplit("foo-bar", "-"))
 	assert.Equal(t, "Foo", stringutils.TitleSplit("foo", "-"))
 }
+
+func TestTitleReplace(t *testing.T) {
+	assert.Equal(t, "Hello World", stringutils.TitleReplace("hello_world", "_", " "))
+	assert.Equal(t, "Hello-World", stringutils.TitleReplace("hello_world", "_", "-"))
+	assert.Equal(t, "Helloworld", stringutils.TitleReplace("helloworld", "_", "-"))
+	assert.Equal(t, "Helloworld", stringutils.TitleReplace("helloworld", "-", " "))
+	assert.Equal(t, "Hello Chef", stringutils.TitleReplace("hello chef", "_", "-"))
+}