Skip to content

Commit efe35d3

Browse files
committed
Add marvin tests and command to revoke certificates
1 parent fc9e153 commit efe35d3

File tree

8 files changed

+519
-21
lines changed

8 files changed

+519
-21
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.api.command.admin.direct.download;
18+
19+
import com.cloud.exception.ConcurrentOperationException;
20+
import com.cloud.exception.InsufficientCapacityException;
21+
import com.cloud.exception.NetworkRuleConflictException;
22+
import com.cloud.exception.ResourceAllocationException;
23+
import com.cloud.exception.ResourceUnavailableException;
24+
import org.apache.cloudstack.acl.RoleType;
25+
import org.apache.cloudstack.api.APICommand;
26+
import org.apache.cloudstack.api.ApiConstants;
27+
import org.apache.cloudstack.api.ApiErrorCode;
28+
import org.apache.cloudstack.api.BaseCmd;
29+
import org.apache.cloudstack.api.Parameter;
30+
import org.apache.cloudstack.api.ServerApiException;
31+
import org.apache.cloudstack.api.response.SuccessResponse;
32+
import org.apache.cloudstack.context.CallContext;
33+
import org.apache.cloudstack.direct.download.DirectDownloadManager;
34+
import org.apache.log4j.Logger;
35+
36+
import javax.inject.Inject;
37+
38+
@APICommand(name = RevokeTemplateDirectDownloadCertificateCmd.APINAME,
39+
description = "Revoke a certificate alias from a KVM host",
40+
responseObject = SuccessResponse.class,
41+
requestHasSensitiveInfo = true,
42+
responseHasSensitiveInfo = true,
43+
since = "4.11.3",
44+
authorized = {RoleType.Admin})
45+
public class RevokeTemplateDirectDownloadCertificateCmd extends BaseCmd {
46+
47+
@Inject
48+
DirectDownloadManager directDownloadManager;
49+
50+
private static final Logger LOG = Logger.getLogger(RevokeTemplateDirectDownloadCertificateCmd.class);
51+
public static final String APINAME = "revokeTemplateDirectDownloadCertificate";
52+
53+
@Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, required = true,
54+
description = "alias of the SSL certificate")
55+
private String certificateAlias;
56+
57+
@Parameter(name = ApiConstants.HYPERVISOR, type = BaseCmd.CommandType.STRING, required = true,
58+
description = "Hypervisor type")
59+
private String hypervisor;
60+
61+
@Override
62+
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
63+
if (!hypervisor.equalsIgnoreCase("kvm")) {
64+
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Currently supporting KVM hosts only");
65+
}
66+
SuccessResponse response = new SuccessResponse(getCommandName());
67+
try {
68+
LOG.debug("Revoking certificate " + certificateAlias + " from " + hypervisor + " hosts");
69+
boolean result = directDownloadManager.revokeCertificateAlias(certificateAlias, hypervisor);
70+
response.setSuccess(result);
71+
setResponseObject(response);
72+
} catch (Exception e) {
73+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
74+
}
75+
}
76+
77+
@Override
78+
public String getCommandName() {
79+
return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
80+
}
81+
82+
@Override
83+
public long getEntityOwnerId() {
84+
return CallContext.current().getCallingAccount().getId();
85+
}
86+
}

api/src/org/apache/cloudstack/direct/download/DirectDownloadManager.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@
2222

2323
public interface DirectDownloadManager extends DirectDownloadService, PluggableService {
2424

25+
/**
26+
* Revoke direct download certificate with alias 'alias' from hosts of hypervisor type 'hypervisor'
27+
*/
28+
boolean revokeCertificateAlias(String certificateAlias, String hypervisor);
2529
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// Licensed to the Apache Software Foundation (ASF) under one
3+
// or more contributor license agreements. See the NOTICE file
4+
// distributed with this work for additional information
5+
// regarding copyright ownership. The ASF licenses this file
6+
// to you under the Apache License, Version 2.0 (the
7+
// "License"); you may not use this file except in compliance
8+
// with the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing,
13+
// software distributed under the License is distributed on an
14+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
// KIND, either express or implied. See the License for the
16+
// specific language governing permissions and limitations
17+
// under the License.
18+
//
19+
package org.apache.cloudstack.agent.directdownload;
20+
21+
import com.cloud.agent.api.Command;
22+
23+
public class RevokeDirectDownloadCertificateCommand extends Command {
24+
25+
private String certificateAlias;
26+
27+
public RevokeDirectDownloadCertificateCommand(final String alias) {
28+
this.certificateAlias = alias;
29+
}
30+
31+
public String getCertificateAlias() {
32+
return certificateAlias;
33+
}
34+
35+
@Override
36+
public boolean executeInSequence() {
37+
return false;
38+
}
39+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//
2+
// Licensed to the Apache Software Foundation (ASF) under one
3+
// or more contributor license agreements. See the NOTICE file
4+
// distributed with this work for additional information
5+
// regarding copyright ownership. The ASF licenses this file
6+
// to you under the Apache License, Version 2.0 (the
7+
// "License"); you may not use this file except in compliance
8+
// with the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing,
13+
// software distributed under the License is distributed on an
14+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
// KIND, either express or implied. See the License for the
16+
// specific language governing permissions and limitations
17+
// under the License.
18+
//
19+
20+
package com.cloud.hypervisor.kvm.resource.wrapper;
21+
22+
import com.cloud.agent.api.Answer;
23+
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
24+
import com.cloud.resource.CommandWrapper;
25+
import com.cloud.resource.ResourceWrapper;
26+
import com.cloud.utils.PropertiesUtil;
27+
import com.cloud.utils.exception.CloudRuntimeException;
28+
import com.cloud.utils.script.Script;
29+
import org.apache.cloudstack.agent.directdownload.RevokeDirectDownloadCertificateCommand;
30+
import org.apache.cloudstack.utils.security.KeyStoreUtils;
31+
import org.apache.log4j.Logger;
32+
33+
import java.io.File;
34+
import java.io.FileNotFoundException;
35+
36+
import static org.apache.commons.lang.StringUtils.isBlank;
37+
38+
@ResourceWrapper(handles = RevokeDirectDownloadCertificateCommand.class)
39+
public class LibvirtRevokeDirectDownloadCertificateWrapper extends CommandWrapper<RevokeDirectDownloadCertificateCommand, Answer, LibvirtComputingResource> {
40+
41+
private static final Logger s_logger = Logger.getLogger(LibvirtRevokeDirectDownloadCertificateWrapper.class);
42+
43+
/**
44+
* Retrieve agent.properties file
45+
*/
46+
private File getAgentPropertiesFile() throws FileNotFoundException {
47+
final File agentFile = PropertiesUtil.findConfigFile("agent.properties");
48+
if (agentFile == null) {
49+
throw new FileNotFoundException("Failed to find agent.properties file");
50+
}
51+
return agentFile;
52+
}
53+
54+
/**
55+
* Get the property 'keystore.passphrase' value from agent.properties file
56+
*/
57+
private String getKeystorePassword(File agentFile) {
58+
s_logger.debug("Retrieving keystore password from agent.properties file");
59+
String privatePasswordFormat = "sed -n '/keystore.passphrase/p' '%s' 2>/dev/null | sed 's/keystore.passphrase=//g' 2>/dev/null";
60+
String privatePasswordCmd = String.format(privatePasswordFormat, agentFile.getAbsolutePath());
61+
return Script.runSimpleBashScript(privatePasswordCmd);
62+
}
63+
64+
/**
65+
* Get keystore path
66+
*/
67+
private String getKeyStoreFilePath(File agentFile) {
68+
return agentFile.getParent() + "/" + KeyStoreUtils.KS_FILENAME;
69+
}
70+
71+
@Override
72+
public Answer execute(RevokeDirectDownloadCertificateCommand command, LibvirtComputingResource serverResource) {
73+
String certificateAlias = command.getCertificateAlias();
74+
try {
75+
File agentFile = getAgentPropertiesFile();
76+
String privatePassword = getKeystorePassword(agentFile);
77+
if (isBlank(privatePassword)) {
78+
return new Answer(command, false, "No password found for keystore: " + KeyStoreUtils.KS_FILENAME);
79+
}
80+
81+
final String keyStoreFile = getKeyStoreFilePath(agentFile);
82+
83+
String checkCmd = String.format("keytool -list -alias %s -keystore %s -storepass %s",
84+
certificateAlias, keyStoreFile, privatePassword);
85+
int existsCmdResult = Script.runSimpleBashScriptForExitValue(checkCmd);
86+
if (existsCmdResult == 1) {
87+
s_logger.debug("Certificate alias " + certificateAlias + " does not exist, no need to revoke it");
88+
} else {
89+
String revokeCmd = String.format("keytool -delete -alias %s -keystore %s -storepass %s",
90+
certificateAlias, keyStoreFile, privatePassword);
91+
s_logger.debug("Revoking certificate alias " + certificateAlias + " from keystore " + keyStoreFile);
92+
Script.runSimpleBashScriptForExitValue(revokeCmd);
93+
}
94+
} catch (FileNotFoundException | CloudRuntimeException e) {
95+
s_logger.error("Error while setting up certificate " + certificateAlias, e);
96+
return new Answer(command, false, e.getMessage());
97+
}
98+
return new Answer(command);
99+
}
100+
}

server/src/com/cloud/server/ManagementServerImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import org.apache.cloudstack.api.command.admin.config.ListHypervisorCapabilitiesCmd;
6666
import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd;
6767
import org.apache.cloudstack.api.command.admin.config.UpdateHypervisorCapabilitiesCmd;
68+
import org.apache.cloudstack.api.command.admin.direct.download.RevokeTemplateDirectDownloadCertificateCmd;
6869
import org.apache.cloudstack.api.command.admin.direct.download.UploadTemplateDirectDownloadCertificateCmd;
6970
import org.apache.cloudstack.api.command.admin.domain.CreateDomainCmd;
7071
import org.apache.cloudstack.api.command.admin.domain.DeleteDomainCmd;
@@ -3052,6 +3053,7 @@ public List<Class<?>> getCommands() {
30523053
cmdList.add(CreateManagementNetworkIpRangeCmd.class);
30533054
cmdList.add(DeleteManagementNetworkIpRangeCmd.class);
30543055
cmdList.add(UploadTemplateDirectDownloadCertificateCmd.class);
3056+
cmdList.add(RevokeTemplateDirectDownloadCertificateCmd.class);
30553057

30563058
// Out-of-band management APIs for admins
30573059
cmdList.add(EnableOutOfBandManagementForHostCmd.class);

server/src/org/apache/cloudstack/direct/download/DirectDownloadManagerImpl.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import com.cloud.event.ActionEventUtils;
2626
import com.cloud.event.EventTypes;
2727
import com.cloud.event.EventVO;
28+
import com.cloud.exception.AgentUnavailableException;
29+
import com.cloud.exception.OperationTimedoutException;
2830
import com.cloud.host.Host;
2931
import com.cloud.host.HostVO;
3032
import com.cloud.host.Status;
@@ -62,6 +64,7 @@
6264
import org.apache.cloudstack.agent.directdownload.MetalinkDirectDownloadCommand;
6365
import org.apache.cloudstack.agent.directdownload.NfsDirectDownloadCommand;
6466
import org.apache.cloudstack.agent.directdownload.HttpsDirectDownloadCommand;
67+
import org.apache.cloudstack.agent.directdownload.RevokeDirectDownloadCertificateCommand;
6568
import org.apache.cloudstack.agent.directdownload.SetupDirectDownloadCertificateCommand;
6669
import org.apache.cloudstack.context.CallContext;
6770
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
@@ -400,4 +403,31 @@ protected boolean uploadCertificate(String certificate, String certificateName,
400403
s_logger.info("Certificate " + certificateName + " successfully uploaded to host: " + hostId);
401404
return true;
402405
}
406+
407+
@Override
408+
public boolean revokeCertificateAlias(String certificateAlias, String hypervisor) {
409+
HypervisorType hypervisorType = HypervisorType.getType(hypervisor);
410+
List<HostVO> hosts = getRunningHostsToUploadCertificate(hypervisorType);
411+
s_logger.info("Attempting to revoke certificate alias: " + certificateAlias + " from " + hosts.size() + " hosts");
412+
if (CollectionUtils.isNotEmpty(hosts)) {
413+
for (HostVO host : hosts) {
414+
if (!revokeCertificateAliasFromHost(certificateAlias, host.getId())) {
415+
s_logger.error("Could not revoke certificate from host: " + host.getName() + " (" + host.getUuid() + ")");
416+
throw new CloudRuntimeException("Revoking certificate " + certificateAlias + " failed from host: " + host.getUuid());
417+
}
418+
}
419+
}
420+
return true;
421+
}
422+
423+
protected boolean revokeCertificateAliasFromHost(String alias, Long hostId) {
424+
RevokeDirectDownloadCertificateCommand cmd = new RevokeDirectDownloadCertificateCommand(alias);
425+
try {
426+
Answer answer = agentManager.send(hostId, cmd);
427+
return answer != null && answer.getResult();
428+
} catch (AgentUnavailableException | OperationTimedoutException e) {
429+
s_logger.error("Error revoking certificate " + alias + " from host " + hostId, e);
430+
}
431+
return false;
432+
}
403433
}

server/test/org/apache/cloudstack/direct/download/DirectDownloadManagerImplTest.java

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,25 +51,25 @@ public class DirectDownloadManagerImplTest {
5151
private static final String HTTP_HEADER_2 = "Accept-Encoding";
5252
private static final String HTTP_VALUE_2 = "gzip";
5353

54-
private static final String VALID_HUNDRED_YEARS_CERTIFICATE =
55-
"MIIDSzCCAjMCFExHvjXI7ffTZqcH4urbc6bqazFaMA0GCSqGSIb3DQEBCwUAMGEx" +
56-
"CzAJBgNVBAYTAkNTMQswCQYDVQQIDAJDUzELMAkGA1UEBwwCQ1MxCzAJBgNVBAoM" +
57-
"AkNTMQswCQYDVQQLDAJDUzELMAkGA1UEAwwCQ1MxETAPBgkqhkiG9w0BCQEWAkNT" +
58-
"MCAXDTE5MDQwMzEzMzgyOFoYDzIxMTkwMzEwMTMzODI4WjBhMQswCQYDVQQGEwJD" +
59-
"UzELMAkGA1UECAwCQ1MxCzAJBgNVBAcMAkNTMQswCQYDVQQKDAJDUzELMAkGA1UE" +
60-
"CwwCQ1MxCzAJBgNVBAMMAkNTMREwDwYJKoZIhvcNAQkBFgJDUzCCASIwDQYJKoZI" +
61-
"hvcNAQEBBQADggEPADCCAQoCggEBANEuKuCOjnRJZtqBeKwZD4XNVDTKmxGLNJ3j" +
62-
"6q71qlLa8quu115di6IxHkeerB9XnQMHHmqCv1qgpoWDuxA8uAra8T/teCvQjGRl" +
63-
"lWDlBBajFZZ4Crsj0MxGIbuoHTQ4Ossyv3vJztbm+RZ79nTEA35xzQj7HxeFVyk+" +
64-
"zqC6e4mMCzhI+UTKd3sOZBt8/y34egCv2UK9Lso9950dHnmlXYREd1j85Kestqjh" +
65-
"tKntw3DLo5i8RLQ/11iW4Z+xOlL11ubhvJ0S8UAF5BU8pcLMNv+WztaoAAc3N+Yc" +
66-
"WSTIXjQUtMT9TlHec8+NKlF9e62o4XMNHiaGEOf1idXC2URqqy8CAwEAATANBgkq" +
67-
"hkiG9w0BAQsFAAOCAQEAFSAjv8dw0Lo8U7CsjWNlW/LBZdP9D54vx0kXOLyWjeYH" +
68-
"7u4DTikoigjunm1lB5QPL2k5jSQEpTclcN313hMGCEMW9GZEtcSoUxqkiTEtKlyw" +
69-
"cC/PO/NHHgDrp1Fg9yhUOLKXJyBp9bfjKtm2YqPNyrpTB5cLfjRp69Hx5G5KuKCm" +
70-
"fAxcVdrfUluu6l+d4Y4FnuvS3rb9rDy/ES36SXczXNQFAzvI8ZuTlgxTRIvM184N" +
71-
"GA0utaoFPJAzZ01HYlRSYmipHx6NZE7roTAC5wmT3R1jkFlfkw8LSBynsR6U6Vkw" +
72-
"90kMmEH4NoYTV+mF4A0iY+NkEsuvnSsqheDknO/8OA==";
54+
private static final String VALID_CERTIFICATE =
55+
"MIIDSzCCAjMCFDa0LoW+1O8/cEwCI0nIqfl8c1TLMA0GCSqGSIb3DQEBCwUAMGEx\n" +
56+
"CzAJBgNVBAYTAkNTMQswCQYDVQQIDAJDUzELMAkGA1UEBwwCQ1MxCzAJBgNVBAoM\n" +
57+
"AkNTMQswCQYDVQQLDAJDUzELMAkGA1UEAwwCQ1MxETAPBgkqhkiG9w0BCQEWAkNT\n" +
58+
"MCAXDTE5MDQyNDE1NTIzNVoYDzIwOTgwOTE1MTU1MjM1WjBhMQswCQYDVQQGEwJD\n" +
59+
"UzELMAkGA1UECAwCQ1MxCzAJBgNVBAcMAkNTMQswCQYDVQQKDAJDUzELMAkGA1UE\n" +
60+
"CwwCQ1MxCzAJBgNVBAMMAkNTMREwDwYJKoZIhvcNAQkBFgJDUzCCASIwDQYJKoZI\n" +
61+
"hvcNAQEBBQADggEPADCCAQoCggEBAKstLRcMGCo6+2hojRMjEuuimnWp27yfYhDU\n" +
62+
"w/Cj03MJe/KCOhwsDqX82QNIr/bNtLdFf2ZJEUQd08sLLlHeUy9y5aOcxt9SGx2j\n" +
63+
"xolqO4MBL7BW3dklO0IvjaEfBeFP6udz8ajeVur/iPPZb2Edd0zlXuHvDozfQisv\n" +
64+
"bpuJImnTUVx0ReCXP075PBGvlqQXW2uEht+E/w3H8/2rra3JFV6J5xc77KyQSq2t\n" +
65+
"1+2ZU7PJiy/rppXf5rjTvNm6ydfag8/av7lcgs2ntdkK4koAmkmROhAwNonlL7cD\n" +
66+
"xIC83cKOqOFiQXSwr1IgoLf7zBNafKoTlSb/ev6Zt18BXEMLGpkCAwEAATANBgkq\n" +
67+
"hkiG9w0BAQsFAAOCAQEAVS5uWZRz2m3yx7EUQm47RTMW5WMXU4pI8D+N5WZ9xubY\n" +
68+
"OqtU3r2OAYpfL/QO8iT7jcqNYGoDqe8ZjEaNvfxiTG8cOI6TSXhKBG6hjSaSFQSH\n" +
69+
"OZ5mfstM36y/3ENFh6JCJ2ao1rgWSbfDRyAaHuvt6aCkaV6zRq2OMEgoJqZSgwxL\n" +
70+
"QO230xa2hYgKXOePMVZyHFA2oKJtSOc3jCke9Y8zDUwm0McGdMRBD8tVB0rcaOqQ\n" +
71+
"0PlDLjB9sQuhhLu8vjdgbznmPbUmMG7JN0yhT1eJbIX5ImXyh0DoTwiaGcYwW6Sq\n" +
72+
"YodjXACsC37xaQXAPYBiaAs4iI80TJSx1DVFO1LV0g==";
7373

7474
@Before
7575
public void setUp() {
@@ -127,13 +127,13 @@ public void testGetHeadersFromDetailsNonHttpHeaders() {
127127

128128
@Test
129129
public void testCertificateSanityValidCertificate() {
130-
String pretifiedCertificate = manager.getPretifiedCertificate(VALID_HUNDRED_YEARS_CERTIFICATE);
130+
String pretifiedCertificate = manager.getPretifiedCertificate(VALID_CERTIFICATE);
131131
manager.certificateSanity(pretifiedCertificate);
132132
}
133133

134134
@Test(expected = CloudRuntimeException.class)
135135
public void testCertificateSanityInvalidCertificate() {
136-
String pretifiedCertificate = manager.getPretifiedCertificate(VALID_HUNDRED_YEARS_CERTIFICATE + "xxx");
136+
String pretifiedCertificate = manager.getPretifiedCertificate(VALID_CERTIFICATE + "xxx");
137137
manager.certificateSanity(pretifiedCertificate);
138138
}
139139
}

0 commit comments

Comments
 (0)