Skip to content

Commit fee24d9

Browse files
committed
Add revoke certificates API
1 parent 50869fe commit fee24d9

7 files changed

Lines changed: 280 additions & 1 deletion

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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.api.command.admin.direct.download;
20+
21+
import com.cloud.exception.ConcurrentOperationException;
22+
import com.cloud.exception.InsufficientCapacityException;
23+
import com.cloud.exception.NetworkRuleConflictException;
24+
import com.cloud.exception.ResourceAllocationException;
25+
import com.cloud.exception.ResourceUnavailableException;
26+
import org.apache.cloudstack.acl.RoleType;
27+
import org.apache.cloudstack.api.APICommand;
28+
import org.apache.cloudstack.api.ApiConstants;
29+
import org.apache.cloudstack.api.ApiErrorCode;
30+
import org.apache.cloudstack.api.BaseCmd;
31+
import org.apache.cloudstack.api.Parameter;
32+
import org.apache.cloudstack.api.ServerApiException;
33+
import org.apache.cloudstack.api.response.SuccessResponse;
34+
import org.apache.cloudstack.context.CallContext;
35+
import org.apache.cloudstack.direct.download.DirectDownloadManager;
36+
import org.apache.log4j.Logger;
37+
38+
import javax.inject.Inject;
39+
40+
@APICommand(name = RevokeTemplateDirectDownloadCertificateCmd.APINAME,
41+
description = "Revoke a certificate alias from a KVM host",
42+
responseObject = SuccessResponse.class,
43+
requestHasSensitiveInfo = true,
44+
responseHasSensitiveInfo = true,
45+
since = "4.13",
46+
authorized = {RoleType.Admin})
47+
public class RevokeTemplateDirectDownloadCertificateCmd extends BaseCmd {
48+
49+
@Inject
50+
DirectDownloadManager directDownloadManager;
51+
52+
private static final Logger LOG = Logger.getLogger(RevokeTemplateDirectDownloadCertificateCmd.class);
53+
public static final String APINAME = "revokeTemplateDirectDownloadCertificate";
54+
55+
@Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, required = true,
56+
description = "alias of the SSL certificate")
57+
private String certificateAlias;
58+
59+
@Parameter(name = ApiConstants.HYPERVISOR, type = BaseCmd.CommandType.STRING, required = true,
60+
description = "Hypervisor type")
61+
private String hypervisor;
62+
63+
@Override
64+
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
65+
if (!hypervisor.equalsIgnoreCase("kvm")) {
66+
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Currently supporting KVM hosts only");
67+
}
68+
SuccessResponse response = new SuccessResponse(getCommandName());
69+
try {
70+
LOG.debug("Revoking certificate " + certificateAlias + " from " + hypervisor + " hosts");
71+
boolean result = directDownloadManager.revokeCertificateAlias(certificateAlias, hypervisor);
72+
response.setSuccess(result);
73+
setResponseObject(response);
74+
} catch (Exception e) {
75+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
76+
}
77+
}
78+
79+
@Override
80+
public String getCommandName() {
81+
return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
82+
}
83+
84+
@Override
85+
public long getEntityOwnerId() {
86+
return CallContext.current().getCallingAccount().getId();
87+
}
88+
}

api/src/main/java/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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
import java.io.IOException;
36+
37+
import static org.apache.commons.lang.StringUtils.isBlank;
38+
39+
@ResourceWrapper(handles = RevokeDirectDownloadCertificateCommand.class)
40+
public class LibvirtRevokeDirectDownloadCertificateWrapper extends CommandWrapper<RevokeDirectDownloadCertificateCommand, Answer, LibvirtComputingResource> {
41+
42+
private static final Logger s_logger = Logger.getLogger(LibvirtRevokeDirectDownloadCertificateWrapper.class);
43+
44+
/**
45+
* Retrieve agent.properties file
46+
*/
47+
private File getAgentPropertiesFile() throws FileNotFoundException {
48+
final File agentFile = PropertiesUtil.findConfigFile("agent.properties");
49+
if (agentFile == null) {
50+
throw new FileNotFoundException("Failed to find agent.properties file");
51+
}
52+
return agentFile;
53+
}
54+
55+
/**
56+
* Get the property 'keystore.passphrase' value from agent.properties file
57+
*/
58+
private String getKeystorePassword(File agentFile) {
59+
String pass = null;
60+
if (agentFile != null) {
61+
try {
62+
pass = PropertiesUtil.loadFromFile(agentFile).getProperty(KeyStoreUtils.KS_PASSPHRASE_PROPERTY);
63+
} catch (IOException e) {
64+
s_logger.error("Could not get 'keystore.passphrase' property value due to: " + e.getMessage());
65+
}
66+
}
67+
return pass;
68+
}
69+
70+
/**
71+
* Get keystore path
72+
*/
73+
private String getKeyStoreFilePath(File agentFile) {
74+
return agentFile.getParent() + "/" + KeyStoreUtils.KS_FILENAME;
75+
}
76+
77+
@Override
78+
public Answer execute(RevokeDirectDownloadCertificateCommand command, LibvirtComputingResource serverResource) {
79+
String certificateAlias = command.getCertificateAlias();
80+
try {
81+
File agentFile = getAgentPropertiesFile();
82+
String privatePassword = getKeystorePassword(agentFile);
83+
if (isBlank(privatePassword)) {
84+
return new Answer(command, false, "No password found for keystore: " + KeyStoreUtils.KS_FILENAME);
85+
}
86+
87+
final String keyStoreFile = getKeyStoreFilePath(agentFile);
88+
89+
String checkCmd = String.format("keytool -list -alias %s -keystore %s -storepass %s",
90+
certificateAlias, keyStoreFile, privatePassword);
91+
int existsCmdResult = Script.runSimpleBashScriptForExitValue(checkCmd);
92+
if (existsCmdResult == 1) {
93+
s_logger.error("Certificate alias " + certificateAlias + " does not exist, no need to revoke it");
94+
} else {
95+
String revokeCmd = String.format("keytool -delete -alias %s -keystore %s -storepass %s",
96+
certificateAlias, keyStoreFile, privatePassword);
97+
s_logger.debug("Revoking certificate alias " + certificateAlias + " from keystore " + keyStoreFile);
98+
Script.runSimpleBashScriptForExitValue(revokeCmd);
99+
}
100+
} catch (FileNotFoundException | CloudRuntimeException e) {
101+
s_logger.error("Error while setting up certificate " + certificateAlias, e);
102+
return new Answer(command, false, e.getMessage());
103+
}
104+
return new Answer(command);
105+
}
106+
}

server/src/main/java/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;
@@ -3075,6 +3076,7 @@ public List<Class<?>> getCommands() {
30753076
cmdList.add(CreateManagementNetworkIpRangeCmd.class);
30763077
cmdList.add(DeleteManagementNetworkIpRangeCmd.class);
30773078
cmdList.add(UploadTemplateDirectDownloadCertificateCmd.class);
3079+
cmdList.add(RevokeTemplateDirectDownloadCertificateCmd.class);
30783080
cmdList.add(ListMgmtsCmd.class);
30793081

30803082
// Out-of-band management APIs for admins

server/src/main/java/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;
@@ -403,4 +406,31 @@ protected boolean uploadCertificate(String certificate, String certificateName,
403406
return true;
404407
}
405408

409+
@Override
410+
public boolean revokeCertificateAlias(String certificateAlias, String hypervisor) {
411+
HypervisorType hypervisorType = HypervisorType.getType(hypervisor);
412+
List<HostVO> hosts = getRunningHostsToUploadCertificate(hypervisorType);
413+
s_logger.info("Attempting to revoke certificate alias: " + certificateAlias + " from " + hosts.size() + " hosts");
414+
if (CollectionUtils.isNotEmpty(hosts)) {
415+
for (HostVO host : hosts) {
416+
if (!revokeCertificateAliasFromHost(certificateAlias, host.getId())) {
417+
String msg = "Could not revoke certificate from host: " + host.getName() + " (" + host.getUuid() + ")";
418+
s_logger.error(msg);
419+
throw new CloudRuntimeException(msg);
420+
}
421+
}
422+
}
423+
return true;
424+
}
425+
426+
protected boolean revokeCertificateAliasFromHost(String alias, Long hostId) {
427+
RevokeDirectDownloadCertificateCommand cmd = new RevokeDirectDownloadCertificateCommand(alias);
428+
try {
429+
Answer answer = agentManager.send(hostId, cmd);
430+
return answer != null && answer.getResult();
431+
} catch (AgentUnavailableException | OperationTimedoutException e) {
432+
s_logger.error("Error revoking certificate " + alias + " from host " + hostId, e);
433+
}
434+
return false;
435+
}
406436
}

test/integration/smoke/test_direct_download.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from marvin.lib.common import (get_pod,
2828
get_zone)
2929
from nose.plugins.attrib import attr
30-
from marvin.cloudstackAPI import uploadTemplateDirectDownloadCertificate
30+
from marvin.cloudstackAPI import (uploadTemplateDirectDownloadCertificate, revokeTemplateDirectDownloadCertificate)
3131
from marvin.lib.decoratorGenerators import skipTestIf
3232

3333

@@ -120,6 +120,7 @@ def test_02_upload_direct_download_certificates(self):
120120

121121
# Validate the following
122122
# 1. Valid certificates are uploaded to hosts
123+
# 2. Revoke uploaded certificate from host
123124

124125
cmd = uploadTemplateDirectDownloadCertificate.uploadTemplateDirectDownloadCertificateCmd()
125126
cmd.hypervisor = self.hypervisor
@@ -131,6 +132,15 @@ def test_02_upload_direct_download_certificates(self):
131132
except Exception as e:
132133
self.fail("Valid certificate must be uploaded")
133134

135+
revokecmd = revokeTemplateDirectDownloadCertificate.revokeTemplateDirectDownloadCertificateCmd()
136+
revokecmd.hypervisor = self.hypervisor
137+
revokecmd.name = cmd.name
138+
139+
try:
140+
self.apiclient.revokeTemplateDirectDownloadCertificate(cmd)
141+
except Exception as e:
142+
self.fail("Uploaded certificates should be revoked when needed")
143+
134144
return
135145

136146

0 commit comments

Comments
 (0)