Skip to content

Commit 516f1f4

Browse files
committed
Update provision certificate command to allow provisioning via SSH
1 parent c23231f commit 516f1f4

File tree

8 files changed

+201
-72
lines changed

8 files changed

+201
-72
lines changed

api/src/main/java/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ public class ProvisionCertificateCmd extends BaseAsyncCmd {
6363
description = "Name of the CA service provider, otherwise the default configured provider plugin will be used")
6464
private String provider;
6565

66+
@Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN,
67+
description = "When true, uses SSH to re-provision the agent's certificate, bypassing the NIO agent connection. " +
68+
"Use this when agents are disconnected due to a CA change. Supported for KVM hosts and SystemVMs. Default is false",
69+
since = "4.23.0")
70+
private Boolean forced;
71+
6672
/////////////////////////////////////////////////////
6773
/////////////////// Accessors ///////////////////////
6874
/////////////////////////////////////////////////////
@@ -79,6 +85,10 @@ public String getProvider() {
7985
return provider;
8086
}
8187

88+
public boolean isForced() {
89+
return forced != null && forced;
90+
}
91+
8292
/////////////////////////////////////////////////////
8393
/////////////// API Implementation///////////////////
8494
/////////////////////////////////////////////////////
@@ -90,7 +100,7 @@ public void execute() {
90100
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to find host by ID: " + getHostId());
91101
}
92102

93-
boolean result = caManager.provisionCertificate(host, getReconnect(), getProvider());
103+
boolean result = caManager.provisionCertificate(host, getReconnect(), getProvider(), isForced());
94104
SuccessResponse response = new SuccessResponse(getCommandName());
95105
response.setSuccess(result);
96106
setResponseObject(response);

api/src/main/java/org/apache/cloudstack/ca/CAManager.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.List;
2424
import java.util.Map;
2525

26+
import com.trilead.ssh2.Connection;
27+
2628
import org.apache.cloudstack.framework.ca.CAProvider;
2729
import org.apache.cloudstack.framework.ca.CAService;
2830
import org.apache.cloudstack.framework.ca.Certificate;
@@ -139,12 +141,25 @@ public interface CAManager extends CAService, Configurable, PluggableService {
139141
boolean revokeCertificate(final BigInteger certSerial, final String certCn, final String provider);
140142

141143
/**
142-
* Provisions certificate for given active and connected agent host
144+
* Provisions certificate for given agent host.
145+
* When forced=true, uses SSH to re-provision bypassing the NIO agent connection (for disconnected agents).
143146
* @param host
147+
* @param reconnect
144148
* @param provider
149+
* @param forced when true, provisions via SSH instead of NIO; supports KVM hosts and SystemVMs
145150
* @return returns success/failure as boolean
146151
*/
147-
boolean provisionCertificate(final Host host, final Boolean reconnect, final String provider);
152+
boolean provisionCertificate(final Host host, final Boolean reconnect, final String provider, final boolean forced);
153+
154+
/**
155+
* Provisions certificate for a KVM host using an existing SSH connection.
156+
* Runs keystore-setup to generate a CSR, issues a certificate, then runs keystore-cert-import.
157+
* Used during host discovery and for forced re-provisioning when the NIO agent is unreachable.
158+
* @param sshConnection active SSH connection to the KVM host
159+
* @param agentIp IP address of the KVM host agent
160+
* @param agentHostname hostname of the KVM host agent
161+
*/
162+
void provisionCertificateViaSsh(Connection sshConnection, String agentIp, String agentHostname);
148163

149164
/**
150165
* Setups up a new keystore and generates CSR for a host

scripts/util/keystore-cert-import

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ elif [ ! -f "$CACERT_FILE" ]; then
7070
fi
7171

7272
# Import cacerts into the keystore
73-
awk '/-----BEGIN CERTIFICATE-----?/{n++}{print > "cloudca." n }' "$CACERT_FILE"
74-
for caChain in $(ls cloudca.*); do
73+
awk 'BEGIN{n=0} /-----BEGIN CERTIFICATE-----/{n++}{print > "cloudca." n }' "$CACERT_FILE"
74+
for caChain in $(ls cloudca.* 2>/dev/null); do
7575
keytool -delete -noprompt -alias "$caChain" -keystore "$KS_FILE" -storepass "$KS_PASS" > /dev/null 2>&1 || true
7676
keytool -import -noprompt -storepass "$KS_PASS" -trustcacerts -alias "$caChain" -file "$caChain" -keystore "$KS_FILE" > /dev/null 2>&1
7777
done
@@ -143,7 +143,7 @@ if [ -f "$SYSTEM_FILE" ]; then
143143
REALHOSTIP_KS_FILE="$(dirname $(dirname $PROPS_FILE))/certs/realhostip.keystore"
144144
REALHOSTIP_PASS="vmops.com"
145145
if [ -f "$REALHOSTIP_KS_FILE" ]; then
146-
awk '/-----BEGIN CERTIFICATE-----/{n++}{print > "cloudca." n }' "$CACERT_FILE"
146+
awk 'BEGIN{n=0} /-----BEGIN CERTIFICATE-----/{n++}{print > "cloudca." n }' "$CACERT_FILE"
147147
for caChain in $(ls cloudca.* 2>/dev/null); do
148148
keytool -delete -noprompt -alias "$caChain" -keystore "$REALHOSTIP_KS_FILE" \
149149
-storepass "$REALHOSTIP_PASS" > /dev/null 2>&1 || true

server/src/main/java/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.net.InetAddress;
2222
import java.net.URI;
2323
import java.util.Arrays;
24-
import java.util.Collections;
2524
import java.util.HashMap;
2625
import java.util.HashSet;
2726
import java.util.List;
@@ -32,11 +31,8 @@
3231

3332
import org.apache.cloudstack.agent.lb.IndirectAgentLB;
3433
import org.apache.cloudstack.ca.CAManager;
35-
import org.apache.cloudstack.ca.SetupCertificateCommand;
3634
import org.apache.cloudstack.direct.download.DirectDownloadManager;
37-
import org.apache.cloudstack.framework.ca.Certificate;
3835
import org.apache.cloudstack.utils.cache.LazyCache;
39-
import org.apache.cloudstack.utils.security.KeyStoreUtils;
4036

4137
import com.cloud.agent.AgentManager;
4238
import com.cloud.agent.Listener;
@@ -66,7 +62,6 @@
6662
import com.cloud.resource.ResourceStateAdapter;
6763
import com.cloud.resource.ServerResource;
6864
import com.cloud.resource.UnableDeleteHostException;
69-
import com.cloud.utils.PasswordGenerator;
7065
import com.cloud.utils.StringUtils;
7166
import com.cloud.utils.UuidUtils;
7267
import com.cloud.utils.exception.CloudRuntimeException;
@@ -174,55 +169,7 @@ private void setupAgentSecurity(final Connection sshConnection, final String age
174169
throw new CloudRuntimeException("Cannot secure agent communication because SSH connection is invalid for host IP=" + agentIp);
175170
}
176171

177-
Integer validityPeriod = CAManager.CertValidityPeriod.value();
178-
if (validityPeriod < 1) {
179-
validityPeriod = 1;
180-
}
181-
182-
String keystorePassword = PasswordGenerator.generateRandomPassword(16);
183-
final SSHCmdHelper.SSHCmdResult keystoreSetupResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection,
184-
String.format("sudo /usr/share/cloudstack-common/scripts/util/%s " +
185-
"/etc/cloudstack/agent/agent.properties " +
186-
"/etc/cloudstack/agent/%s " +
187-
"%s %d " +
188-
"/etc/cloudstack/agent/%s",
189-
KeyStoreUtils.KS_SETUP_SCRIPT,
190-
KeyStoreUtils.KS_FILENAME,
191-
keystorePassword,
192-
validityPeriod,
193-
KeyStoreUtils.CSR_FILENAME));
194-
195-
if (!keystoreSetupResult.isSuccess()) {
196-
throw new CloudRuntimeException("Failed to setup keystore on the KVM host: " + agentIp);
197-
}
198-
199-
final Certificate certificate = caManager.issueCertificate(keystoreSetupResult.getStdOut(), Arrays.asList(agentHostname, agentIp), Collections.singletonList(agentIp), null, null);
200-
if (certificate == null || certificate.getClientCertificate() == null) {
201-
throw new CloudRuntimeException("Failed to issue certificates for KVM host agent: " + agentIp);
202-
}
203-
204-
final SetupCertificateCommand certificateCommand = new SetupCertificateCommand(certificate);
205-
final SSHCmdHelper.SSHCmdResult setupCertResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection,
206-
String.format("sudo /usr/share/cloudstack-common/scripts/util/%s " +
207-
"/etc/cloudstack/agent/agent.properties %s " +
208-
"/etc/cloudstack/agent/%s %s " +
209-
"/etc/cloudstack/agent/%s \"%s\" " +
210-
"/etc/cloudstack/agent/%s \"%s\" " +
211-
"/etc/cloudstack/agent/%s \"%s\"",
212-
KeyStoreUtils.KS_IMPORT_SCRIPT,
213-
keystorePassword,
214-
KeyStoreUtils.KS_FILENAME,
215-
KeyStoreUtils.SSH_MODE,
216-
KeyStoreUtils.CERT_FILENAME,
217-
certificateCommand.getEncodedCertificate(),
218-
KeyStoreUtils.CACERT_FILENAME,
219-
certificateCommand.getEncodedCaCertificates(),
220-
KeyStoreUtils.PKEY_FILENAME,
221-
certificateCommand.getEncodedPrivateKey()));
222-
223-
if (setupCertResult != null && !setupCertResult.isSuccess()) {
224-
throw new CloudRuntimeException("Failed to setup certificate in the KVM agent's keystore file, please see logs and configure manually!");
225-
}
172+
caManager.provisionCertificateViaSsh(sshConnection, agentIp, agentHostname);
226173

227174
if (logger.isDebugEnabled()) {
228175
logger.debug("Succeeded to import certificate in the keystore for agent on the KVM host: " + agentIp + ". Agent secured and trusted.");

0 commit comments

Comments
 (0)